Skip to content

fix(menubar): await terminationHandler, not a blocked queue thread (real fix for the Loading wedge)#462

Merged
iamtoruk merged 1 commit into
mainfrom
fix/menubar-terminationhandler-wait
Jun 9, 2026
Merged

fix(menubar): await terminationHandler, not a blocked queue thread (real fix for the Loading wedge)#462
iamtoruk merged 1 commit into
mainfrom
fix/menubar-terminationhandler-wait

Conversation

@iamtoruk

@iamtoruk iamtoruk commented Jun 9, 2026

Copy link
Copy Markdown
Member

Summary

Fixes the menubar wedging on "Loading…" with an unresponsive popover after sustained idle/use — the exact symptom #426 targeted but did not actually resolve. A build carrying #426's runProcess still wedged after ~a week of soak; a live sample showed ~80 threads parked in DataClient.runProcess → waitUntilExit with the timeout never firing.

Root cause

#426 moved the blocking waitUntilExit and its timeout watchdog onto the same global(qos:.utility) queue. Under sustained load every utility worker ends up blocked in waitUntilExit, so the timeout (same queue) can never be scheduled to kill the stuck processes — re-creating the original deadlock at the larger global-queue scale (which is why it took ~a week to saturate instead of minutes). A watchdog living on the queue it's meant to rescue can't fire once that queue is saturated.

The CLI itself is healthy (status --format menubar-json --period 30days returns in 16s) — the wedge is entirely in the Swift subprocess-wait layer.

Fix

  • Await process.terminationHandler (set before run(), bridged through a one-shot ProcessExitSignal) instead of parking a worker thread in waitUntilExit. terminationHandler fires on a Foundation-managed queue and blocks nothing, so the timeout always has a free thread to fire on.
  • Add an actor-based AsyncSemaphore capping concurrent CLI spawns at 6 so a wake-burst of refreshes can't fan out into dozens of node processes.

Testing

  • swift build clean. Rebuilt + relaunched the menubar; live sample of the new build shows zero waitUntilExit frames and no CLI pile-up.
  • New tests: 50 concurrent normally-exiting processes all complete via the terminationHandler path; the timeout reliably fires + terminates under concurrency; the semaphore caps concurrency at its limit.

Heads-up: pre-existing red CI on main (unrelated to this PR)

swift test currently fails to compile two test files on mainContributionHeatmapTests and AppStoreRefreshRecoveryTests — missing localModelSavings/savingsUSD args after a payload field was added during the week. This PR doesn't touch those files; it's a separate pre-existing breakage. Can fix in a follow-up.

Supersedes the same-queue approach merged in #426.

…read; cap CLI spawns

The #426 fix moved waitUntilExit and its timeout onto the same global(qos:.utility)
queue. Under sustained load every utility worker blocked in waitUntilExit, so the
timeout could never be scheduled to kill them and the menubar wedged on Loading
forever (confirmed via sample after ~a week of soak). Await process.terminationHandler
(fires on a Foundation queue, blocks no worker) so the timeout always has a free
thread. Add an actor-based async semaphore capping concurrent CLI spawns at 6.
@iamtoruk iamtoruk merged commit f5d1a85 into main Jun 9, 2026
3 checks passed
@iamtoruk iamtoruk deleted the fix/menubar-terminationhandler-wait branch June 9, 2026 19:27
@iamtoruk iamtoruk mentioned this pull request Jun 9, 2026
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