From 20a68c5e213c9fc872adb379f8646a79dbb9b0c2 Mon Sep 17 00:00:00 2001 From: neiljpowell <52715665+neiljpowell@users.noreply.github.com> Date: Wed, 4 Jun 2025 09:31:58 -0500 Subject: [PATCH 1/6] optional sigterm --- httpadapter/httpadapter.go | 51 +++++++++++++++++++-- httpadapter/listener.go | 91 +++++++++++++++++++++++++++++++++++--- 2 files changed, 134 insertions(+), 8 deletions(-) diff --git a/httpadapter/httpadapter.go b/httpadapter/httpadapter.go index 6c022fb..336def7 100644 --- a/httpadapter/httpadapter.go +++ b/httpadapter/httpadapter.go @@ -37,24 +37,69 @@ func SetLogger(l *slog.Logger) { logger = l } +type adapterOptions struct { + //baseContext context.Context + enableSIGTERM bool + sigtermFuncs []func() +} + +type Option func(*adapterOptions) + // Start starts the HTTP server on the specified port and listens for incoming requests. When a request is received, // it is converted to the appropriate Lambda event type and passed to the handler function. The response from the // handler function is then converted to an HTTP response and returned to the client. func Start(port int, handler interface{}) { + StartWithOptions(port, handler) +} + +// StartWithOptions starts the HTTP server on the specified port and listens for incoming requests. When a request is received, +// it is converted to the appropriate Lambda event type and passed to the handler function. The response from the +// handler function is then converted to an HTTP response and returned to the client. +func StartWithOptions(port int, handler interface{}, options ...Option) { + //if h, ok := handler.(*adapterOptions); ok { + // return h + //} + opts := &adapterOptions{} + for _, option := range options { + option(opts) + } + reflectHandler(handler) switch delegateHandlerType { case apigwV2HandlerType: - listenAndServe(port, handleRequestForApigwV2) + listenAndServe(port, handleRequestForApigwV2, opts) case albHandlerType: - listenAndServe(port, handleRequestForAlb) + listenAndServe(port, handleRequestForAlb, opts) case apigwHandlerType: - listenAndServe(port, handleRequestForApigw) + listenAndServe(port, handleRequestForApigw, opts) default: panic("unsupported handler type") } } +// WithEnableSIGTERM enables SIGTERM behavior with the HTTP Server for use with the provided handler function(s). +// The HTTP server will listen for SIGTERM signals and run the provided callback functions before +// gracefully shutting down and performing cleanup tasks before the server is terminated. If the server +// does not shut down within a certain time frame (usually 500ms), a SIGKILL signal will forcefully terminate +// the server (similar to how AWS Lambda handles function shutdowns). +// SIGKILL will occur ~500ms after SIGTERM. +// Optionally, an array of callback functions to run on SIGTERM may be provided. +// +// Usage: +// +// httpadapter.StartWithOptions(8080, Handler, +// lambda.WithEnableSIGTERM(func() { +// log.Print("HTTP server shutting down...") +// }) +// ) +func WithEnableSIGTERM(callbacks ...func()) Option { + return Option(func(opts *adapterOptions) { + opts.sigtermFuncs = append(opts.sigtermFuncs, callbacks...) + opts.enableSIGTERM = true + }) +} + // reflectHandler reflects the handler function to determine the input and output event types, and whether a context is // required. It panics if the function signature is not supported. func reflectHandler(handler interface{}) { diff --git a/httpadapter/listener.go b/httpadapter/listener.go index 979c236..86b1259 100644 --- a/httpadapter/listener.go +++ b/httpadapter/listener.go @@ -1,21 +1,102 @@ package httpadapter import ( + "context" "log/slog" "net/http" + "os" + "os/signal" "strconv" + "syscall" + "time" ) -func listenAndServe(port int, handler func(httpResponseWriter http.ResponseWriter, httpRequest *http.Request)) { +func listenAndServe(port int, handler func(httpResponseWriter http.ResponseWriter, httpRequest *http.Request), opts *adapterOptions) { logger.Info("Starting http listener.", slog.Int("port", port)) http.HandleFunc("/", handler) portStr := ":" + strconv.Itoa(port) - err := http.ListenAndServe(portStr, nil) - if err != nil { - logger.Error("Could not start http listener.", - slog.Any("err", err)) + server := &http.Server{ + Addr: portStr, + Handler: nil, // Use the typical defaultServerMux with the handler set by http.HandleFunc + } + + if !opts.enableSIGTERM { + defaultListenAndServe(server) //port, handler) + } else { + sigtermListenAndServe(server, opts) + } +} + +func defaultListenAndServe(server *http.Server) { //port int, handler func(httpResponseWriter http.ResponseWriter, httpRequest *http.Request)) { + + //http.HandleFunc("/", handler) + //portStr := ":" + strconv.Itoa(port) + //err := http.ListenAndServe(portStr, nil) + //err := server.ListenAndServe() + //if err != nil { + // logger.Error("Could not start http listener.", + // slog.Any("err", err)) + // panic(err) + //} + if err := server.ListenAndServe(); err != nil { //&& err != http.ErrServerClosed { + logger.Error("Could not start http listener.", slog.Any("err", err)) panic(err) } } + +func sigtermListenAndServe(server *http.Server, opts *adapterOptions) { + + registerSigtermFuncs(opts.sigtermFuncs) + + // Create a context that cancels on SIGTERM or SIGINT + sigCtx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) + defer stop() + + //http.HandleFunc("/", handler) + //portStr := ":" + strconv.Itoa(port) + //server := &http.Server{ + // Addr: portStr, + // Handler: nil, // Use the typical defaultServerMux with the handler set by http.HandleFunc + //} + go func() { + //if err := server.ListenAndServe(); err != nil { //&& err != http.ErrServerClosed { + // logger.Error("Could not start http listener.", slog.Any("err", err)) + // panic(err) + //} + defaultListenAndServe(server) + }() + + // Wait for the termination signal + <-sigCtx.Done() + logger.Info("SIGTERM: Shutting server down gracefully...") + + // Perform cleanup with a timeout with a 500ms limit like AWS Lambda SIGTERM behavior + shutdownCtx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + if err := server.Shutdown(shutdownCtx); err != nil { + logger.Error("Graceful server shutdown failed", slog.Any("err", err)) + } + + logger.Info("Server shutdown gracefully") + + //TODO: Verify app cleanup is done after the server is shutdown. + // If not, will need to call cleanupFuncs here. + logger.Info("Application cleanup completed") +} + +// registerSigtermFuncs configures an optional list of sigtermHandlers to run on HTTP Server shutdown. +func registerSigtermFuncs(sigtermFuncs []func()) { + // optionally register SIGTERM handlers + if len(sigtermFuncs) > 0 { + signaled := make(chan os.Signal, 1) + signal.Notify(signaled, syscall.SIGTERM) + go func() { + <-signaled + for _, f := range sigtermFuncs { + f() + } + }() + } +} From 23778bf6fae4b49cf8f72015d64dafecd05cfe92 Mon Sep 17 00:00:00 2001 From: neiljpowell <52715665+neiljpowell@users.noreply.github.com> Date: Wed, 4 Jun 2025 18:34:04 -0500 Subject: [PATCH 2/6] WithEnableSIGTERM support --- README.md | 54 +++++++++++++++++++++++ httpadapter/httpadapter.go | 26 +++++------ httpadapter/listener.go | 78 +++++++++++++-------------------- httpadapter/listener_test.go | 84 ++++++++++++++++++++++++++++++++++++ 4 files changed, 180 insertions(+), 62 deletions(-) create mode 100644 httpadapter/listener_test.go diff --git a/README.md b/README.md index 2ad8390..cfcc94c 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Testing a Lambda Function outside of AWS: ```go get -u github.com/Evernorth/aws-lambda-go-adapter``` ## Usage +### Start ``` package main @@ -71,6 +72,59 @@ func main() { } } ``` +### StartWithOptions WithEnableSIGTERM +This is useful for application cleanup logic needed during graceful shutdown. The httpadapter will mimic the AWS Lambda behavior of allowing ~500ms for cleanup before it is terminated. +``` +package main + +import ( + "context" + "github.com/Evernorth/aws-lambda-go-adapter/httpadapter" + "github.com/Evernorth/aws-lambda-go-adapter/pkg/util" + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambda" + "net/http" +) + +// Handler is the Lambda handler function. It returns a 200 status code with a "Hello, World!" message for GET requests, +// and a 405 status code for all other requests. +func Handler(ctx context.Context, request events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { + + if request.RequestContext.HTTP.Method == http.MethodGet { + return events.APIGatewayV2HTTPResponse{ + StatusCode: http.StatusOK, + Body: "Hello, World!", + Headers: map[string]string{ + "Content-Type": "text/html", + }, + }, nil + } + + return events.APIGatewayV2HTTPResponse{ + StatusCode: http.StatusMethodNotAllowed, + Body: "Method not allowed", + }, nil +} + +func cleanup() { + // Perform any necessary cleanup here, such as closing database connections or releasing resources. + // This function is called during graceful shutdown. +} + +func main() { + if util.IsLambdaRuntime() { + // Start AWS Lambda function with SIGTERM support for graceful shutdown. + // Cleanup logic has ~500ms to complete before it's terminated with a Lambda SIGKILL + lambda.StartWithOptions(Handler, + lambda.WithEnableSIGTERM(cleanup)) + } else { + // Start HTTP server with SIGTERM support for graceful shutdown. + // Cleanup logic has ~500ms to complete before it's terminated with a "SIGKILL" panic + httpadapter.StartWithOptions(8080, Handler, + httpadapter.WithEnableSIGTERM(cleanup)) + } +} +``` ## Dependencies See the [go.mod](go.mod) file. diff --git a/httpadapter/httpadapter.go b/httpadapter/httpadapter.go index 336def7..b7c8f0b 100644 --- a/httpadapter/httpadapter.go +++ b/httpadapter/httpadapter.go @@ -48,6 +48,7 @@ type Option func(*adapterOptions) // Start starts the HTTP server on the specified port and listens for incoming requests. When a request is received, // it is converted to the appropriate Lambda event type and passed to the handler function. The response from the // handler function is then converted to an HTTP response and returned to the client. +// See StartWithOptions for other start options. func Start(port int, handler interface{}) { StartWithOptions(port, handler) } @@ -56,16 +57,14 @@ func Start(port int, handler interface{}) { // it is converted to the appropriate Lambda event type and passed to the handler function. The response from the // handler function is then converted to an HTTP response and returned to the client. func StartWithOptions(port int, handler interface{}, options ...Option) { - //if h, ok := handler.(*adapterOptions); ok { - // return h - //} + + reflectHandler(handler) + opts := &adapterOptions{} for _, option := range options { option(opts) } - reflectHandler(handler) - switch delegateHandlerType { case apigwV2HandlerType: listenAndServe(port, handleRequestForApigwV2, opts) @@ -78,24 +77,21 @@ func StartWithOptions(port int, handler interface{}, options ...Option) { } } -// WithEnableSIGTERM enables SIGTERM behavior with the HTTP Server for use with the provided handler function(s). -// The HTTP server will listen for SIGTERM signals and run the provided callback functions before -// gracefully shutting down and performing cleanup tasks before the server is terminated. If the server -// does not shut down within a certain time frame (usually 500ms), a SIGKILL signal will forcefully terminate -// the server (similar to how AWS Lambda handles function shutdowns). -// SIGKILL will occur ~500ms after SIGTERM. -// Optionally, an array of callback functions to run on SIGTERM may be provided. +// WithEnableSIGTERM enables SIGTERM behavior with the HTTP server for graceful shutdown with the optional handler function(s). +// Graceful shutdown of the HTTP server before the provided functions are invoked with a ~500ms timeout. If the functions +// do not complete within the timeout limit, a "SIGKILL" panic will occur. This enables testing in a local environment, +// mimicking AWS Lambda's SIGTERM and SIGKILL behavior. // // Usage: // // httpadapter.StartWithOptions(8080, Handler, // lambda.WithEnableSIGTERM(func() { -// log.Print("HTTP server shutting down...") +// log.Print("Cleaning up application components...") // }) // ) -func WithEnableSIGTERM(callbacks ...func()) Option { +func WithEnableSIGTERM(sigtermFuncs ...func()) Option { return Option(func(opts *adapterOptions) { - opts.sigtermFuncs = append(opts.sigtermFuncs, callbacks...) + opts.sigtermFuncs = append(opts.sigtermFuncs, sigtermFuncs...) opts.enableSIGTERM = true }) } diff --git a/httpadapter/listener.go b/httpadapter/listener.go index 86b1259..c0be9d5 100644 --- a/httpadapter/listener.go +++ b/httpadapter/listener.go @@ -4,7 +4,6 @@ import ( "context" "log/slog" "net/http" - "os" "os/signal" "strconv" "syscall" @@ -22,49 +21,28 @@ func listenAndServe(port int, handler func(httpResponseWriter http.ResponseWrite Handler: nil, // Use the typical defaultServerMux with the handler set by http.HandleFunc } - if !opts.enableSIGTERM { - defaultListenAndServe(server) //port, handler) - } else { + if opts.enableSIGTERM { sigtermListenAndServe(server, opts) + } else { + defaultListenAndServe(server) } } -func defaultListenAndServe(server *http.Server) { //port int, handler func(httpResponseWriter http.ResponseWriter, httpRequest *http.Request)) { - - //http.HandleFunc("/", handler) - //portStr := ":" + strconv.Itoa(port) - //err := http.ListenAndServe(portStr, nil) - //err := server.ListenAndServe() - //if err != nil { - // logger.Error("Could not start http listener.", - // slog.Any("err", err)) - // panic(err) - //} - if err := server.ListenAndServe(); err != nil { //&& err != http.ErrServerClosed { +func defaultListenAndServe(server *http.Server) { + // start the server and ignore the error if it is http.ErrServerClosed from Shutdown + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { logger.Error("Could not start http listener.", slog.Any("err", err)) panic(err) } } func sigtermListenAndServe(server *http.Server, opts *adapterOptions) { - - registerSigtermFuncs(opts.sigtermFuncs) - // Create a context that cancels on SIGTERM or SIGINT sigCtx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) defer stop() - //http.HandleFunc("/", handler) - //portStr := ":" + strconv.Itoa(port) - //server := &http.Server{ - // Addr: portStr, - // Handler: nil, // Use the typical defaultServerMux with the handler set by http.HandleFunc - //} + // Start the server in a goroutine go func() { - //if err := server.ListenAndServe(); err != nil { //&& err != http.ErrServerClosed { - // logger.Error("Could not start http listener.", slog.Any("err", err)) - // panic(err) - //} defaultListenAndServe(server) }() @@ -72,31 +50,37 @@ func sigtermListenAndServe(server *http.Server, opts *adapterOptions) { <-sigCtx.Done() logger.Info("SIGTERM: Shutting server down gracefully...") - // Perform cleanup with a timeout with a 500ms limit like AWS Lambda SIGTERM behavior - shutdownCtx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + // Create a context with a timeout for a limited clean server shutdown (5s should be plenty, right?) + // Shutdown usually completes much faster, of course. + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() + if err := server.Shutdown(shutdownCtx); err != nil { - logger.Error("Graceful server shutdown failed", slog.Any("err", err)) + logger.Error("SIGTERM: Graceful server shutdown failed", slog.Any("err", err)) } + logger.Info("SIGTERM: Server shutdown gracefully") - logger.Info("Server shutdown gracefully") - - //TODO: Verify app cleanup is done after the server is shutdown. - // If not, will need to call cleanupFuncs here. - logger.Info("Application cleanup completed") -} - -// registerSigtermFuncs configures an optional list of sigtermHandlers to run on HTTP Server shutdown. -func registerSigtermFuncs(sigtermFuncs []func()) { - // optionally register SIGTERM handlers - if len(sigtermFuncs) > 0 { - signaled := make(chan os.Signal, 1) - signal.Notify(signaled, syscall.SIGTERM) + // Perform cleanup with a 500ms limit like AWS Lambda SIGTERM behavior. This will help ensure + // that cleanup functions can be locally tested with the same behavior as in AWS Lambda. + if len(opts.sigtermFuncs) > 0 { + logger.Info("SIGTERM: running functions (mimicking the 500ms AWS Lambda limit)") + timedCtx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + done := make(chan struct{}) go func() { - <-signaled - for _, f := range sigtermFuncs { + for _, f := range opts.sigtermFuncs { f() } + close(done) }() + select { + case <-done: + logger.Info("SIGTERM: functions completed") + case <-timedCtx.Done(): + //Panic too much? It's more attention-grabbing than an error log. + panic("SIGKILL: functions did not complete within the AWS Lambda limit") + } } + + logger.Info("SIGTERM: Completed successfully, exiting...") } diff --git a/httpadapter/listener_test.go b/httpadapter/listener_test.go new file mode 100644 index 0000000..50319ef --- /dev/null +++ b/httpadapter/listener_test.go @@ -0,0 +1,84 @@ +package httpadapter + +import ( + "net/http" + "os" + "testing" + "time" +) + +func TestSigtermListenAndServe(t *testing.T) { + + opts := &adapterOptions{ + enableSIGTERM: true, + } + sigtermListenAndServeTest(t, opts, false) +} + +func TestSigtermListenAndServe_WithFuncs(t *testing.T) { + + opts := &adapterOptions{ + enableSIGTERM: true, + sigtermFuncs: []func(){ + func() { + t.Log("Running cleanup function 1") + }, + func() { + t.Log("Running cleanup function 2") + }, + }, + } + sigtermListenAndServeTest(t, opts, false) +} + +func TestSigtermListenAndServe_ExpectFuncTimeout(t *testing.T) { + opts := &adapterOptions{ + enableSIGTERM: true, + sigtermFuncs: []func(){ + func() { + t.Log("Long running cleanup function") + time.Sleep(502 * time.Millisecond) // Simulate a long-running function (>500ms) + }, + }, + } + sigtermListenAndServeTest(t, opts, true) +} + +func sigtermListenAndServeTest(t *testing.T, opts *adapterOptions, expectPanic bool) { + // Create a dummy HTTP server that does nothing, just to test the SIGTERM handling + server := &http.Server{ + Addr: ":0", // Use a port no one should care about + } + + // Create a channel to signal when the server has started + done := make(chan struct{}) + go func() { + // Use defer to ensure that we recover from a timeout panic to assert + // behavior of AWS Lambda where cleanup functions take too long + defer func() { + if r := recover(); r == nil && expectPanic { + t.Fatal("Expected panic due to cleanup function timeout, but no panic occurred") + } + }() + + sigtermListenAndServe(server, opts) + close(done) + }() + + // Wait for the server to start + time.Sleep(20 * time.Millisecond) + + // Send SIGINT to self + p, _ := os.FindProcess(os.Getpid()) + _ = p.Signal(os.Interrupt) + + // Wait for the server to exit or timeout + select { + case <-done: + t.Log("server successfully exited on SIGINT") + case <-time.After(1 * time.Second): + if !expectPanic { + t.Fatal("server did not exit on SIGINT") + } + } +} From ab52d3002c176609ba20677df92627e491480620 Mon Sep 17 00:00:00 2001 From: neiljpowell <52715665+neiljpowell@users.noreply.github.com> Date: Thu, 5 Jun 2025 09:17:36 -0500 Subject: [PATCH 3/6] readme polish --- README.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index cfcc94c..ccc231d 100644 --- a/README.md +++ b/README.md @@ -11,19 +11,23 @@ only HTTP triggers are supported, but additional triggers may be supported in th ### How it works Lambda Function in AWS: -1. The main function calls the lambda.Start function, passing in the Handler function. -2. AWS Lambda invokes the Handler function when an HTTP request is received. +1. The main function calls the `lambda.Start` function, passing in the `Handler` function. +2. AWS Lambda invokes the `Handler` function when an HTTP request is received. +3. Alternatively, the main function can call `lambda.StartWithOptions` to enable graceful shutdown with a cleanup function. + ![diagram1](docs/images/diagram1.png) Testing a Lambda Function outside of AWS: -1. The main function calls the httpadapter.Start function, passing in the Handler function and the port number to listen on. -2. The httpadapter listens for incoming HTTP requests on the specified port and invokes the Handler function when a request is received. +1. The main function calls the `httpadapter.Start` function, passing in the `Handler` function and the port number to listen on. +2. The httpadapter listens for incoming HTTP requests on the specified port and invokes the `Handler` function when a request is received. +3. Alternatively, the main function can call `httpadapter.StartWithOptions` to enable graceful shutdown with a cleanup function. + ![diagram2](docs/images/diagram2.png) ## Features -* Supports APIGatewayV2HTTP, APIGatewayProxy, and ALBTargetGroup events. -* Supports handler functions with and without context.Context parameters. +* Supports `APIGatewayV2HTTP`, `APIGatewayProxy`, and `ALBTargetGroup` events. +* Supports handler functions with and without `context.Context` parameters. * Supports both values and pointers for handler function request events. * Supports both values and pointers for handler function response events. @@ -32,7 +36,7 @@ Testing a Lambda Function outside of AWS: ## Usage ### Start -``` +```go package main import ( @@ -74,7 +78,7 @@ func main() { ``` ### StartWithOptions WithEnableSIGTERM This is useful for application cleanup logic needed during graceful shutdown. The httpadapter will mimic the AWS Lambda behavior of allowing ~500ms for cleanup before it is terminated. -``` +```go package main import ( From f557afb2c1a4629a9f1c7d6464a0bcb14e963f94 Mon Sep 17 00:00:00 2001 From: neiljpowell <52715665+neiljpowell@users.noreply.github.com> Date: Thu, 5 Jun 2025 13:48:46 -0500 Subject: [PATCH 4/6] readme polish, cleanup -> shutdown --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ccc231d..aae6d08 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,14 @@ only HTTP triggers are supported, but additional triggers may be supported in th Lambda Function in AWS: 1. The main function calls the `lambda.Start` function, passing in the `Handler` function. 2. AWS Lambda invokes the `Handler` function when an HTTP request is received. -3. Alternatively, the main function can call `lambda.StartWithOptions` to enable graceful shutdown with a cleanup function. +3. Alternatively, the main function can call `lambda.StartWithOptions` to enable graceful shutdown with an optional shutdown function(s). ![diagram1](docs/images/diagram1.png) Testing a Lambda Function outside of AWS: 1. The main function calls the `httpadapter.Start` function, passing in the `Handler` function and the port number to listen on. 2. The httpadapter listens for incoming HTTP requests on the specified port and invokes the `Handler` function when a request is received. -3. Alternatively, the main function can call `httpadapter.StartWithOptions` to enable graceful shutdown with a cleanup function. +3. Alternatively, the main function can call `httpadapter.StartWithOptions` to enable graceful shutdown with an optional shutdown function(s). ![diagram2](docs/images/diagram2.png) @@ -77,7 +77,7 @@ func main() { } ``` ### StartWithOptions WithEnableSIGTERM -This is useful for application cleanup logic needed during graceful shutdown. The httpadapter will mimic the AWS Lambda behavior of allowing ~500ms for cleanup before it is terminated. +This is useful for application shutdown logic needed for a graceful shutdown. The httpadapter will mimic the AWS Lambda behavior of allowing ~500ms for shutdown before it is terminated. ```go package main @@ -110,7 +110,7 @@ func Handler(ctx context.Context, request events.APIGatewayV2HTTPRequest) (event }, nil } -func cleanup() { +func shutdown() { // Perform any necessary cleanup here, such as closing database connections or releasing resources. // This function is called during graceful shutdown. } @@ -118,14 +118,14 @@ func cleanup() { func main() { if util.IsLambdaRuntime() { // Start AWS Lambda function with SIGTERM support for graceful shutdown. - // Cleanup logic has ~500ms to complete before it's terminated with a Lambda SIGKILL + // App shutdown logic has ~500ms to complete before it's terminated with a Lambda SIGKILL lambda.StartWithOptions(Handler, - lambda.WithEnableSIGTERM(cleanup)) + lambda.WithEnableSIGTERM(shutdown)) } else { // Start HTTP server with SIGTERM support for graceful shutdown. - // Cleanup logic has ~500ms to complete before it's terminated with a "SIGKILL" panic + // App shutdown logic has ~500ms to complete before it's terminated with a "SIGKILL" panic httpadapter.StartWithOptions(8080, Handler, - httpadapter.WithEnableSIGTERM(cleanup)) + httpadapter.WithEnableSIGTERM(shutdown)) } } ``` From 610ed9da7bea9c6e5cce93dca73cdf52bf5ddc1b Mon Sep 17 00:00:00 2001 From: neiljpowell <52715665+neiljpowell@users.noreply.github.com> Date: Wed, 11 Jun 2025 14:52:10 -0500 Subject: [PATCH 5/6] tweak comment & logging --- httpadapter/httpadapter.go | 5 +++-- httpadapter/listener.go | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/httpadapter/httpadapter.go b/httpadapter/httpadapter.go index b7c8f0b..df65747 100644 --- a/httpadapter/httpadapter.go +++ b/httpadapter/httpadapter.go @@ -77,8 +77,9 @@ func StartWithOptions(port int, handler interface{}, options ...Option) { } } -// WithEnableSIGTERM enables SIGTERM behavior with the HTTP server for graceful shutdown with the optional handler function(s). -// Graceful shutdown of the HTTP server before the provided functions are invoked with a ~500ms timeout. If the functions +// WithEnableSIGTERM enables SIGTERM behavior with the HTTP server for graceful shutdown. +// Optionally, an array of shutdown functions to run on SIGTERM may be provided. +// After HTTP server graceful shutdown, the provided functions are invoked within a ~500ms timeout. If the shutdown functions // do not complete within the timeout limit, a "SIGKILL" panic will occur. This enables testing in a local environment, // mimicking AWS Lambda's SIGTERM and SIGKILL behavior. // diff --git a/httpadapter/listener.go b/httpadapter/listener.go index c0be9d5..84502a7 100644 --- a/httpadapter/listener.go +++ b/httpadapter/listener.go @@ -63,7 +63,7 @@ func sigtermListenAndServe(server *http.Server, opts *adapterOptions) { // Perform cleanup with a 500ms limit like AWS Lambda SIGTERM behavior. This will help ensure // that cleanup functions can be locally tested with the same behavior as in AWS Lambda. if len(opts.sigtermFuncs) > 0 { - logger.Info("SIGTERM: running functions (mimicking the 500ms AWS Lambda limit)") + logger.Info("SIGTERM: Running shutdown functions (mimicking the 500ms AWS Lambda limit)") timedCtx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() done := make(chan struct{}) @@ -75,12 +75,12 @@ func sigtermListenAndServe(server *http.Server, opts *adapterOptions) { }() select { case <-done: - logger.Info("SIGTERM: functions completed") + logger.Info("SIGTERM: Shutdown functions completed") case <-timedCtx.Done(): //Panic too much? It's more attention-grabbing than an error log. - panic("SIGKILL: functions did not complete within the AWS Lambda limit") + panic("SIGKILL: Shutdown functions did not complete within the AWS Lambda limit") } } - logger.Info("SIGTERM: Completed successfully, exiting...") + logger.Info("SIGTERM: Completed, exiting...") } From 56f595b066dcae5876a70547a848249d33f2acbf Mon Sep 17 00:00:00 2001 From: neiljpowell <52715665+neiljpowell@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:55:18 -0500 Subject: [PATCH 6/6] removed Q's in comments --- httpadapter/listener.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httpadapter/listener.go b/httpadapter/listener.go index 84502a7..377c2e9 100644 --- a/httpadapter/listener.go +++ b/httpadapter/listener.go @@ -50,7 +50,7 @@ func sigtermListenAndServe(server *http.Server, opts *adapterOptions) { <-sigCtx.Done() logger.Info("SIGTERM: Shutting server down gracefully...") - // Create a context with a timeout for a limited clean server shutdown (5s should be plenty, right?) + // Create a context with a timeout for a limited clean server shutdown (5s should be plenty) // Shutdown usually completes much faster, of course. shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -77,7 +77,7 @@ func sigtermListenAndServe(server *http.Server, opts *adapterOptions) { case <-done: logger.Info("SIGTERM: Shutdown functions completed") case <-timedCtx.Done(): - //Panic too much? It's more attention-grabbing than an error log. + //Panic is more attention-grabbing than an error log. panic("SIGKILL: Shutdown functions did not complete within the AWS Lambda limit") } }