Repository navigation
fix(runner): evict spent box handles, map every core status - #1138
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
🚧 Files skipped from review as they are similar to previous changes (6)
📝 WalkthroughWalkthroughThe runner now maps all runtime states, reads persisted state directly, and validates cached handles before reuse. Conditional eviction supports restart after self-stopped boxes. Go state constants and network test endpoints were updated. ChangesBox lifecycle state handling
Network integration endpoint update
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
📦 BoxLite review — couldn't completepowered by BoxLite |
A box can stop itself — its main command exits and the guest powers the VM off — leaving the runner's cached handle bound to a dead VM. Nothing cleared that entry on a self-stop, so every later Start, exec or copy on that box was answered with the corpse for the life of the runner process: "Box <id> is no longer running and this handle is spent". getOrFetchBox now validates a cached handle before serving it and hands back a fresh one once the box is no longer up, which is the auto-restart the control plane relies on. Paused counts as up, matching the runtime's own BoxStatus::is_active. The equivalent guard already existed on the Rust side in `boxlite serve`. Eviction unmaps but does not free the handle: the same *boxlite.Box is handed to every caller and nothing serializes per box, so freeing would pull it out from under a goroutine mid-call. That trade only holds while eviction stays on operator-driven paths, so GetBoxState — which the sync loop runs for every box every 10s — now reads through the runtime, which needs no handle at all. Routing it through the cache would have evicted and re-fetched per box per tick, accumulating dead handles for as long as the runner ran. Separately, the core-to-control-plane state mapping covered 3 of the runtime's 7 statuses. Failed and Stopping both fell through to Unknown, which the API's usage accounting counts as compute-consuming while Error is excluded. The mapping is now exhaustive and unit-tested, and the Go SDK gained the three State constants the FFI layer could already emit but Go could not name. Claude-Session: https://claude.ai/code/session_01CGk6BR7BHsUqijZagdfVWN
The Go secret-substitution test reached httpbin.org while its Python and Node counterparts both use httpbingo.org. httpbin.org started answering 503 and took the whole pre-push suite down with it, letting a third party decide whether any sdks/ change can be pushed. Aligning on the host the other two SDKs already rely on removes the odd one out. Tolerating an upstream 5xx instead was considered and rejected: the guest reaches this host through boxlite's own MITM, which answers 502 for any transport error of its own, secret substitution included. Skipping on a server error would therefore hide the regression this test exists to catch. Claude-Session: https://claude.ai/code/session_01CGk6BR7BHsUqijZagdfVWN
The three SDK secret suites all reach httpbingo.org; this case was the last reference to httpbin.org left in any test. Aligning it removes a second third-party dependency from the test surface. The prompt was an httpbin.org outage — it served 503 for long enough to take a pre-push run down — which exposed how this case degrades: it skips on a nonzero exit, so while the host was unreachable it reported "network not available in guest" and stopped exercising guest networking, looking identical to a real egress break. The silent-skip mechanism itself is unchanged here; only the host that was tripping it. httpbingo.org/get was confirmed answering 200. Claude-Session: https://claude.ai/code/session_01CGk6BR7BHsUqijZagdfVWN
49a64bb to
3641450
Compare
A box can stop itself — its main command exits and the guest powers the VM
off. The handle the runner cached while that box was up is spent from that
moment: it still holds the dead VM and can never boot another. Nothing cleared
the entry on a self-stop, so every later Start, exec or copy on that box was
answered with the corpse for the life of the runner process, surfacing on the
dashboard as
Box <id> is no longer running and this handle is spent (code=11).The equivalent guard already existed on the Rust side in
boxlite serve(
src/cli/src/commands/serve/mod.rs:922); the Go copy in the runner was theunfixed sibling.
Before
After
Why eviction does not free the handle
getOrFetchBoxhands the same*boxlite.Boxto every caller and the runnerserializes nothing per box, so calling
Closethere would free an FFI handleanother goroutine is mid-call on. Eviction therefore unmaps only, and the
unmapped handle leaks for the process lifetime. The cost tracks request volume —
which is why
GetBoxState, run for every box on a timer, was moved off thecache entirely. Ref-counting the wrapper is the real fix and belongs in the SDK.
Second commit: exhaustive state mapping
The core→control-plane mapping covered 3 of the runtime's 7 statuses.
Failedand
Stoppingboth fell through toUnknown, which the API's usage accountingcounts as compute-consuming (
BOX_STATES_CONSUMING_COMPUTE) whileErrorisexcluded — so a dead box read as occupying compute. The mapping is now
exhaustive and unit-tested, and the Go SDK gained the three
Stateconstantsthe FFI layer could already emit but Go could not name.
Third commit: unblocking the pre-push suite
The Go secret-substitution test reached
httpbin.orgwhile its Python and Nodecounterparts both use
httpbingo.org.httpbin.orgbegan answering 503 andtook the whole pre-push suite down with it. Tolerating an upstream 5xx instead
was considered and rejected: the guest reaches that host through boxlite's own
MITM, which answers 502 for any transport error of its own — secret
substitution included — so skipping on a server error would hide the regression
that test exists to catch.
Verification
only the reproducer present,
TestIntegrationSelfStoppedBoxRestartsAfterCachedHandleGoesSpentfails with the genuine
code=11error; restored, it passes (18.30s, real microVM).stopping,pausedandfailed. A literal full revert is impossible there —apiStateFromCoreis itself the new symbol — so that side is a compile error,recorded rather than papered over.
evictBox's identity guard has unit coverage including a concurrent case,green under
-race.go vetclean;apps/runner ./pkg/...and the fullsdks/gosuite green onthis rebased base.
Summary by CodeRabbit
New Features
Bug Fixes
Tests