Unwind runner resources in reverse order on shutdown (#37) - #39
Merged
Merged
Conversation
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
This was referenced Oct 3, 2026
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #37.
What
Registrations are effects (adapter-seam.md admission discipline 2), applied to the native runner:
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)registerstask-store→activity-log→memory-store→task-loopin creation order;_run_runnerunwinds them in afinallyinside theRunnerLease, for both theserveand--oncepaths. Previously closing the SQLite connections was left to garbage collection afterservereturned — 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.pyis 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_runnerstill 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)
prepare()itself fails mid-wiring, partial registrations are not unwound — the runner is a whole process exiting on the spot, ownership is still released byRunnerLease's exception path, and the OS reclaims the rest.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 onrunner --once; unpatched pass leaves no shutdown warnings and readable data.test_runner_loads_policy_under_lock_before_readyupdated to the new_wiring(teardown)signature (same intent: configuration loads under ownership).Full offline suite (dead-proxy), plus
examples/first_pr_watch.py: all 8 scenarios pass, including the real background-runner cooperative-shutdown scenario.