FlowExtra.Admission (flowextra v0.6.0)

Copy Markdown View Source

The admission owner (FX-001): one process outside the replaceable stage subtree, holding its pipeline generation's admitted-work ledger.

The contract, in brief — a submission is one transaction: the owner reserves the permit AND forwards the packet (submit/3), so no caller death can strand a reservation without its packet. A submission either acquires a permit (the work is accounted from there) or meets a definite refusal — every definite refusal is enforced again at dequeue. The one thing that can still execute after its caller heard otherwise is a submission admitted in the last instant before its acknowledgment was lost, and that outcome carries its own name — {:unacknowledged, ref} — never the name of a refusal. Permits are held for the work's whole lifetime: a caller's timeout or death releases nothing, and a topology failure RETAINS its generation's permits — surviving work keeps executing and keeps its capacity until its own terminal release, while work destroyed with the topology stays admitted, outcome unknown. Nothing is invented into success or failure, and no slot is reused under surviving work. Every admitted job is either released to exactly one terminal outcome or remains an unresolved reservation — reported active, outcome unknown — until the pipeline is restarted (retention with manual recovery: stop and start again, a confirmed termination of the whole execution generation). See docs/research/flowex/C-admission-design.md.

The owner monitors every line worker. Any worker death quiesces the generation; because the wrapper supervisor is :rest_for_one with the owner FIRST, the owner's own death restarts the whole line too — a lost ledger can never overlap still-executing old work.

Summary

Functions

Reserves a permit for id without a packet — a diagnostic for ledger and reconciliation probes; the engine paths use submit/3. Refusal semantics as submit/3 with the settling grace as the whole budget; an unacknowledged diagnostic attempt reports {:error, {:unacknowledged, nil}}.

Returns a specification to start this module under a supervisor.

Releases a permit with its terminal outcome — exactly once, by ref, in any generation: a job admitted before a topology failure still holds its permit while it survives, and this is how it frees it. A second notice, or one for an unknown ref, is refused as stale.

The generation's ledger, for diagnostics and reconciliation tests. active counts unresolved reservations — queued and executing work, and work destroyed with the topology whose outcome is unknown until the pipeline is restarted; refs exposes those reservations' identities. refs is unresolved-reservation VISIBILITY, not admission history: a completed request leaves the list, so a ref's absence means released-or-never-admitted and does not prove non-admission. The ledger keeps no record of past admissions.

Submits ip as one transaction: the permit is reserved and the packet forwarded to the pipeline's ingress inside the owner, so the reservation can never exist without its packet. Returns the consumer incarnation the packet was forwarded to — one incarnation for the caller's monitor and for the submission itself.

Types

outcome()

@type outcome() ::
  :succeeded | :recovered | :expired | :failed | :cancelled | :unknown

Functions

admit(owner, id)

@spec admit(GenServer.name(), reference()) ::
  {:ok, integer()}
  | {:error, :overloaded | :unavailable | :noprocess}
  | {:error, {:unacknowledged, nil}}

Reserves a permit for id without a packet — a diagnostic for ledger and reconciliation probes; the engine paths use submit/3. Refusal semantics as submit/3 with the settling grace as the whole budget; an unacknowledged diagnostic attempt reports {:error, {:unacknowledged, nil}}.

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

release(owner, id, outcome)

@spec release(GenServer.name(), reference(), outcome()) :: :ok | {:error, :stale}

Releases a permit with its terminal outcome — exactly once, by ref, in any generation: a job admitted before a topology failure still holds its permit while it survives, and this is how it frees it. A second notice, or one for an unknown ref, is refused as stale.

report(owner)

@spec report(GenServer.name()) :: %{
  generation: integer(),
  capacity: pos_integer(),
  active: non_neg_integer(),
  refs: [reference()],
  status: :open | :settling,
  counts: %{optional(outcome()) => non_neg_integer()}
}

The generation's ledger, for diagnostics and reconciliation tests. active counts unresolved reservations — queued and executing work, and work destroyed with the topology whose outcome is unknown until the pipeline is restarted; refs exposes those reservations' identities. refs is unresolved-reservation VISIBILITY, not admission history: a completed request leaves the list, so a ref's absence means released-or-never-admitted and does not prove non-admission. The ledger keeps no record of past admissions.

start_link(capacity, worker_names, ingress, name)

@spec start_link(pos_integer(), [term()], {term(), term()} | nil, GenServer.name()) ::
  GenServer.on_start()

submit(owner, ip, deadline \\ nil)

@spec submit(GenServer.name(), FlowExtra.IP.t(), integer() | nil) ::
  {:ok, pid()}
  | {:error, :overloaded | :unavailable | :deadline | :noprocess}
  | {:error, {:unacknowledged, reference()}}

Submits ip as one transaction: the permit is reserved and the packet forwarded to the pipeline's ingress inside the owner, so the reservation can never exist without its packet. Returns the consumer incarnation the packet was forwarded to — one incarnation for the caller's monitor and for the submission itself.

While the topology settles, the attempt retries against ONE fixed budget: the caller's deadline when given (the call's clock is the admission's clock), capped by the settling grace. That budget travels IN the submission envelope and is checked at dequeue — a submission the caller has already been refused for (its budget expired waiting for the acknowledgment) is refused again by the owner, never forwarded, never executed after the fact.

Refusal certainty — the outcome names what the caller actually knows:

  • {:error, :overloaded} — definite: at capacity, nothing reserved, nothing will execute.
  • {:error, :unavailable} — definite: the owner itself refused — settling, or the attempt's deadline expired at dequeue — nothing reserved, nothing will execute.
  • {:error, {:unacknowledged, ref}} — UNKNOWN: the acknowledgment timed out and the submission may have been admitted in the last instant before the reply was lost. Keep the ref and inspect report/1's :refs — unresolved-reservation visibility, not admission history: a ref present means admitted-and-unresolved; a ref ABSENT means released or never admitted, and does not prove non-admission.
  • {:error, :deadline} — the caller's own budget ran out; the residual uncertainty is the ordinary timeout contract (execution may continue past a caller's deadline).
  • {:error, :noprocess} — communication with the admission owner failed because the process was unavailable or terminated. This result does not establish whether the submission executed or produced effects before that failure. It is not a definite admission refusal.