diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a8582390..d4b5959b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,8 @@ - Trigger the `app.OnTerminate()` hook on `app.Restart()` call. _A new bool `IsRestart` field was also added to the `core.TerminateEvent` event._ +- Fixed the graceful shutdown handling. + ## v0.20.0-rc3 diff --git a/apis/serve.go b/apis/serve.go index d0063d2c0..c4c172900 100644 --- a/apis/serve.go +++ b/apis/serve.go @@ -8,6 +8,7 @@ import ( "net/http" "path/filepath" "strings" + "sync" "time" "github.com/fatih/color" @@ -189,14 +190,37 @@ func Serve(app core.App, config ServeConfig) (*http.Server, error) { regular.Printf("└─ Admin UI: %s\n", color.CyanString("%s://%s/_/", schema, addr)) } + // WaitGroup to block until server.ShutDown() returns because Serve and similar methods exit immediately. + // Note that the WaitGroup would not do anything if the app.OnTerminate() hook isn't triggered. + var wg sync.WaitGroup + // try to gracefully shutdown the server on app termination app.OnTerminate().Add(func(e *core.TerminateEvent) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() + + wg.Add(1) server.Shutdown(ctx) + if e.IsRestart { + // wait for execve up to 3 seconds before exit + time.AfterFunc(3*time.Second, func() { + wg.Done() + }) + } else { + wg.Done() + } + return nil }) + // wait for the graceful shutdown to complete before exit + defer wg.Wait() + + // --- + // @todo consider removing the server return value because it is + // not really useful when combined with the blocking serve calls + // --- + // start HTTPS server if config.HttpsAddr != "" { // if httpAddr is set, start an HTTP server to redirect the traffic to the HTTPS version diff --git a/core/base.go b/core/base.go index f99ea71c9..e5330431f 100644 --- a/core/base.go +++ b/core/base.go @@ -553,20 +553,15 @@ func (app *BaseApp) Restart() error { return err } - // restart the app bootstrap as a fallback in case the - // terminate event or execve fails for some reason - defer app.Bootstrap() - - // optimistically trigger the terminate event - terminateErr := app.OnTerminate().Trigger(&TerminateEvent{ + return app.OnTerminate().Trigger(&TerminateEvent{ App: app, IsRestart: true, - }) - if terminateErr != nil { - return terminateErr - } + }, func(e *TerminateEvent) error { + // attempt to restart the bootstrap process in case execve returns an error for some reason + defer app.Bootstrap() - return syscall.Exec(execPath, os.Args, os.Environ()) + return syscall.Exec(execPath, os.Args, os.Environ()) + }) } // RefreshSettings reinitializes and reloads the stored application settings. diff --git a/examples/base/main.go b/examples/base/main.go index d5586067a..133714220 100644 --- a/examples/base/main.go +++ b/examples/base/main.go @@ -110,7 +110,7 @@ func main() { // GitHub selfupdate ghupdate.MustRegister(app, app.RootCmd, ghupdate.Config{}) - app.OnAfterBootstrap().Add(func(e *core.BootstrapEvent) error { + app.OnAfterBootstrap().PreAdd(func(e *core.BootstrapEvent) error { app.Dao().ModelQueryTimeout = time.Duration(queryTimeout) * time.Second return nil }) diff --git a/pocketbase.go b/pocketbase.go index 7995da5bc..95b874ffe 100644 --- a/pocketbase.go +++ b/pocketbase.go @@ -152,12 +152,13 @@ func (pb *PocketBase) Execute() error { sigch := make(chan os.Signal, 1) signal.Notify(sigch, os.Interrupt, syscall.SIGTERM) <-sigch + done <- true }() // execute the root command go func() { - // leave to the commands to decide whether to print their error or not + // note: leave to the commands to decide whether to print their error pb.RootCmd.Execute() done <- true