Skip to content

fix: cancel uncleared timers and abort listeners left by lost Promise.race branches - #176

Merged
V3RON merged 2 commits into
mainfrom
fix/cancel-race-timers-and-listeners
Jul 30, 2026
Merged

fix: cancel uncleared timers and abort listeners left by lost Promise.race branches#176
V3RON merged 2 commits into
mainfrom
fix/cancel-race-timers-and-listeners

Conversation

@V3RON

@V3RON V3RON commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

What is this?

Harness runs on iOS could linger for up to ~25 seconds after test results printed, because the CLI process could not exit. Several places raced real work against a setTimeout-based delay or an AbortSignal listener but never cleaned up the losing side of the race. A left-over ref'd timer keeps the event loop alive until it fires; a left-over abort listener leaks on signals that live for the whole session.

How does it work?

Two small cancellable primitives are added to packages/tools: delay(ms) returns { promise, cancel } so a race loser's timer can be cleared immediately, and waitForAbort(signal) returns { promise, cancel } so a race loser's 'abort' listener can be detached immediately. Every site that raced against a bare timer or listener now clears the loser in a finally:

  • The XCTest agent's graceful-shutdown wait (the confirmed ~25s regression: two uncancelled 30s timers stayed alive well after shutdown actually completed in under a second).
  • Android's waitWithSignal, used by three 1-second poll loops against a session-lifetime signal, now cleans up both directions of the race, plus the emulator-boot observation timeout.
  • The iOS and Vega app session poll loops now resolve their pending poll delay immediately when dispose() is called, instead of blocking teardown on a live 1-second wait.
  • The Jest package's startup-crash detection race now detaches its abort listener once a native crash watch settles.

Private per-file duplicates of delay/waitForAbort are removed in favor of the shared helpers. Sites that are already safe are left untouched, notably anywhere using AbortSignal.timeout() (its internal timer is unref'd and can't hold the process open) and the sequential (non-raced) delays in the crash-artifact polling code.

Why is this useful?

Every test run now exits promptly after results print instead of waiting out a stale timer, and long test suites no longer accumulate leaked 'abort' listeners on session-lifetime signals.

….race branches

Several sites raced real work against a setTimeout-based delay or an
AbortSignal listener and never cleaned up the loser. A ref'd pending
timer keeps the event loop alive, and an uncancelled listener leaks on
session-lifetime signals. The worst instance kept iOS harness runs
alive for ~25s after test results printed, via two uncancelled 30s
timers in the XCTest agent's shutdown wait.

Adds two shared, cancellable primitives to packages/tools (delay(),
waitForAbort()) and uses them to fix all six known sites: XCTest agent
graceful shutdown, Android's waitWithSignal (both directions) and its
emulator-boot observation timeout, the iOS/Vega app session poll loops
(dispose() no longer blocks on a live poll delay), and the jest
package's startup-crash race. Private per-file duplicates of
delay/waitForAbort are removed in favor of the shared helpers.
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
react-native-harness Ready Ready Preview Jul 30, 2026 6:14pm

Request Review

# Conflicts:
#	packages/platform-vega/src/runner.ts
@V3RON
V3RON merged commit a4d63b5 into main Jul 30, 2026
6 checks passed
@V3RON
V3RON deleted the fix/cancel-race-timers-and-listeners branch July 30, 2026 18:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant