fix(sandbox): keep a stopped run from starting or touching the runner's next run - #164
Merged
Merged
Conversation
…'s next run A start waiting for a sandbox slot survived stop(); when the pool handed the runner to the next user, the stale run started first, with its own removed sketch directory, on the next user's session. Runs now carry a generation and an abort signal, the slot wait is cancellable, and the process controller drops events of replaced children. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Purpose
R3a of the refactoring series (
docs/UNOSIM_REFACTORING_OPL.md), audit finding S4, plus S4-CHILD found while verifying it.Findings – reproduced deterministically
ExecutionStateis reused across runs of a pooled runner and had no run identity.STARTING. A started the sandbox with its own, already cleaned-up sketch directory, on B's session and batchers.expected '<A's removed sketch dir>' to contain 'RUN_B'.ProcessControllerforwarded stdout/stderr/close/error of every child to whatever listeners the controller held at the time. A replaced child's late output andclosereached the next run (expected 'OLD-CHILD\n' to be 'NEW-CHILD\n').Change
ExecutionState.runGeneration/runAbort(optional fields).runSketchstarts a run token; every step after anawaitchecksrun.isStale(). A stale run:currentSketchDirandcurrentContainerNameare set only after the run has proven it is current; the container name is set after the slot is granted.SandboxStartSemaphore.acquire(onQueued, timeoutMs, signal): an aborted waiter leaves the queue and never takes a slot.SandboxRunner.stop()aborts the current run first.ProcessControllerforwarders ignore events from a child that is no longer the current one.this.procis only reassigned byspawn, so the current child's events are unaffected.Remaining window (documented in the OPL):
stop()landing exactly during the millisecond-scalespawncall. Orphan cleanup (R4a) covers it. Reset ownership in the runner is the separate follow-up R3b.Tests
tests/server/services/sandbox/runner-reuse-race.test.ts(real pool, runner, execution manager and semaphore; only the child process and the Docker CLI are faked). It asserts:runAresolvesfalse;docker rmtouches B.ExecutionManagerchange.docker-compile-semaphore-abort.test.ts,process-controller-stale-child.test.ts(both RED before).npm run check, ESLint, unit 2714 passed, Docker integration 26/26 locally against real sandbox containers, pre-push incl. Sonar quality gate PASSED.🤖 Generated with Claude Code