[EN-1048] fix(api): heal sandbox missing from eviction index#3199
[EN-1048] fix(api): heal sandbox missing from eviction index#3199jakubno wants to merge 7 commits into
Conversation
PR SummaryHigh Risk Overview Expiration index members are now keyed by execution ID (with a rolling-deploy path for the old A background healer on each API pod periodically scans team sandboxes and ZADD NX’s any missing index members (batched, grace period for in-flight adds), addressing the production “immortal sandbox” gap when index writes were lost. Reviewed by Cursor Bugbot for commit 48134ad. 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! |
|
Warning Gemini encountered an error creating the review. You can try again by commenting |
Prevents accidentally report duration than actual sandbox duration. Prerequisite for #3199
There was a problem hiding this comment.
This PR touches core Redis-backed sandbox eviction paths and introduces a background healer, legacy-member migration, and multiple new race-handling code paths — worth a human review despite the good test coverage.
Extended reasoning...
Overview
The PR changes the global expiration ZSET member format from teamID:sandboxID to teamID:sandboxID:executionID to make ZREM structurally safe against TOCTOU where a lockless Add for a fresh execution of the same sandbox ID races a Remove or the evictor's stale sweep. It also adds a background healer that scans stored sandboxes and ZADD NX-re-adds missing index members, and expands ExpiredItems to sweep orphans and dead-execution members, lazily upgrade legacy members, and re-score drifted entries. removeSandboxScript now returns the deleted JSON so Remove can scope the ZREM to the execution it actually deleted. New OTEL counters (healed, swept, rescored) provide observability.
Security risks
None material. Changes are internal Redis bookkeeping; no auth/crypto/permission surface is touched. The new metrics attributes are static strings, not user input.
Level of scrutiny
High. Sandbox eviction is production-critical: getting index membership wrong can either leave sandboxes immortal (the incident this fixes) or falsely evict live sandboxes. Several code paths interact non-trivially (Add / Remove / Update / ExpiredItems / healer, plus a rolling-deploy migration window with legacy members coexisting), and the correctness argument relies on subtle ordering (ZADD NX before ZREM of legacy, XX-only rescoring, execution-scoped ZREM). Human eyes should confirm the invariants hold under Redis Cluster and rolling deploys.
Other factors
- No bugs surfaced by the automated bug hunter.
- Extensive race-focused unit tests exercise the key scenarios (fresh-execution not unindexed by Remove, stale-sweep execution-scoped, legacy upgrade, drift rescore, healer restoration, healer grace period, no-clobber).
- Patch coverage is ~72% per Codecov, with the healer's error paths mostly uncovered — acceptable for warn-and-continue paths, but a reviewer should sanity-check.
- No prior human review on this PR yet.
arkamar
left a comment
There was a problem hiding this comment.
Not necessarily request for changes, it's rather a request for discussion.
SMEMBERS + single MGET/ZMSCORE over a whole team index produces huge commands and replies for teams with many sandboxes, stalling Redis. Walk the index with SSCAN and heal in 256-id batches; ZADD NX keeps duplicate SSCAN returns idempotent.
Split-and-switch reads clearer than Cut + LastIndexByte and no longer returns sandbox IDs containing ':' — malformed members (non-UUID execution tag, extra segments) are rejected instead of folded into a legacy sandbox ID. Co-authored-by: Petr Vaněk <6185600+arkamar@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1572212514
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
arkamar
left a comment
There was a problem hiding this comment.
I think the cancel() for startHealer() makes sense. Otherwise LGTM.
Co-authored-by: Petr Vaněk <petr.vanek@e2b.dev>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 48134ad9ec
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // a newer execution wrote a different member, so it can never be | ||
| // unindexed here. If the key was already gone, any leftover execution | ||
| // member is swept by ExpiredItems once its score passes. | ||
| members := []any{legacyExpirationMember(teamID.String(), sandboxID)} |
There was a problem hiding this comment.
Avoid deleting fresh legacy members during rollout
When a new pod removes an old execution while an old pod is still in the rolling deploy path, that old pod can write the fresh execution's legacy team:sandbox member between the DEL/SREM script and this unconditional legacy ZREM; this line then deletes the fresh execution's only expiration-index entry, leaving the newly started sandbox invisible to ExpiredItems until the periodic healer eventually recreates it. The file already treats legacy members as still written during rolling deploys, so this race remains possible for the exact compatibility window the migration supports.
Useful? React with 👍 / 👎.
Uh oh!
There was an error while loading. Please reload this page.