Skip to content

Fix lifecycle races: same-tick start/stop deadlock and uncancelled foreground health-check sweep #363

Description

@momics

Workstream B of the PR #350 follow-up re-review. Groups the withLifecycle concurrency/ownership blockers found against head a77e9c9. Tracked under #361.

Summary

The Tauri guest-js node lifecycle manager (packages/iroh-http-tauri/guest-js/lifecycle.ts) has two races: (1) calling start() then stop()/restart()/close() in the same tick (before the queued start yields) deadlocks, because the AbortController is not created until the queued startInternal runs, so the cancelling op's abortCurrent() sees controller === null; (2) the foreground health-check remover only detaches DOM listeners — it cannot cancel an in-flight probe/backoff sweep and does not re-check ownership after await, so onUnhealthy() can fire after the node was closed or replaced, and a replacement created with the same stable key does not retire the old listener, causing rebuild storms.

Evidence

Head a77e9c9.

  • fetch() signature is not web-standard — should accept httpi:// URL instead of (peer, path) #1 (P1) — same-tick lifecycle cancellation deadlocks. guest-js/lifecycle.ts:160-165,193-250. start() only enqueues startInternal() (lifecycle.ts:221-223); controller is assigned inside startInternal (lifecycle.ts:160-161). A caller doing start(); stop(); without yielding hits abortCurrent() (lifecycle.ts:206-208) while controller is still null → no-op. The queued start then installs a fresh controller and awaits a signal-owned run, while the queued cancel sits behind the unresolved start. Reviewer repro against exact head: start(); stop();{"result":"timeout","state":"starting","aborted":false}. Same deadlock shape as the originally-fixed bug, one scheduling boundary earlier.
  • Trailer headers on incoming requests — verify req.trailers is accessible and add TypeScript types #4 (P1) — removed/replaced foreground health checks can still fire. guest-js/lifecycle.ts:349-393, guest-js/index.ts:792-821. The returned remover (lifecycle.ts:388-393) removes listeners but sets no disposed flag; there is no ownership re-check after await probe() (line 363) or the backoff delay (line 371), so onUnhealthy() (line 375) can run post-close/replace. The unhealthy path calls onReconnectNeeded while leaving its listener installed, so a replacement with the same stable key does not retire the old JS listener → a later foreground event makes the stale node request another replacement (rebuild storm, accumulating listeners). void handler() (line 382) also drops any async rejection past the synchronous try/catch. Reviewer repro (invoke remover while a probe is pending): {"unhealthy":1}.

Impact

Remediation

  1. fetch() signature is not web-standard — should accept httpi:// URL instead of (peer, path) #1: represent a pending cancellation/generation independently of the current controller (or install the controller synchronously when start() is requested), so a same-tick cancel is honored. Add same-tick tests for all three of stop, restart, close.
  2. Trailer headers on incoming requests — verify req.trailers is accessible and add TypeScript types #4: make the sweep cancellable/disposable (a disposed flag checked after every await), make the unhealthy transition one-shot by unsubscribing before handing off to onReconnectNeeded, and await/explicitly consume the async recovery callback so rejections are observed.

Acceptance criteria

  1. Vitest: start(); stop(), start(); restart(), and start(); close() issued synchronously each resolve to the correct terminal state (no timeout, no starting hang).
  2. Vitest: invoking the health-check remover (or replacing the node) while a probe/backoff is pending prevents any later onUnhealthy()/reconnect from firing (unhealthy == 0), and no duplicate listeners remain.
  3. Async recovery-callback rejections are surfaced, not swallowed.
  4. packages/iroh-http-tauri lifecycle Vitest suite green.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High prioritybugSomething isn't workingconnectivityPeer discovery and connectionjavascriptPull requests that update javascript code

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions