Skip to content

feat(duplex): add graceful shutdown support and port validation#556

Closed
k4leung4 wants to merge 1 commit into
chainguard-dev:mainfrom
k4leung4:kleung/graceful-shutdown
Closed

feat(duplex): add graceful shutdown support and port validation#556
k4leung4 wants to merge 1 commit into
chainguard-dev:mainfrom
k4leung4:kleung/graceful-shutdown

Conversation

@k4leung4

@k4leung4 k4leung4 commented Mar 22, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Honor context cancellation in ListenAndServe and Serve by calling http.Server.Shutdown() when the context is done, allowing in-flight requests (both REST and gRPC-over-HTTP) to complete within a 30-second timeout
  • Previously the context parameter was accepted but ignored (_ context.Context), meaning in-flight requests were dropped during deployments
  • ~30 callers in mono already pass a signal.NotifyContext — they will automatically gain graceful shutdown on the next go-grpc-kit bump
  • Also validate that port is in range [0, 65535] in New()

Note: grpc.Server.GracefulStop() is NOT used because the duplex server runs gRPC via grpcHandlerFunc on an http.Server, which uses serverHandlerTransport — a transport that does not support Drain(). http.Server.Shutdown handles graceful draining for both REST and gRPC-over-HTTP connections. The serve error channel is drained after shutdown and http.ErrServerClosed is filtered as expected.

Test plan

  • go test ./pkg/duplex/ -count=1 -race passes
  • TestGracefulShutdown — starts a slow in-flight RPC, cancels the server context, verifies the RPC completes successfully (graceful drain) and Serve returns nil
  • TestNewInvalidPort — table-driven test verifying both negative and >65535 ports panic

🤖 Generated with Claude Code

@k4leung4 k4leung4 force-pushed the kleung/graceful-shutdown branch 2 times, most recently from 7270858 to eb64a24 Compare March 22, 2026 21:10
Honor context cancellation in ListenAndServe and Serve by shutting down
the HTTP server gracefully via http.Server.Shutdown(), allowing in-flight
requests (both REST and gRPC-over-HTTP) to complete within a 30-second
timeout. Drain the serve error channel and filter http.ErrServerClosed.

Note: grpc.Server.GracefulStop() is NOT used because the duplex server
runs gRPC via grpcHandlerFunc on an http.Server, which uses
serverHandlerTransport — a transport that does not support Drain().
http.Server.Shutdown handles draining for both protocols.

Also validate that port is in range [0, 65535] in New().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Kenny Leung <kleung@chainguard.dev>
@k4leung4 k4leung4 force-pushed the kleung/graceful-shutdown branch from eb64a24 to b955c1b Compare March 22, 2026 21:17
@k4leung4 k4leung4 marked this pull request as ready for review March 26, 2026 20:52

@eslerm eslerm left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖: Graceful shutdown implementation looks clean — serveAndWait with buffered errCh and http.Server.Shutdown is the right approach. Port validation is straightforward. A few suggestions:

Comment thread pkg/duplex/server.go
// requests (both REST and gRPC-over-HTTP) to complete.
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖: When server.Shutdown(shutdownCtx) returns an error (timeout), errCh is never drained — the serve goroutine's error is silently discarded. Not a leak (buffered channel), but if the serve error is meaningful it would be lost. Consider draining errCh before returning the shutdown error.

Comment thread pkg/duplex/server_test.go
}()

// Give server time to start.
time.Sleep(100 * time.Millisecond)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖: time.Sleep(100ms) as synchronisation — there's no signal from inside slowServer.SayHello confirming the handler has been entered before cancel() fires. On a loaded CI runner, cancel could fire before the RPC is in-flight, making this test validate clean-stop-under-no-load rather than graceful drain. A channel closed at the top of SayHello would make this deterministic.

@iainlane

Copy link
Copy Markdown
Contributor

sorry, I think I stole this feature in 24cc91b...

can you check if there's anything left over from yours?

@k4leung4 k4leung4 closed this Jun 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants