fix(router): bound + detach old graph-server shutdown on config swap (#3286)#12
Merged
Merged
Conversation
SwapGraphServer shuts the previous graph server down synchronously on the
config-poller goroutine with an unbounded ctx. graphServer.Shutdown drains
in-flight requests via wait() (polls inFlightRequests, no independent timeout),
so a single stuck request — e.g. a handler blocked on a dead-client write
(WriteTimeout=0) or a custom module's own HTTP call — freezes the entire config
pipeline. Production showed in-flight-drain stalls up to 1h21m, which:
- froze CDN config hot-reload → pods served stale schema for hours (#3286)
- pinned the old generation's schema AST + ristretto caches + protojson in
memory, driving a GC mark-phase storm and planning-latency spikes.
Behind mondaytweaks.AsyncBoundedOldGraphServerShutdown (default on): run the old
server's Shutdown off the poller goroutine and bound the drain by the configured
grace_period (fallback 90s > 60s subgraph request_timeout when unset), via
context.WithoutCancel + WithTimeout. New traffic already routes to the swapped-in
server, so abandoning a stuck old-server request after the drain window is safe
and lets reloads proceed + releases the old generation.
Also re-enable DisableUpstreamSubscriptionPingWhenClientWebSocketDisabled: prod
runs websocket.enabled=false, yet a goroutine profile showed WSTransport.pingLoop
at ~65% of all goroutines (1.5M) accumulating across reloads. Zeroing PingInterval
when client WS is disabled stops that leak.
Router-nonroot image scan passed✅ No security vulnerabilities found in image: |
Collaborator
Author
Effective drain bound in prod (helm values)Confirmed via helm-charts (
So this change bounds the async old-server drain at 65s, which is intentionally > the 60s subgraph Net: no config change required — the fix inherits the already-tuned |
budziam
approved these changes
Jul 9, 2026
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.
Problem
SwapGraphServershuts the previous graph server down synchronously on the config-poller goroutine with an unbounded ctx.graphServer.Shutdowndrains in-flight requests inwait()(pollsinFlightRequests, no independent timeout), so one stuck request blocks the entire config pipeline.Stuck-request sources observed / possible:
WriteTimeout: 0, kept for SSE)request_timeout60s, so those aren't the long pole)Production impact (this incident)
#10stall instrumentation: 100% of shutdown stalls arein-flight request drain, up to 1h21m.scanobject/findObject~60% CPU) → planning-latency spikes when CPU headroom was reduced.Fix
Behind
mondaytweaks.AsyncBoundedOldGraphServerShutdown(default on):Shutdownoff the poller goroutine.grace_period(context.WithoutCancel(ctx)to keep trace values but detach lifetime, thenWithTimeout). Fallback90s(> 60s subgraphrequest_timeout) ifgrace_periodis unset, so the drain is never unbounded.New traffic already routes to the swapped-in server, so abandoning a stuck old-server request after the drain window is safe — reloads proceed and the old generation is released.
Also re-enables
DisableUpstreamSubscriptionPingWhenClientWebSocketDisabled: prod runswebsocket.enabled: false, yet a goroutine profile showedWSTransport.pingLoopat ~65% of all goroutines (1.5M) accumulating across reloads. ZeroingPingIntervalwhen client WS is disabled stops that leak.Test
go build ./core/... ./pkg/mondaytweaks/...✅go vet ./core/ ./pkg/mondaytweaks/✅go test ./core/ -run 'Swap|GraphServer|Shutdown|Server'✅Follow-ups (separate)
traffic_shaping.all.request_timeout: 60sin the router config (defends against env override).