One owner-backed cleanup stack for a command's active provider work.
A session initially monitors its build creator and owns every acquisition
scope opened through PtcRunner.Kernel.ResourceRegistrar. An active session
is sealed to one exact prepared operation. After optional application
admission it anchors that operation's absolute run deadline, then atomically
claims the operation before provider acquisition, so neither another prepared
run nor a replay can share its cleanup boundary. Before provider
callbacks can start, execution binds the session exactly once to its lifecycle
owner and to the run's one provider-task owner. That task owner is an
internal process outside both lifecycles and monitors the session in return,
so live tasks are drained before any provider closer runs and are killed
outright when the session or the run state disappears, including when the
session is terminated at its cleanup deadline and terminate/2 never runs.
Each scope starts provisional, activates immediately before acquisition, and
is either committed with one provider close operation or aborted. Normal
close and lifecycle-owner death drain committed scopes in reverse commit
order, then discard remaining provisional scopes in reverse creation order.
A local process root monitors its scope's signal owner and synchronously
registers with the private scope controller before its start operation
returns. The controller forwards registration into one authoritative cleanup
owner; terminalization handoff and cleanup address that owner directly and
remain available if the controller stalls. Scope cleanup runs its closer
first, then the cleanup owner stops the signal owner so registered roots can
shut down normally; roots still alive at the bounded cutoff are killed and
observed. Registration, terminalization handoff, normal cleanup, and
session-crash cleanup therefore share one serialized root set. An OAuth root with unsettled
terminal work must close itself to new work and transfer ownership before the
force-close phase; abnormal session death permits that handoff during the
cooperative owner-down window.
The sealed provider_cleanup_timeout_ms is one budget for the whole terminal
episode, not one per stage. The first terminal action anchors this session's
absolute cleanup deadline; an OAuth cancellation and the reverse-order
cleanup that follows it each consume what remains of that one deadline, and
every close operation runs in a heap-bounded worker inside it. That budget
also bounds the caller: close/1 waits one reply grace beyond what remains of
that deadline and then terminates a session that has not answered, so a wedged
session becomes a classified cleanup failure rather than an unbounded wait and
never a second budget. That deadline bounds this session's own cleanup, not
the last root's exit: terminating a wedged session ends the caller's wait, and
its scope reapers then observe that death and force their registered roots
within their own separately bounded tail. Aborting one acquisition scope
mid-run is likewise not part of the episode and keeps its own bounded budget.
Failures never stop later cleanup attempts. This is a process-local ownership
boundary, not a durable resource journal or a security boundary against
trusted code in the same VM.
Scope lifecycle budgets
Three deadline classes cover a scope, and each registrar action belongs to
exactly one. The operation deadline bounds useful work — opening, activating,
and attempting to commit a scope. A per-scope abort deadline, one freshly
anchored provider_cleanup_timeout_ms per abort episode, bounds tearing one
provisional scope down while the session stays usable; N independent aborts
may spend N budgets, because anchoring the terminal deadline at a mid-run
abort would let one early rejected scope exhaust the session's eventual
shutdown, and spending the operation deadline would hand cleanup a zero
remainder exactly when abort is most often triggered by that deadline
expiring. The terminal episode above is the third.
The session's own anchored deadline decides admission — the open, activate,
and commit fences — never a caller's copy of it; an abort is deliberately
anchored by its caller, where the episode begins, and the session only clamps
what it will spend. The operation deadline sealed into a ResourceRegistrar
is likewise returned by the session rather than supplied: begin_operation/2
returns a new sealed handle while the pre-begin one stays valid carrying no
deadline, so either could otherwise mint a legitimately attested handle that
waits forever. The handle's cleanup budget is not returned this way — it is
pinned by the session attestation instead, and claim_operation/3 validates
limit equality, so the two budgets are protected by different mechanisms. For the same reason an
active session refuses to open a scope until its operation has started; a
session with no operation identity is a direct embedding and keeps its
synchronous semantics.
Commit is the only ownership-transfer edge and the only call that may not
abandon its reply. Every branch of the commit handler replies, and the branch
that takes ownership replies in the same return, so a reply is sent if and
only if the handler took ownership — and a session killed between that state
change and the reply leaving will never run the closer, so the caller taking
it is still exactly one owner. Signals arrive in send order, so a reply that
was ever sent precedes any :DOWN from that session. A commit therefore
keeps its request alive across timeouts — the operation budget, then one
freshly anchored cleanup budget — and a session silent through both is wedged
and is killed, after which a single zero-timeout look at the reply assigns the
owner. Timeouts decide only when to escalate; ownership is always assigned by
a reply or a :DOWN, never by elapsed time.
Summary
Functions
Anchors the operation clock this operation is entitled to.
Returns a specification to start this module under a supervisor.
Types
Functions
Anchors the operation clock this operation is entitled to.
A run spends run_duration_ms; doctor --connect spends
doctor_connectivity_timeout_ms, which is a different and much shorter
budget. The caller names its operation rather than supplying a duration:
both budgets are sealed into the session from the limits it was started with,
so no caller can anchor a clock longer than the one its limits allow, and no
operation can silently inherit a clock sized for another.
run_deadline/1 returns whichever was anchored.
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec close(t()) :: :ok | {:error, :provider_cleanup_failed}
@spec open_registrar(t()) :: {:ok, PtcRunner.Kernel.ResourceRegistrar.t()} | {:error, :provider_session_unavailable}
@spec start(PtcRunner.Kernel.Limits.t()) :: {:ok, t()} | {:error, term()}