diff --git a/cookbook/graceful-multi/server.go b/cookbook/graceful-multi/server.go new file mode 100644 index 00000000..b9ceb9fb --- /dev/null +++ b/cookbook/graceful-multi/server.go @@ -0,0 +1,110 @@ +package main + +import ( + "context" + "fmt" + "net/http" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/labstack/echo/v4" + "github.com/labstack/gommon/log" + "golang.org/x/sync/errgroup" +) + +// TaskFunc is a errorgroup.Go compatible function signature +type TaskFunc func() error + +func main() { + // create a context that automatically cancels thenever one of the + // configured os signals is received + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGKILL) + defer stop() + + // HTTP servers + e1 := NewHTTPServer() + e2 := NewHTTPServer() + + // an errorgroup runs multiple goroutines, and allows all goroutines to be + // stopped whenever one of the goroutines fails/errors + group, gctx := errgroup.WithContext(ctx) + group.Go(NewStartServerTask(e1, ":8888")) + group.Go(NewStartServerTask(e2, ":8889")) + group.Go(NewWatchDogTask(gctx, stop, e1, e2, 10*time.Second)) + + // simply wait for all tasks in the group to be done + if err := group.Wait(); err != nil && err != context.Canceled && err != http.ErrServerClosed { + fmt.Printf("error: %v\n", err) + } + fmt.Println("exit.") +} + +// NewHTTPServer returns a dummy echo instance +func NewHTTPServer() *echo.Echo { + e := echo.New() + e.HideBanner = true + e.Logger.SetLevel(log.INFO) + e.GET("/", func(c echo.Context) error { + time.Sleep(3 * time.Second) + return c.JSON(http.StatusOK, "OK") + }) + // please note we can't use Logger.Fatal, because echo has implemented that + // with an os.Exit(1), which prevents any graceful handling from code + e.GET("/panic", func(c echo.Context) error { + e.Logger.Error("something went horribly wrong..") + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + return e.Shutdown(ctx) + }) + return e +} + +// NewStartServerTask returns a TaskFunc that starts/runs a HTTP server +func NewStartServerTask(e *echo.Echo, listenAddr string) TaskFunc { + return func() error { + return e.Start(listenAddr) + } +} + +// NewWatchDogTask returns the watchdog task that is responsible for doing +// a graceful shutdown of the HTTP servers when an os interrupt is received +func NewWatchDogTask(ctx context.Context, stop context.CancelFunc, e1, e2 *echo.Echo, timeout time.Duration) TaskFunc { + return func() error { + // waitgroup for parallellising shutdown of multiple servers + var wg sync.WaitGroup + + // wait for context cancellation (== os interrupt, e.g. ctrl-C) + <-ctx.Done() + stop() + fmt.Println("signal received: shutting down servers") + + wg.Add(1) + go TryGracefulShutdown(&wg, e1, timeout) + wg.Add(1) + go TryGracefulShutdown(&wg, e2, timeout) + + // wait for shutdown routines to finish + wg.Wait() + fmt.Println("shutdown complete") + return nil + } +} + +// TryGracefulShutdown tries to shutdown a server, giving it a maximum timeout +// for finishing the shutdown. Any shutdown errors are logged. +func TryGracefulShutdown(wg *sync.WaitGroup, e *echo.Echo, timeout time.Duration) { + // decrement waitgroup to signal this gorouting has finished when we return + defer wg.Done() + + // set context with a timeout to use for graceful shutdown + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + // try graceful shutdown + e.Logger.Warn("received shutdown signal") + if err := e.Shutdown(ctx); err != nil { + fmt.Printf("error shutting down server: %v\n", err) + } +} diff --git a/go.mod b/go.mod index 1d1259ba..0e6a719e 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,7 @@ require ( github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect + golang.org/x/sync v0.1.0 golang.org/x/sys v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect diff --git a/go.sum b/go.sum index 49ad9222..69095ae6 100644 --- a/go.sum +++ b/go.sum @@ -97,6 +97,7 @@ golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/website/docs/cookbook/graceful-multi.md b/website/docs/cookbook/graceful-multi.md new file mode 100644 index 00000000..098fec60 --- /dev/null +++ b/website/docs/cookbook/graceful-multi.md @@ -0,0 +1,27 @@ +--- +description: Graceful shutdown for multiple instances recipe +--- + +# Multi-instance + +This example will start two echo instances that can not only be shutdown gracefully +using ctrl-C, but also automatically tries a graceful shutdown of the other servers +when one (or more) instance(s) shutdown/fail. + +This allows for e.g. running seperate echo instances for e.g. a private and public +api on different ports in a single binary. + +Using [signal.NotifyContext()](https://pkg.go.dev/os/signal#NotifyContext) +and [errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) + +## Recipe + +```go reference +https://github.com/labstack/echox/blob/master/cookbook/graceful-multi/server.go +``` + +:::note + +Requires go1.16+ + +:::