diff --git a/cmd/camp/main.go b/cmd/camp/main.go index 8c02566..5e7c0a8 100644 --- a/cmd/camp/main.go +++ b/cmd/camp/main.go @@ -1,11 +1,15 @@ package main import ( + "context" "log" "net/http" + "os" + "time" "github.com/codersgyan/camp/internal/contact" "github.com/codersgyan/camp/internal/database" + graceful "github.com/codersgyan/camp/internal/shutdown" ) func main() { @@ -13,17 +17,39 @@ func main() { if err != nil { log.Fatal(err) } - defer db.Close() - // run the migration if err := database.RunMigration(db); err != nil { log.Fatal(err) } + shutdown := graceful.New(10 * time.Second) + contactRepository := contact.NewRepository(db) contactHandler := contact.NewHandler(contactRepository) + srv := &http.Server{ + Addr: ":8080", + } + http.HandleFunc("POST /api/contacts", contactHandler.Create) - log.Fatal(http.ListenAndServe(":8080", nil)) + // Register cleanup + shutdown.Register(srv.Shutdown) + shutdown.Register(db.Close) + + // Start server in goroutine + go func() { + log.Println("Server running on :8080") + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatal(err) + } + }() + + // Wait for signal + exitCode, err := shutdown.Wait(context.Background()) + if err != nil { + log.Fatal(err) + } + os.Exit(exitCode) + } diff --git a/internal/contact/handler.go b/internal/contact/handler.go index 7aa31c9..4adf36c 100644 --- a/internal/contact/handler.go +++ b/internal/contact/handler.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "time" ) type Handler struct { @@ -25,6 +26,7 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request) { return } + time.Sleep(time.Second * 10) w.Header().Set("Content-Type", "application/json") createdId, err := h.repo.CreateContactOrUpsertTags(&contactBody) diff --git a/internal/shutdown/graceful.go b/internal/shutdown/graceful.go new file mode 100644 index 0000000..5c45550 --- /dev/null +++ b/internal/shutdown/graceful.go @@ -0,0 +1,63 @@ +package shutdown + +import ( + "context" + "os" + "os/signal" + "syscall" + "time" +) + +type GracefulShutdown struct { + timeout time.Duration + cleanup []func(context.Context) error +} + +func New(timeout time.Duration) *GracefulShutdown { + return &GracefulShutdown{ + timeout: timeout, + } +} + +// Register accepts BOTH: +// +// func() error +// func(context.Context) error +func (gs *GracefulShutdown) Register(fn any) { + switch f := fn.(type) { + + case func(context.Context) error: + gs.cleanup = append(gs.cleanup, f) + + case func() error: + wrapped := func(ctx context.Context) error { + return f() + } + gs.cleanup = append(gs.cleanup, wrapped) + + default: + panic("Register: invalid function type (must be func() error or func(context.Context) error)") + } +} + +func (gs *GracefulShutdown) Wait(ctx context.Context) (int, error) { + sigChan := make(chan os.Signal, 1) + + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + sig := <-sigChan + + // Convert to UNIX exit code + exitCode := 128 + int(sig.(syscall.Signal)) + + // cleanup with timeout + cleanupCtx, cancel := context.WithTimeout(ctx, gs.timeout) + defer cancel() + + for _, fn := range gs.cleanup { + if err := fn(cleanupCtx); err != nil { + return exitCode, err + } + } + return exitCode, nil +}