Repository navigation
feat(server): zero-downtime restart via socket handoff - #415
Merged
Merged
Conversation
CWIST listen sockets already set SO_REUSEPORT; make that an explicit, documented contract for zero-downtime restarts: - New CWIST_REUSEPORT knob (default on; 0/false disables) gates SO_REUSEPORT on the TCP listen socket (cwist_make_socket_ipv4), the C1M accept path, and both HTTP/3 UDP bind sites. Disabling restores bind() failure on port conflicts and requires CWIST_WORKERS=1. - tests/test_socket_handoff.c: process B binds the port under 256 held keep-alive connections to A, A is SIGTERMed to drain, every request still gets a response (draining A, or B after reconnect), and all fresh connections reach B once A exits. - docs/deployment/zero-downtime-restart.md (with Korean mirror): the handoff sequence, kernel >= 3.9 constraint, systemd socket activation as an alternative, and the UDP/HTTP-3 decision (QUIC connection state cannot migrate; clients recover via retransmit/retry). Closes #405
gg582
added a commit
that referenced
this pull request
Oct 19, 2026
…415) CWIST listen sockets already set SO_REUSEPORT; make that an explicit, documented contract for zero-downtime restarts: - New CWIST_REUSEPORT knob (default on; 0/false disables) gates SO_REUSEPORT on the TCP listen socket (cwist_make_socket_ipv4), the C1M accept path, and both HTTP/3 UDP bind sites. Disabling restores bind() failure on port conflicts and requires CWIST_WORKERS=1. - tests/test_socket_handoff.c: process B binds the port under 256 held keep-alive connections to A, A is SIGTERMed to drain, every request still gets a response (draining A, or B after reconnect), and all fresh connections reach B once A exits. - docs/deployment/zero-downtime-restart.md (with Korean mirror): the handoff sequence, kernel >= 3.9 constraint, systemd socket activation as an alternative, and the UDP/HTTP-3 decision (QUIC connection state cannot migrate; clients recover via retransmit/retry). Closes #405
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #405
Summary
Zero-downtime restart via
SO_REUSEPORTsocket handoff: the new process binds the same port while the old one still serves, the old process is then signalled (SIGTERM/SIGINT) into the existing graceful-shutdown drain, and clients never see the port refuse a connection.The kernel capability was already there —
cwist_make_socket_ipv4()setSO_REUSEPORTunconditionally — but it was an undocumented side effect of the multi-worker fork design. This PR makes it an explicit, documented contract:CWIST_REUSEPORTknob (default on;0/falsedisables) now gatesSO_REUSEPORTat every listen-socket bind site: TCP (cwist_make_socket_ipv4), the C1M accept path (async_server.c), and both HTTP/3 UDP binds (single-port and multiport). Disabling restores bind()-fails-on-conflict as a guard against accidental double starts; since worker processes re-bind after fork,CWIST_REUSEPORT=0requiresCWIST_WORKERS=1(documented).tests/test_socket_handoff.c: the full handoff sequence as a test (see below).docs/deployment/zero-downtime-restart.md(+ Korean mirror, runtime-config rows EN/KO): handoff sequence, kernel constraints, systemd integration, HTTP/3 behavior.Design notes
SIGTERMthe old one (CWIST closes its listen sockets at once, so new flows now hash to the new process only) → old finishes in-flight requests up toCWIST_DRAIN_TIMEOUTand exits; idle keep-alive connections get a graceful FIN, and clients with standard stale-keep-alive retry land on the new process with no failed request.SO_REUSEPORT; BSDs/macOS also have it. Where it doesn't exist the code compiles it out and a second bind fails as before.Type=simple+ deploy script; notsystemctl restart, which reopens the downtime window) and socket activation (.socketunit holds the listener across restarts and crashes) as an alternative.Test plan
tests/test_socket_handoff.cis a scaled-down local version of the C10k acceptance scenario (256 held keep-alive connections instead of 10k — full C10k is impractical inmake test; stated here per the issue's allowance):SIGTERMto A; every held connection is then driven with another request: each is either answered by draining A or cleanly closed, in which case the client reconnects and must reach B. 0 requests failed (1 answered by draining A, 255 reconnected to B).Results:
make test_socket_handoff— passmake SANITIZE=address,undefined test_socket_handoff— pass, leak/UB-clean (children_exit; parent closes all fds)WERROR=1:test_listen_ex,test_shutdown,test_listen_signal_restore,test_classic_shutdown,test_multiport,test_httpsmake env-check— pass (new knob documented EN+KO)make api-check— pass (no public API change; the knob helper lives in a private header)python3 scripts/ci/check_test_wiring.py— passPossible CI note: the test uses 256+ fds and forked servers on an ephemeral port; it follows the same pattern as
test_listen_ex.c/test_classic_shutdown.c, so environments that already run those should run this one.