graterm

Provides primitives to perform ordered GRAceful TERMination (aka shutdown) in Go application.
⚡ ️️Description
Library provides fluent methods to register ordered application termination (aka shutdown) hooks,
and block the main goroutine until the registered os.Signal
will occur.
Termination hooks registered with the
same Order will be executed concurrently.
It is possible to set individual timeouts for each registered termination hook
and global termination timeout for the whole application.
🎯 Features
- Dependency only on a standard Go library (except tests).
- Component-agnostic (can be adapted to any 3rd party technology).
- Clean and tested code: 100% test coverage, including goroutine leak tests.
- Rich set of examples.
⚙️ Usage
Get the library:
go get -u github.com/subtleutens/graterm
Import the library into the project:
import (
"github.com/subtleutens/graterm"
)
Create a new instance of Terminator and get an application context
that will be cancelled when one of the registered os.Signal
s will occur:
terminator, appCtx := graterm.NewWithSignals(context.Background(), syscall.SIGINT, syscall.SIGTERM)
terminator.SetLogger(log.Default())
Optionally define Order of components to be terminated at the end:
const (
HTTPServerTerminationOrder graterm.Order = 1
MessagingTerminationOrder graterm.Order = 1
DBTerminationOrder graterm.Order = 2
)
Register some termination Hooks with priorities:
terminator.WithOrder(HTTPServerTerminationOrder).
WithName("HTTP Server").
Register(1*time.Second, func(ctx context.Context) {
if err := httpServer.Shutdown(ctx); err != nil {
log.Printf("shutdown HTTP Server: %+v\n", err)
}
})
Block main goroutine until the application receives one of the registered os.Signal
s:
if err := terminator.Wait(appCtx, 20 * time.Second); err != nil {
log.Printf("graceful termination period was timed out: %+v", err)
}
👀 Versioning
The library follows SemVer policy. With the release of v1.0.0 the public API is stable.
📚 Example
Each public function has example attached to it. Here is the simple one:
package main
import (
"context"
"log"
"syscall"
"time"
"github.com/subtleutens/graterm"
)
func main() {
const (
HTTPServerTerminationOrder graterm.Order = 1
MessagingTerminationOrder graterm.Order = 1
DBTerminationOrder graterm.Order = 2
)
terminator, appCtx := graterm.NewWithSignals(context.Background(), syscall.SIGINT, syscall.SIGTERM)
terminator.SetLogger(log.Default())
terminator.WithOrder(HTTPServerTerminationOrder).
WithName("HTTP Server").
Register(1*time.Second, func(ctx context.Context) {
log.Println("terminating HTTP Server...")
defer log.Println("...HTTP Server terminated")
})
terminator.WithOrder(MessagingTerminationOrder).
Register(1*time.Second, func(ctx context.Context) {
log.Println("terminating Messaging...")
defer log.Println("...Messaging terminated")
})
terminator.WithOrder(DBTerminationOrder).
WithName("DB").
Register(1*time.Second, func(ctx context.Context) {
log.Println("terminating DB...")
defer log.Println("...DB terminated")
const sleepTime = 3 * time.Second
select {
case <-time.After(sleepTime):
log.Printf("DB termination sleep time %v is over\n", sleepTime)
case <-ctx.Done():
log.Printf("DB termination Context is Done because of: %+v\n", ctx.Err())
}
})
if err := terminator.Wait(appCtx, 20 * time.Second); err != nil {
log.Printf("graceful termination period was timed out: %+v", err)
}
}
💡 Integration with HTTP server
The library doesn't have out of the box support to start/terminate the HTTP server, but that's easy to handle:
package main
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"syscall"
"time"
"github.com/subtleutens/graterm"
)
func main() {
const HTTPServerTerminationOrder graterm.Order = 1
terminator, appCtx := graterm.NewWithSignals(context.Background(), syscall.SIGINT, syscall.SIGTERM)
terminator.SetLogger(log.Default())
httpServer := &http.Server{
Addr: ":8080",
Handler: http.DefaultServeMux,
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello, world!")
})
go func() {
if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Printf("terminated HTTP Server: %+v\n", err)
}
}()
terminator.WithOrder(HTTPServerTerminationOrder).
WithName("HTTPServer").
Register(10*time.Second, func(ctx context.Context) {
if err := httpServer.Shutdown(ctx); err != nil {
log.Printf("shutdown HTTP Server: %+v\n", err)
}
})
if err := terminator.Wait(appCtx, 30*time.Second); err != nil {
log.Printf("graceful termination period is timed out: %+v\n", err)
}
}
The full-fledged example located here: example.go
📖 Testing
Unit-tests with code coverage:
make test
Run linter:
make code-quality
⚠️ LICENSE
MIT
🕶️ AUTHORS