From 120c12a1f1cdebc7614a3e9fca26606377c232c1 Mon Sep 17 00:00:00 2001 From: Jonas Date: Tue, 7 Apr 2026 14:29:25 +0200 Subject: [PATCH] feat: tokenization start, waitlist for background task graceful exit --- projects/greenlight/cmd/api/helpers.go | 3 ++ projects/greenlight/cmd/api/main.go | 4 +- projects/greenlight/cmd/api/server.go | 41 +++++++++---------- .../greenlight-bruno/Users/Create User.yml | 2 +- .../000005_create_tokens_table.down.sql | 1 + .../000005_create_tokens_table.up.sql | 7 ++++ 6 files changed, 35 insertions(+), 23 deletions(-) create mode 100644 projects/greenlight/migrations/000005_create_tokens_table.down.sql create mode 100644 projects/greenlight/migrations/000005_create_tokens_table.up.sql diff --git a/projects/greenlight/cmd/api/helpers.go b/projects/greenlight/cmd/api/helpers.go index 8297d16..02e75b2 100644 --- a/projects/greenlight/cmd/api/helpers.go +++ b/projects/greenlight/cmd/api/helpers.go @@ -178,8 +178,11 @@ func (app *application) readInt(qs url.Values, key string, defaultValue int, v * // The background() helper accepts an arbitrary function as a parameter. func (app *application) background(fn func()) { + app.wg.Add(1) // Launch a background goroutine. go func() { + // Use defer to decrement the WaitGroup counter before the goroutine returns. + defer app.wg.Done() // Recover any panic. defer func() { if err := recover(); err != nil { diff --git a/projects/greenlight/cmd/api/main.go b/projects/greenlight/cmd/api/main.go index 4f12679..f8a3d21 100644 --- a/projects/greenlight/cmd/api/main.go +++ b/projects/greenlight/cmd/api/main.go @@ -5,6 +5,7 @@ import ( "database/sql" "flag" "os" + "sync" "time" "github.com/joho/godotenv" @@ -57,6 +58,7 @@ type application struct { logger *jsonlog.Logger models data.Models mailer mailer.Mailer + wg sync.WaitGroup } func main() { @@ -94,7 +96,7 @@ func main() { // make sure to replace the default values for smtp-username and smtp-password // with your own Mailtrap credentials. flag.StringVar(&cfg.smtp.host, "smtp-host", "sandbox.smtp.mailtrap.io", "SMTP host") - flag.IntVar(&cfg.smtp.port, "smtp-port", 25, "SMTP port") + flag.IntVar(&cfg.smtp.port, "smtp-port", 2525, "SMTP port") flag.StringVar(&cfg.smtp.username, "smtp-username", "3d946287da35ea", "SMTP username") flag.StringVar(&cfg.smtp.password, "smtp-password", "d06a774b484ca3", "SMTP password") flag.StringVar(&cfg.smtp.sender, "smtp-sender", "Greenlight ", "SMTP sender") diff --git a/projects/greenlight/cmd/api/server.go b/projects/greenlight/cmd/api/server.go index 75f13f2..5783492 100644 --- a/projects/greenlight/cmd/api/server.go +++ b/projects/greenlight/cmd/api/server.go @@ -20,31 +20,39 @@ func (app *application) serve() error { WriteTimeout: 30 * time.Second, } - // Create a shutdownError channel. We will use this to receive any errors returned - // by the graceful Shutdown() function. shutdownError := make(chan error) go func() { quit := make(chan os.Signal, 1) - // Intercept the signals, as before. signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) s := <-quit - // Update the log entry to say "shutting down server" instead of "caught signal". - app.logger.PrintInfo("shutting down server", map[string]string{ + app.logger.PrintInfo("caught signal", map[string]string{ "signal": s.String(), }) - // Create a context with a 5-second timeout. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - // Call Shutdown() on our server, passing in the context we just made. - // Shutdown() will return nil if the graceful shutdown was successful, or an - // error (which may happen because of a problem closing the listeners, or - // because the shutdown didn't complete before the 5-second context deadline is - // hit). We relay this return value to the shutdownError channel. - shutdownError <- srv.Shutdown(ctx) + // Call Shutdown() on the server like before, but now we only send on the + // shutdownError channel if it returns an error. + err := srv.Shutdown(ctx) + if err != nil { + shutdownError <- err + } + + // Log a message to say that we're waiting for any background goroutines to + // complete their tasks. + app.logger.PrintInfo("completing background tasks", map[string]string{ + "addr": srv.Addr, + }) + + // Call Wait() to block until our WaitGroup counter is zero --- essentially + // blocking until the background goroutines have finished. Then we return nil on + // the shutdownError channel, to indicate that the shutdown completed without + // any issues. + app.wg.Wait() + shutdownError <- nil }() app.logger.PrintInfo("starting server", map[string]string{ @@ -52,25 +60,16 @@ func (app *application) serve() error { "env": app.config.env, }) - // Calling Shutdown() on our server will cause ListenAndServe() to immediately - // return a http.ErrServerClosed error. So if we see this error, it is actually a - // good thing and an indication that the graceful shutdown has started. So we check - // specifically for this, only returning the error if it is NOT http.ErrServerClosed. err := srv.ListenAndServe() if !errors.Is(err, http.ErrServerClosed) { return err } - // Otherwise, we wait to receive the return value from Shutdown() on the - // shutdownError channel. If return value is an error, we know that there was a - // problem with the graceful shutdown and we return the error. err = <-shutdownError if err != nil { return err } - // At this point we know that the graceful shutdown completed successfully and we - // log a "stopped server" message. app.logger.PrintInfo("stopped server", map[string]string{ "addr": srv.Addr, }) diff --git a/projects/greenlight/docs/api/greenlight-bruno/Users/Create User.yml b/projects/greenlight/docs/api/greenlight-bruno/Users/Create User.yml index fbd38ad..26198a5 100644 --- a/projects/greenlight/docs/api/greenlight-bruno/Users/Create User.yml +++ b/projects/greenlight/docs/api/greenlight-bruno/Users/Create User.yml @@ -11,7 +11,7 @@ http: data: |- { "name": "Carol 3", - "email": "carol3@example.com", + "email": "carol5@example.com", "password": "pa55word" } auth: inherit diff --git a/projects/greenlight/migrations/000005_create_tokens_table.down.sql b/projects/greenlight/migrations/000005_create_tokens_table.down.sql new file mode 100644 index 0000000..1029218 --- /dev/null +++ b/projects/greenlight/migrations/000005_create_tokens_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS tokens; diff --git a/projects/greenlight/migrations/000005_create_tokens_table.up.sql b/projects/greenlight/migrations/000005_create_tokens_table.up.sql new file mode 100644 index 0000000..e4ceb5e --- /dev/null +++ b/projects/greenlight/migrations/000005_create_tokens_table.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS tokens ( + hash bytea PRIMARY KEY, + user_id bigint NOT NULL REFERENCES users ON DELETE CASCADE, + expiry timestamp(0) with time zone NOT NULL, + scope text NOT NULL +); +