PtcRunner.Kernel.ProviderSession (PtcRunner v0.14.0)

Copy Markdown View Source

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

t()

@opaque t()

Functions

begin_operation(session, operation)

@spec begin_operation(t(), :run | :connect) ::
  {:ok, t()} | {:error, :provider_session_unavailable}

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.

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

close(session)

@spec close(t()) :: :ok | {:error, :provider_cleanup_failed}

open_registrar(session)

@spec open_registrar(t()) ::
  {:ok, PtcRunner.Kernel.ResourceRegistrar.t()}
  | {:error, :provider_session_unavailable}

start(limits)

@spec start(PtcRunner.Kernel.Limits.t()) :: {:ok, t()} | {:error, term()}

valid?(session)

@spec valid?(term()) :: boolean()