Skip to content

fix(orchestrator): bound Consul KV slot operations with a context and timeout - #3509

Draft
tomassrnka wants to merge 2 commits into
mainfrom
fix/network-storage-release-ctx
Draft

fix(orchestrator): bound Consul KV slot operations with a context and timeout#3509
tomassrnka wants to merge 2 commits into
mainfrom
fix/network-storage-release-ctx

Conversation

@tomassrnka

@tomassrnka tomassrnka commented Aug 1, 2026

Copy link
Copy Markdown
Member

Linear: https://linear.app/e2b/issue/EN-981/networkstoragekvrelease-makes-unbounded-consul-calls-without-context

Broken: a wedged Consul agent (accepts TCP, never answers) stalls orchestrator shutdown indefinitely — Pool.Close drains every pooled slot through Storage.Release, and since FORCE_STOP cancels the shutdown context up-front, even a forced stop couldn't cut the calls short.

Root cause: Storage.Release took no context, StorageKV.Acquire named its context _, all four Consul KV calls passed nil options, and consul/api builds its http.Client with no Timeout or ResponseHeaderTimeout — nothing bounded the round trip.

Repro: against a black-hole HTTP server on unmodified main: Release still blocked after 5 s, Release with an already-cancelled context still blocked, Acquire(cancelledCtx) still blocked.

Fix: Release takes a context, both methods plumb it through Consul Query/WriteOptions, every round trip gets a 5 s per-request timeout, the client gets an http.Client.Timeout backstop for future call sites, and Acquire checks cancellation between retries. Pool.Close detaches its drain from the caller's context and caps it at 15 s instead of passing a possibly-cancelled context through — the template-manager is the orchestrator binary with FORCE_STOP=true and a Consul-backed pool, so passing the cancelled context would skip releasing up to 131 node-scoped keys per deploy that nothing reclaims (ReclaimLeakedSlots only handles netns entries). 15 s sits inside the shared 1 m kill_timeout (~26 closers, leaves 45 s); a healthy drain finishes well under a second, so the cap only bites when Consul is unresponsive.

Alternative considered: #3515 fixes the same ticket without changing the Storage interface (context.Background() + 10 s inside Release, plus the same client timeout) — avoids the key leak but leaves the drain stall: Release is uncancellable and the drain is serial, so a wedged agent costs up to 131 x 10 s (~22 min), far past the 1 m kill_timeout — the exact symptom EN-981 reports. This PR threads the context and bounds the whole drain once, getting both properties.

Verification: new storage_kv_test.go covers cancelled-context, per-request timeout, client backstop, and happy paths against a fake Consul (it doesn't compile against main — the missing context parameter is the bug). TestClose_ReleasesPooledSlotsWithCanceledContext asserts Storage.Release gets a live context during forced shutdown and fails against the interface-only version. go build, go vet, go test -race, golangci-lint v2.11.4 all clean.

@cla-bot cla-bot Bot added the cla-signed label Aug 1, 2026
@cursor

cursor Bot commented Aug 1, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes orchestrator shutdown and Consul-backed network slot lifecycle; wedged Consul or partial drain failures can still leave KV keys unreleased, but the prior behavior could block shutdown indefinitely.

Overview
Fixes orchestrator shutdown hanging when Consul accepts connections but never responds: network slot Release and Consul KV calls now honor caller context, each round trip is capped at 5s, and the Consul HTTP client has a matching timeout backstop. On pool shutdown (including FORCE_STOP with an already-cancelled context), slot drains use a detached context bounded at 15s so releases still run without wedging past Nomad’s kill window, while error paths that used to skip release on a dead context now detach so node-scoped KV keys are not leaked.

Reviewed by Cursor Bugbot for commit b0a2345. Bugbot is set up for automated code reviews on this repo. Configure here.

@codecov

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 53.94737% with 35 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...ges/orchestrator/pkg/sandbox/network/storage_kv.go 56.89% 24 Missing and 1 partial ⚠️
packages/orchestrator/pkg/sandbox/network/pool.go 50.00% 8 Missing ⚠️
.../orchestrator/pkg/sandbox/network/storage_local.go 0.00% 1 Missing ⚠️
...orchestrator/pkg/sandbox/network/storage_memory.go 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@tomassrnka
tomassrnka force-pushed the fix/network-storage-release-ctx branch from 1849c40 to a76c268 Compare August 1, 2026 12:49
… timeout

StorageKV.Release took no context and StorageKV.Acquire ignored the one it
was given, so every Consul KV round trip was unbounded. The Consul API client
builds its http.Client with neither a Timeout nor a ResponseHeaderTimeout, so
an agent that accepts the connection but never answers blocks the caller
forever.

That stalls orchestrator shutdown: Pool.Close waits for in-flight returns and
then drains every pooled slot through Storage.Release. Because the shutdown
context is cancelled immediately when FORCE_STOP is set, even a forced stop
could not cut these calls short.

- Storage.Release now takes a context; StorageKV honors it on Get/DeleteCAS,
  and Acquire honors it on CAS/Keys, via Consul's Query/WriteOptions.
- Every round trip is additionally bounded by a per-request timeout (5s), so
  a caller context without a deadline still cannot hang. The Consul client
  now also carries an explicit http.Client.Timeout as a backstop for any call
  site added later without a per-operation context.
- Acquire checks for cancellation between retries instead of spending the
  remaining attempts on doomed round trips.
- Pool.Close detaches its drain from the caller's context and bounds it with
  a 15s deadline instead of passing a possibly-cancelled context through. The
  template-manager runs the orchestrator binary with FORCE_STOP=true, which
  cancels the shutdown context up front, so passing it through would skip the
  release of every pooled slot. The keys are node-scoped and nothing reclaims
  them, so that would leak up to 131 slot indices per deploy.
- The two pool paths that clean up with an already-dead context likewise
  detach via context.WithoutCancel, bounded by the per-request timeout.
@tomassrnka
tomassrnka force-pushed the fix/network-storage-release-ctx branch from a76c268 to 0fdba68 Compare August 1, 2026 13:03
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jakubno
jakubno force-pushed the fix/network-storage-release-ctx branch from 70949dd to b0a2345 Compare August 3, 2026 13:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant