SIGINT/SIGTERM does not shut down the HTTP server, so the process can hang on exit #3

Closed
opened 2026-04-27 18:38:12 +07:00 by codex · 1 comment
Collaborator

Summary

The signal handler cancels the shared context, but the Gin HTTP server is started with router.Run(...) and never receives a shutdown signal. As a result, the process can block forever in WaitGroup.Wait() after SIGINT or SIGTERM.

Evidence

  • cmd/bridge/main.go:42-45 handles SIGINT/SIGTERM by calling cancel()
  • cmd/bridge/main.go:57-60 runs the IMAP watcher, which respects the context and returns
  • cmd/bridge/main.go:66-73 starts the HTTP server with router.Run(cfg.ListenAddr)
  • There is no owned http.Server instance and no call to Shutdown() on cancellation

Why this is a problem

  • On shutdown, the IMAP goroutine can stop, but the HTTP goroutine stays blocked inside router.Run(...).
  • The main goroutine waits on the WaitGroup, so the process may never exit cleanly.
  • In Docker or process supervisors, this can turn graceful stop into a forced kill.

Expected behavior

  • Own the HTTP server explicitly.
  • On context cancellation or signal, call Shutdown() with a timeout.
  • Let both goroutines finish so WaitGroup.Wait() can return.

Impact

Graceful shutdown is currently incomplete and can leave the service hanging during deploys or container stops.

## Summary The signal handler cancels the shared context, but the Gin HTTP server is started with `router.Run(...)` and never receives a shutdown signal. As a result, the process can block forever in `WaitGroup.Wait()` after `SIGINT` or `SIGTERM`. ## Evidence - `cmd/bridge/main.go:42-45` handles `SIGINT/SIGTERM` by calling `cancel()` - `cmd/bridge/main.go:57-60` runs the IMAP watcher, which respects the context and returns - `cmd/bridge/main.go:66-73` starts the HTTP server with `router.Run(cfg.ListenAddr)` - There is no owned `http.Server` instance and no call to `Shutdown()` on cancellation ## Why this is a problem - On shutdown, the IMAP goroutine can stop, but the HTTP goroutine stays blocked inside `router.Run(...)`. - The main goroutine waits on the `WaitGroup`, so the process may never exit cleanly. - In Docker or process supervisors, this can turn graceful stop into a forced kill. ## Expected behavior - Own the HTTP server explicitly. - On context cancellation or signal, call `Shutdown()` with a timeout. - Let both goroutines finish so `WaitGroup.Wait()` can return. ## Impact Graceful shutdown is currently incomplete and can leave the service hanging during deploys or container stops.
Collaborator

Validation: Issue confirmed

cmd/bridge/main.go:69router.Run() blocks indefinitely, no http.Server owned, no Shutdown() on context cancel. Process hangs at wg.Wait().

Implementation Plan

Single-file change in cmd/bridge/main.go:

  1. Replace router.Run(cfg.ListenAddr) with explicit http.Server
  2. Start server in goroutine via srv.ListenAndServe()
  3. Watch ctx.Done() and call srv.Shutdown() with 5s timeout
  4. Keep existing error logging and cancel() on server error

No changes to api/router.go or any other package.

## Validation: Issue confirmed `cmd/bridge/main.go:69` — `router.Run()` blocks indefinitely, no `http.Server` owned, no `Shutdown()` on context cancel. Process hangs at `wg.Wait()`. ## Implementation Plan Single-file change in `cmd/bridge/main.go`: 1. Replace `router.Run(cfg.ListenAddr)` with explicit `http.Server` 2. Start server in goroutine via `srv.ListenAndServe()` 3. Watch `ctx.Done()` and call `srv.Shutdown()` with 5s timeout 4. Keep existing error logging and `cancel()` on server error No changes to `api/router.go` or any other package.
Sign in to join this conversation.
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: thuanle/claw-email#3