ReviewOS

zig-utils/zig-js

Threads: prevent async lock and condition corpus stalls after task dequeue

#836
Closed chris opened this 12 days ago ·
12 days ago

Parents: #502, #812. Related: #792 and #41.

Evidence

Pushed Linux CI for current main bcc3e75 reproducibly exhausted the full 600-second no-GIL deadline in both Debug and ReleaseSafe:

Both runners produced empty stdout/stderr and were reaped by the corpus deadline. The same run passed the normal mid-GC profile at 900/900 and the unsuppressed TSan mid-GC profile at 90/90, so this is not the #812 collection-completion signature or a reported sanitizer race. Root cause and introducing revision are not yet established.

Scope

Reproduce both promoted cases in direct no-GIL mode and inspect lock/condition waiter ownership, queued versus in-flight realm tasks, completion notification, run-loop quiescence, and teardown publication. Determine whether the stalled owner is parked behind a task that was dequeued but cannot complete, a lost wake, or another exact lifecycle boundary; do not infer the cause from the timeout alone.

Acceptance

  • Both named cases finish within their established Debug and ReleaseSafe budgets with exact semantic results.
  • Native task, waiter, reacquire, and completion ownership is proved exactly once, including exception and teardown paths.
  • A focused deterministic witness fails before and passes after the fix.
  • Normal and TSan no-GIL corpus, lifecycle/mid-GC fuzz, and full unit gates pass.
  • No added sleeps or polling windows, unconditional extra drains, weakened timeout/assertion, hidden GIL, or disabled GC.
12 days ago

Fixed on main by 14278310 — fix(threads): separate host task quiescence from child completion (#836, #837).

Root cause: d05d274c correctly made realm-host quiescence account for dequeued/in-flight tasks, but applied that peer-task wait to every spawned Thread evaluation tail. A host-owned asyncHold task could synchronously join() a child; the child then waited for the host task to finish before publishing completion, while the host task waited for the child. The same interpreter also represented only one active task burst, so reentrant task pumping could overwrite outer GC roots and owned-task accounting.

The fix:

  • makes only the realm host wait for peer-owned task quiescence; spawned Threads publish their own completion independently;
  • represents reentrant task-pump bursts as linked stack frames, preserving every outer/inner HoldJob for moving-GC trace/relocation and exact owned-task counts;
  • adds a deterministic host-task -> child join unit witness and a nested-root ownership unit.

Verification on the committed tree:

  • api/lock-async-hold.js: local >30s hang before -> pass; Debug 926 ms, ReleaseSafe 9 ms, TSan no-GIL 7.109 s
  • api/condition-async-wait.js: Debug 905 ms, ReleaseSafe 10 ms, TSan no-GIL 7.220 s
  • complete no-GIL corpus: 253/253 Debug and 253/253 ReleaseSafe, zero baseline regressions
  • full unit suite: 2,228 passed, 1 skipped, 0 failed/leaked
  • mid-GC threadfuzz: 900/900 normal and 90/90 unsuppressed TSan

No sleeps, polling windows, extra unconditional drains, hidden GIL, disabled GC, weakened assertions, or corpus exemptions were added. Linux push CI: https://github.com/zig-utils/zig-js/actions/runs/33440235057

Sign in to comment on this issue.