Skip to content

Unwind runner resources in reverse order on shutdown (#37) - #39

Merged
hsliuustc0106 merged 2 commits into
mainfrom
refactor/disposer-shutdown
Oct 3, 2026
Merged

hsliuustc0106 merged 2 commits into
mainfrom
refactor/disposer-shutdown

Conversation

@hsliuustc0106

@hsliuustc0106 hsliuustc0106 commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Closes #37.

What

Registrations are effects (adapter-seam.md admission discipline 2), applied to the native runner:

  • new core.teardown.Teardown — a registry: resources register a named disposer in creation order; run() pops them in reverse, runs every step even when one raises, reports failures by name only (never exception text, which can carry private data), and is idempotent across repeated stops. Not thread-safe by contract: register and run from the orchestrating thread.
  • TaskLoop.close() — drains an in-flight summary bounded by the summary budget (a hung provider costs the wait, not the shutdown), then drops the per-task single-flight locks. Safe to call twice.
  • _wiring(teardown) registers task-store → activity-log → memory-store → task-loop in creation order; _run_runner unwinds them in a finally inside the RunnerLease, for both the serve and --once paths. Previously closing the SQLite connections was left to garbage collection after serve returned — nondeterministic and unordered (any lingering reference, e.g. an exception traceback or the summary worker, could keep a store open past the stop window); now they close explicitly, in reverse creation order, while still under the lease.
  • runner_control.py is deliberately unchanged: its __exit__ already unwinds in correct reverse order (watcher join → file cleanup → lock release) and its behavior is pinned by its own test suite.

Cooperative-stop semantics are untouched: stop_runner still waits for real lock release and reports a pending stop on timeout; teardown completes before ownership is released, so a replacement runner never meets a half-closed store.

Deliberate edges (documented, not coded)

  • If prepare() itself fails mid-wiring, partial registrations are not unwound — the runner is a whole process exiting on the spot, ownership is still released by RunnerLease's exception path, and the OS reclaims the rest.
  • A second SIGINT during the teardown window could interrupt it; the first SIGINT already triggers the graceful stop path, and the process exits regardless.

Tests

  • tests/test_teardown.py: reverse order + idempotence; every step runs on failure with names-only reporting; close() bounded under a hung provider and draining a finishing one; the real wiring unwinds all four registrations in reverse on runner --once; unpatched pass leaves no shutdown warnings and readable data.
  • test_runner_loads_policy_under_lock_before_ready updated to the new _wiring(teardown) signature (same intent: configuration loads under ownership).
545 passed in 22.83s

Full offline suite (dead-proxy), plus examples/first_pr_watch.py: all 8 scenarios pass, including the real background-runner cooperative-shutdown scenario.

Registrations are effects: resources register a named disposer in
creation order and shutdown unwinds them in reverse, under the lifetime
lease — the next runner never meets a half-closed store.

- core.teardown.Teardown: registry that pops disposers in reverse,
  runs every step even when one raises, reports failures by name only
  (never exception text, which can carry private data), idempotent.
- TaskLoop.close: drain an in-flight summary bounded by the summary
  budget (a hung provider costs the wait, not the shutdown), then drop
  the per-task single-flight locks. Safe to call twice.
- _wiring(teardown) registers task-store, activity-log, memory-store,
  task-loop closes; _run_runner unwinds them in a finally inside the
  RunnerLease for both serve and --once paths. Previously nothing
  closed the SQLite connections after serve returned.
- runner_control is unchanged: its __exit__ already unwinds in correct
  reverse order and its behavior is pinned by its own tests.

Cooperative-stop semantics are untouched: stop_runner still waits for
real lock release and reports a pending stop on timeout.

Closes #37
@hsliuustc0106
hsliuustc0106 merged commit fd95369 into main Oct 3, 2026
2 checks passed
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.

Runner registrations as effects: reverse-order disposers for shutdown

1 participant