fix(sandbox-cache): broadcast state transition event from StartRemoving - #3596
Open
AdaAibaby wants to merge 2 commits into
Open
fix(sandbox-cache): broadcast state transition event from StartRemoving#3596AdaAibaby wants to merge 2 commits into
AdaAibaby wants to merge 2 commits into
Conversation
…ET bandwidth TeamItems issues SMEMBERS + MGET on every call. With teams of 9 000+ sandboxes and multiple API allocations the read bandwidth scales as O(allocations × team_size), saturating the API allocation NIC. This PR introduces a per-allocation in-process cache for sandbox state backed by the existing pub/sub infrastructure (publisher + subscriptionManager, introduced in e2b-dev#2099 / e2b-dev#2668). The cache eliminates the O(allocations × team_size) multiplier: each allocation maintains a local snapshot and TeamItems reads from memory after the first cold-fetch. Design - sandbox_event.go: sandboxEvent JSON type published alongside existing plain routing-key strings on globalStorageNotifyChannel. JSON prefix '{' is an unambiguous discriminator from routing keys ('sandbox:...', 'lock:...'). - sandbox_cache.go: sandboxCache keyed by sandbox ID, indexed by team, with warm/cold state per team. Thread-safe via sync.RWMutex. - publisher.go: publishSandboxEvent marshals and enqueues events on the existing 32-worker publish pool. - subscription_manager.go: dispatch detects JSON events and applies them to the embedded sandboxCache before routing-key fan-out. - operations.go: Add/Update/Remove broadcast events after each Redis write; TeamItems checks the cache (warm-path) or falls back to SMEMBERS+MGET and warms the team on cold-start. Gated by SandboxTeamItemsCacheFlag (default false). - featureflags/flags.go: SandboxTeamItemsCacheFlag for safe rollout. Tests - sandbox_cache_test.go: unit tests for apply/evict/warmTeam/getTeam, state filtering, team isolation, stale-entry eviction, event marshal/unmarshal, routing-key disambiguation. - team_items_test.go: extended with 9 new integration tests (real Redis via testcontainers) covering cold-start warming, empty-team warming, event-driven add/remove reflection, end-to-end Add/Remove/Update broadcast, team isolation, and flag-off behaviour. Closes e2b-dev#3593
Before this change, StartRemoving atomically wrote the new sandbox state
(e.g. Running→Killing) to Redis via startTransitionScript, but never
called publishSandboxEvent. Every allocation continued to see the old
Running state in its local TeamItems cache until the sandbox was
eventually deleted and a remove event arrived.
Fix: publish a sandboxEvent{update} immediately after the Lua script
succeeds. dispatch() applies it via cache.apply, so all allocations
reflect the intermediate state (Killing, Pausing, Snapshotting) without
any Redis read.
The createCallback path needs no separate publish:
- Terminal transitions: the subsequent Remove() already broadcasts remove.
- Transient transitions: restoreToRunning calls Update() which already
broadcasts the restored Running state.
Adds two integration tests:
- TestStartRemoving_CacheBroadcastsTransitionState: Kill transition
visible as Killing in cache before Remove is called.
- TestStartRemoving_CacheBroadcastsTransientTransition: Snapshot
transition visible as Snapshotting, then restored to Running.
Fixes e2b-dev#3595
AdaAibaby
requested review from
ValentaTomas,
dobrac and
jakubno
as code owners
August 22, 2026 11:00
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.
Summary
StartRemovingatomically writes a new sandbox state to Redis via a Lua script (e.g.Running → Killing), but never calledpublishSandboxEvent. Every allocation's in-process sandbox cache (introduced in #3593) continued to serve the oldRunningstate until the sandbox was eventually deleted and aremoveevent arrived.Symptoms:
TeamItems(states: [Running])returned sandboxes already mid-removalRoot Cause
Three code locations form the gap:
1.
StartRemoving— Lua write succeeds, no event published2.
createCallback— publishes routing key, not a sandboxEvent3.
dispatch— routing key never reaches cache.applyFix
After
startTransitionScript.Run()succeeds, publish asandboxEvent{update}:dispatch()routes it tocache.apply, so all allocations immediately reflect the intermediate state.createCallbackneeds no extra publish:Remove()broadcasts aremoveeventrestoreToRunningcallsUpdate()which broadcasts the restoredRunningstateTests
Two new integration tests (real Redis via testcontainers):
TestStartRemoving_CacheBroadcastsTransitionState— Kill transition is visible asKillingin cache beforeRemoveis calledTestStartRemoving_CacheBroadcastsTransientTransition— Snapshot transition visible asSnapshotting, restored toRunningafter callbackRelated