Skip to content

A health recovery that starts while the daemon stop waits for cleanup is not waited for #1000

Closed

Description

@V3RON

What happened

When a daemon stops, DaemonServer.stop waits for the cleanup reaper's runs in progress (#985, PR #999) and then disposes the health monitor. A health recovery that started while the stop was waiting for the reaper is not waited for, so it can still act on a device after state.db or the drivers are closed.

What you expected

A graceful stop waits for work already started (architecture rule 12), including a health recovery that begins during the stop.

How to reproduce

Not reproduced. The code lens of the #999 review saw it while reading src/daemon/server.ts (stop order around line 642) and the health monitor's dispose.

Environment

main at 2ccae23. Found by reading code, not by a user report.

Written by an agent.

Activity

  1. added
    bug:newReported. An agent may triage it (rule 8), and nothing more.
    on Oct 25, 2026
  2. self-assigned this
    on Oct 25, 2026
  3. V3RON commented on Oct 25, 2026

    @V3RON
    ContributorAuthor

    Reproduction

    Defect, and wider than reported: a stop waits for no health-monitor work, so a recovery reboot started before the stop also outlives it.

    src/daemon/main.test.ts, "leaves no recovery reboot $name running once the drivers are disposed" (describe … (#1000)), driven through startDaemon with a driver that holds the recover boot:

    pnpm exec vitest run --project unit src/daemon/main.test.ts -t "#1000"
    
    started before the stop:
      AssertionError: expected [ [ 'recover started', 'recover finished', 'dispose' ] ]
      to deep equally contain [ 'recover started', 'dispose', 'recover finished' ]
    due while the stop waits for a cleanup run:
      AssertionError: expected [ [ 'recover started', 'recover finished', 'dispose' ], [ 'dispose' ] ]
      to deep equally contain [ 'recover started', 'dispose', 'recover finished' ]
    

    Case 2 accepts both fixes: the stop waits, or the recovery never starts.

    Root cause

    LeaseHealthMonitor.dispose() (src/leasing/lease-health-monitor.ts:77) only cancels timers. The tick (:101) and each recovery (:257) are fire-and-forget promises nobody keeps, so there is nothing to await, and DaemonServer.#stop (src/daemon/server.ts:643) awaits nothing from the monitor before options.dispose (:649) disposes drivers and closes state.db. The armed tick during reaper.settle() (:642) is just one way in; #dispatchRecovery (:251) also ignores #disposed, so a running tick can start a recovery after dispose.

    Simplest fix

    LeaseHealthMonitor.settle() awaiting the running tick and recoveries; #dispatchRecovery returns once disposed. In #stop, dispose the monitor beside reaper.dispose(), then await its settle() after reaper.settle(); update step 4 of docs/internal/flows/startup.md. Tried locally (not pushed): the test, server.test.ts and lease-health-monitor.test.ts pass.

    Alternatives rejected

    • Only disposing before the reaper wait: case 1 still fails.
    • Draining via leasing.settle(): runs after the monitor is disposed; the server owns its stop, like the reaper.
    • Aborting the reboot: makeReady takes no signal; a half reboot is worse than waiting.

    Risk

    • A stop may now wait up to one recovery reboot (~30 s); check the SIGTERM and update paths.
    • After dispose() resolves a backoff early, #recover must still return without rebooting.
    • A tick's #giveUp → releaseDeviceLost may run during the stop, before leasing.settle() drains reclaims.

    Written by an agent.

  4. removed their assignment
    on Oct 25, 2026
  5. added
    bug:readyAn agent may fix it.
    and removed
    bug:newReported. An agent may triage it (rule 8), and nothing more.
    on Oct 25, 2026
  6. self-assigned this
    on Oct 25, 2026
  7. V3RON commented on Oct 25, 2026

    @V3RON
    ContributorAuthor

    Delivery run

    Merged: #1004 (#1000)

    Written by an agent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bug:readyAn agent may fix it.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions