fix(ios-runner): dispose a busy runner on close so open boots clean (#2552) - #2605
Conversation
…2552) A runner with watchdog-abandoned main-thread work refuses every command until it drains or escalates to RUNNER_WEDGED. Plain close retained it and the next open reused it, so close recovered nothing. The runner now stamps its live main-thread occupancy on every successful response; the daemon mirrors it on the session and, at the close retention decision, disposes a runner reported busy instead of pooling it back. Killing the process is the only way to abort uncancellable XCTest work.
Size Report
Startup median (7 runs, lower is better):
|
|
At c1c17da idle runners keep warm reuse, and only this session's runner is disposed. But the direct repro from #2552 is not covered yet. The command that stalls never marks the runner busy. When the watchdog gives up, the runner answers with an error ( The flag can also stay set after the runner drains. Only a successful stamped response clears it, so if every reply after the drain is an error (for example element not found),
The close test sets the flag by hand and calls the scheduler directly. A test that goes through close and then There is no live wedge repro yet, Smoke Tests are still running, and there are no conflicts. |
…y at close (#2552) Address review on #2552: - Runner tags the execution-watchdog timeout with the typed MAIN_THREAD_TIMEOUT code so the stalling command itself reports main-thread occupancy, not only a later RUNNER_BUSY refusal. - Host mirrors occupancy: set on RUNNER_BUSY or MAIN_THREAD_TIMEOUT, cleared by any other served reply (which reached the main thread and drained), left intact by transport failures. - Close that would retain a runner first stops it when occupied, awaited and lease-released via the new stopRunnerSessionIfBusy seam, before the retain-vs-stop decision. - Cover the production close route with lifecycle finalize tests and the Swift wire-code mapping.
|
Pushed 1. Stalled command now marks the runner busy from a typed code. The runner maps 2. Flag clears once the runner drains, even on an error. The catch clears 3. 4. Production close route is covered. Still no live 2–3 min wedge repro on demand; deterministic host + Swift tests carry the coverage. Validation: full |
|
Verified at
|
|
Reviewed at 44f78a9. The runner now sends a typed The tests do not yet prove that the steps chain. The close-finalization tests in lifecycle.test.ts#L247 cast the host with This change is device-facing, and the PR reports unit and build checks only. Please add a live run of the #2552 route on 44f78a9: a snapshot that trips the watchdog, then CI is green on 44f78a9, and there are no conflicts. The next step is the chained test and the live run. |
…dule (#2552) Collapse the retain-vs-stop close decision into the module that owns the occupancy bit. Close now calls one intent, releaseRunnerOnClose(deviceId, { retain }), instead of asking stopRunnerSessionIfBusy for a boolean and re-branching in lifecycle. The runner module keeps warm reuse for an idle runner and stops one still draining, so the busy fact never crosses the package boundary, the fire-and-forget scheduling path is gone, and the close route stays awaited.
|
Follow-up refactor
vitest-related 2912 green (420 files), fallow clean, format/lint/typecheck green, |
|
Reviewed at 029c9e9, as a follow-up to 44f78a9. The runner-session-close.test.ts#L290: the MAIN_THREAD_TIMEOUT test at L238 stops at the busy flag, and the release test sets The daemon test mocks copy the retain policy in three places (session-test-harness.ts#L52, session-relaunch-close.test.ts#L42, session-close-shutdown.fixtures.ts#L585), and the copies leave out the busy branch. If the real policy changes, those tests keep passing against stale behavior. Could they check only the call arguments, as session-relaunch-close.test.ts#L567 already does, or share one mock factory? session-test-harness.ts#L474 also un-exports about nine unrelated mocks ( Close finalization now waits for the runner module to stop a busy runner (runner-session.ts#L591), and the PR reports only unit, build and fallow checks. This device-facing route needs one live simulator run of #2552 on the current head: a snapshot that trips MAIN_THREAD_TIMEOUT, then close, then open, with the next snapshot served by a new runner session, plus an idle close and open that still reuses the warm runner. Smoke Tests was still running at review time, not failed; it runs open and close flows, so its result applies to this change. There are no conflicts. The next step is the chained timeout-to-fresh-session test and the live #2552 run. |
…, pin daemon close by call arguments (#2552) The release-on-close test now trips MAIN_THREAD_TIMEOUT through executeRunnerCommandWithSession instead of setting runnerMainThreadBusy by hand, so it fails if the catch-side busy marking stops reaching close. The daemon mocks for releaseIosRunnerOnClose no longer restate the retain policy; the close tests assert the { retain } argument instead. The unrelated session-test-harness un-exports are reverted.
|
All points from the review at 029c9e9 are addressed in 57ab7ad (test-only):
Details are in the PR body. |
|
Summary
closethenopenno longer hands a wedged iOS runner back to the caller (#2552). A runner still finishing watchdog-abandoned main-thread work refuses every command until it drains or escalates toRUNNER_WEDGED, but plaincloseretained it and the nextopenreused the same stalled runner, socloserecovered nothing.The runner now stamps its live main-thread occupancy (
runnerMainThreadBusy) on every successful response and tags the execution-watchdog timeout with the typedMAIN_THREAD_TIMEOUTcode, so the stalling command itself reports the occupancy — not only a laterRUNNER_BUSYrefusal. The daemon mirrors it on theRunnerSession: set onRUNNER_BUSYorMAIN_THREAD_TIMEOUT, cleared by any other served reply (reaching the main thread proves the drain), and left intact by a transport failure or an unstamped recovered response. At close,finalizeAppleApplicationClosehands a single intent,releaseRunnerOnClose(deviceId, { retain }), to the runner module that owns the occupancy bit: an idle retained runner keeps warm reuse under the idle timer, while a runner still draining is disposed (like theRUNNER_WEDGEDrestart) so the nextopenboots a clean runner. The decision lives with the state it reads,scheduleIosRunnerIdleStopstays pure scheduling, and the close route is awaited so the lease is released before the next request.Closes #2552.
Validation
Tested commit 57ab7ad (test-only on top of 029c9e9; production code unchanged).
pnpm check:affected --run: everything passed exceptmutation-model, which fails the same way on a cleanorigin/main(scripts/mutation/ownership.test.ts, kernel ownership).runner-session-close.test.ts: the release test now trips a realMAIN_THREAD_TIMEOUTthroughexecuteRunnerCommandWithSessioninstead of setting the flag; deleting the catch-side busy marking makes it fail. test(apple-runner): pin the close release truth table at the lifecycle seam #2623 (stacked) pins the full close truth table at the lifecycle seam.releaseIosRunnerOnClose(id, { retain })arguments; the mocks no longer copy the retain policy.--debug:snapshot,closelogsios_runner_idle_stop_scheduled; reopen +snapshotlogsios_runner_session_reuse; same xcodebuild pid.longpress 200 130 45000returnsMAIN_THREAD_TIMEOUTat 30 s with the runner still alive;closelogsios_runner_retain_skipped_busyand the runner exits; reopen logsios_runner_session_startupwith a new pid.Earlier at 029c9e9:
check:xctest-selection,check:packaged-runner-swift, and macOSbuild-for-testinggreen.