fix(orchestrator): bound the Consul calls behind network slot acquire and release - #3515
fix(orchestrator): bound the Consul calls behind network slot acquire and release#3515tomassrnka wants to merge 1 commit into
Conversation
… and release The Consul client was built from DefaultConfig(), whose http.Client has no Timeout, so an agent that accepts a connection and then goes silent blocks the caller forever. Release does two such round trips on every network slot teardown, and Pool.Close waits for in-flight returns, so a wedged agent could stall a slot return and the shutdown drain with no way out. Give the client an explicit per-request timeout, and bound a whole release with its own deadline so a slot return cannot outlive it. Acquire now honours the context it was already being handed, including a bail-out in the fallback scan so a cancelled create does not walk all 32766 slots before it reaches a cancellable call. Release keeps taking no context on purpose: its callers are teardown paths, some already stripped of cancellation, and honouring cancellation there would skip the delete and leak the node-scoped Consul key, which nothing reclaims. It is unconditional but bounded instead.
PR SummaryMedium Risk Overview Consul client setup now applies a 5s per-request HTTP timeout and 10s end-to-end cap on slot Release (Get + DeleteCAS), which matters because pool shutdown waits on releases serially. Acquire propagates the caller’s context into every KV operation and bails out of the large fallback slot scan on cancellation instead of ignoring context. Release still does not take a context so teardown is not aborted in a way that would skip Consul deletes and leak reservations; it is only time-bounded. New unit tests use a wedged fake agent to assert timeout behavior and context cancellation on Acquire. Reviewed by Cursor Bugbot for commit 1031302. Bugbot is set up for automated code reviews on this repo. Configure here. |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
What was broken
The Consul client backing network-slot allocation is built from
consulApi.DefaultConfig(), whosehttp.Clienthas noTimeout. An agent that accepts the TCP connection and then goes silent blocks the caller forever — the transport's dial/TLS timeouts do not help once a connection is established.Releasedoes two such round trips (kv.Get+kv.DeleteCAS) on every network-slot teardown, andPool.Closewaits on in-flight slot returns during shutdown. So a wedged Consul agent could hang a slot return indefinitely and stall the orchestrator's shutdown drain, with nothing able to cut it short.Acquirehad the same exposure on the create path — up to 10kv.CASround trips plus akv.Keysscan and a fallback loop over 32766 slots — while taking acontext.Contextit then discarded entirely (func (s *StorageKV) Acquire(_ context.Context)).Fix
Two bounds, no interface change:
http.Client.Timeouton the Consul client. This is the load-bearing part: it caps every Consul call regardless of caller, including the teardown paths that deliberately run on non-cancellable contexts.Releasegets its own deadline so a whole release (both round trips) cannot outlive it.Acquirenow honours the context it was already given —WithContexton every query/write, plus actx.Err()bail-out inside the fallback scan, which otherwise walks all 32766 entries (skipping reserved slots costs no round trip, so a cancelled context would not be noticed until the first CAS).Deliberate design note:
Releasestill takes no contextThe issue suggests threading
context.Contextthrough theStorageinterface. I built that first and then backed it out, because honouring cancellation inReleaseintroduces a resource leak:Pool.ClosereceivescloseCtx, which is already cancelled underForceStop. With a cancellableRelease, every slot in both drain loops would fail instantly on an expired deadline and skip the Consul delete — leaking up toNewSlotsPoolSize + ReusedSlotsPoolSize= 132 node-scoped KV keys per force-stop. Nothing reclaims them:ReclaimLeakedSlotsonly scans/var/run/netnsand never touches Consul, so on a stablenodeIDthose reservations persist across restarts and slowly eat the slot space. ThecreateNetworkSlotrollback would skip its compensating delete for the same reason.Threading the context but not honouring it is pointless —
context.WithoutCancelstrips deadlines too, leaving only trace values. SoReleaseis unconditional but bounded, which is also consistent with the teardown paths inpool.gothat alreadyWithoutCancelon purpose. Happy to flip it if you'd rather have fast-abort-on-force-stop than key retention; it's a small change.Timeouts are kept modest (5s per request, 10s per release) because
Closedrains slots serially, so the release bound is the per-slot cost of an unreachable agent during shutdown. Today that cost is unbounded, so this is a strict improvement either way.How verified
New
storage_kv_test.godrives a realconsulApi.Clientagainst anhttptest"wedged agent" that accepts requests and never answers — what a hung Consul looks like on the wire. Three tests, all pure unit, run in a linux container (golang:1.26):Both are mutation-proven, not just green. Reverting the release deadline to a plain
context.Background():Removing
httpClient.Timeout(back toDefaultConfig()behaviour):gofmtclean;GOOS=linux go vet ./pkg/sandbox/network/...clean;golangci-lintv2.11.4 (repo-pinned) withGOOS=linux GOARCH=amd64reports 0 issues.Stated plainly — what I did not run locally: the full
./pkg/sandbox/network/...suite. Two tests there (TestDenyEgress_InstallsAllProtocolDrop,TestCreateNetwork_TagsEgressWithDSCP) needCAP_SYS_ADMIN+ iptables/nftables, and my local Docker VM ran out of capacity partway through. The diff is confined tostorage_kv.go, which no other test touches, and theStorageinterface is unchanged sopool.go/pool_test.goare untouched — CI runs the orchestrator shard with sudo and covers the rest. Nothing was verified against a real Consul agent; all Consul-facing tests use the hermetic fake.Follow-ups, not done here
Pool.Close's barep.returnsWG.Wait()has no context selection, soCloseis now bounded but still not interruptible.NewClientreplacesconfig.HttpClientforunix://addresses, which would silently drop the timeout. NoCONSUL_HTTP_ADDRis configured anywhere in this repo, so the default HTTP scheme applies — noted in a comment.Linear: https://linear.app/e2b/issue/EN-981/networkstoragekvrelease-makes-unbounded-consul-calls-without-context