Skip to content

feat: add graceful shutdown with drain window and idle-connection grace period - #62

Open
kaiburjack wants to merge 1 commit into
daichirata:masterfrom
kaiburjack:feat/graceful-shutdown
Open

feat: add graceful shutdown with drain window and idle-connection grace period#62
kaiburjack wants to merge 1 commit into
daichirata:masterfrom
kaiburjack:feat/graceful-shutdown

Conversation

@kaiburjack

@kaiburjack kaiburjack commented Jul 11, 2026

Copy link
Copy Markdown

Motivation

Currently, when receiving SIGTERM (i.e. when used in Kubernetes), the Go HTTP server will immediately initiate shutdown, closing its listen socket and also all currently idle connections immediately.
This is problematic when doing a rolling update in Kubernetes, because:

  1. kube-proxy takes a while to notice a terminating pod and updating the node's iptables/netfilter rules (roughly 5s). During that time, new connections can still be routed to the terminated gcsproxy pod/process, resulting in TCP connection resets.
  2. by default, the Go HTTP Server closes idle connections on shutdown immediately. This is also problematic, because it can result in the race condition of a client (using HTTP persistent/keep-alive connections) to initiate a new request right as the server is closing the idle connection.
    The HTTP/1.1 RFC 9112 also briefly discusses this:

For example, a client might have started to send a new request at the same time that the server has decided to close the “idle” connection. From the server’s point of view, the connection is being closed while it was idle, but from the client’s point of view, a request is in progress.

We have seen all of these problems in our Kubernetes production deployment of gcsproxy (and other services) and have forked it because of this (and because of many other features we would like to have): https://github.com/HBTGmbH/gcsproxy
Now, that the main gcsproxy project is actively being maintained again, we would like to add these features to the upstream project as well.

I've also written some blog posts about all of these problems :)
https://medium.com/hamburger-berater-team/achieving-zero-downtime-in-kubernetes-d9e3e5b5927c

What this PR adds

  1. A configurable "drain duration" during which nothing is stopped or shutdown or closed: People in the Kubernetes world usually do this with a preStop sleep lifecycle hook. However, it also does not hurt when the service itself provides this as well. This is to avoid the problem 1. mentioned above. During this drain period, requests will receive a Connection: close response header in the response, indicating to the client to not resuse this connection anymore, because the server is about to shutdown. Eventually, the random connection routing in iptables/netfilter will result in all such active connections being routed somewhere else
  2. After this drain duration, the listen socket is closed, not accepting any further new connections. Existing connections and existing requests on such connections are unaffected.
  3. A configurable "idle grace" duration during which idle connections are not closed, to avoid problem 2. mentioned above. This duration should ideally be set to the maximum client-side idle/keep-alive timeout. Once, no idle connections exist anymore (because they either have seen a next request, for which we respond with Connection: close, or the client closed it on their own, due to client-side idle/keep-alive timeout), the server is shutdown.
  4. A configurable "shutdown timeout", for Go's HTTP Server Shutdown(ctx), effectively waiting for this long for currently active requests to finish on active connections.

@daichirata daichirata left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thank you for adding graceful shutdown support! I left a few comments.

Comment thread main.go Outdated
select {
case <-tracker.noneOpen():
slog.Info("shutdown: no connections remain")
case <-time.After(s.minCloseDelay):

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Since this wait may end before minCloseDelay elapses, I wonder if naming this setting something like idleGracePeriod would better reflect its behavior. This would also apply to the corresponding CLI flag and documentation. What do you think?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes, very good idea. That is definitely not a "minimum" delay.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I've incorporated the suggested changes in the latest (squashed) commit.

Comment thread main.go Outdated
corsOrigin = flag.String("cors-origin", "", "Value for the Access-Control-Allow-Origin header.")
shutdownDelay = flag.Duration("shutdown-delay", 0, "Delay after SIGTERM/SIGINT during which requests are served normally but responses carry Connection: close.")
minCloseDelay = flag.Duration("shutdown-min-close-delay", 0, "After the listener closes, keep idle connections alive at least this long before closing them.")
shutdownTimeout = flag.Duration("shutdown-timeout", 0, "Max wait for in-flight requests after the min-close-delay; 0 waits indefinitely.")

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

With the current default, shutdown may wait indefinitely if an in-flight request gets stuck, for example while streaming to a slow client or waiting on GCS. Would a finite default such as 30 seconds be safer, while still allowing users to explicitly set 0 if they want to wait indefinitely? What do you think?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes, setting it to something reasonable, like the proposed 30s is a good idea.
The other flags should keep their 0s default, though, IMO, since it is highly deployment-scenario-dependent what values make sense there, and we should keep the behaviour mostly compliant with the previous behaviour before these changes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I've incorporated the suggested changes in the latest (squashed) commit.

@kaiburjack
kaiburjack force-pushed the feat/graceful-shutdown branch 2 times, most recently from 0e3212d to 6527a08 Compare July 12, 2026 17:11
@kaiburjack
kaiburjack force-pushed the feat/graceful-shutdown branch from 6527a08 to fa4de1a Compare July 13, 2026 15:05
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.

2 participants