Owns the ETS table that holds open macrostep spans and, per session, the last span context ots-j82's links read.
The table is created in init/1 and otherwise untouched by this
process - it is :public and :named_table so handlers read and write
it directly from the session process that calls
OpentelemetryStatifier.Handler.handle_event/4, with no message hop
through this GenServer. Owning the table this way (rather than letting
setup/1 create it) means the table exists before any handler can fire,
survives every session process, and gives ots-lt6's sweep timer a
natural home - opentelemetry_ecto's latent "owned by whoever called
setup" bug does not reach this package.
Row shapes, fixed here because ots-j82 and ots-lt6 both read them
directly against the table name carried in OpentelemetryStatifier.Config:
{{:span, span_ref}, session_id, %SpanEntry{}}
{{:last_span_ctx, session_id}, session_id, span_ctx}
{{:invoke_parent, session_id}, session_id, span_ctx}
{{:session_pid, session_id}, session_id, pid}
{{:sibling_span, span_ref}, pid, %SiblingEntry{}}
{{:parent_span, ref}, pid, %ParentEntry{}}The :sibling_span row is the sibling families' half (ADR-0004): a
[:statifier_persistence, :run, :step, :start] opens one keyed on the
same span_ref convention, and it carries a pid in element 2 where
the macrostep rows carry a session_id, because a step span is scoped
to the process that drove it rather than to a logical session. That is
what makes fetch_innermost_sibling_span/2 a one-match_object
lookup for "is one of this bridge's own spans open around me, in this
process" - the question every nested sibling span and every durable
macrostep span asks.
The :parent_span row is the foreign-driver half of the same
mechanism (ADR-0004 decision 4, and its 2026-09-02 Notes): a host that
runs a durable stepper this package knows nothing about declares its
own enclosing span through OpentelemetryStatifier.Parent.register/2,
and the row it writes is read by exactly the same "is something of
mine open around me, in this process" lookup. It carries a pid in
element 2 for that reason. The one thing it is not is a span this
bridge owns: sweep/1 deletes an abandoned :parent_span row without
ending its span, where a :sibling_span row's span is ended with an
error status.
The :invoke_parent row is written when a child session's :init event
names an invoked_by parent whose macrostep span is open at that
moment, and consumed (removed) by the child's own :initialize
macrostep start, which turns it into a span link.
The :session_pid row records the session process behind session_id -
the handler runs inside the session process, so self() at event time
is that pid. It exists for the sweep: :terminate does not fire on a
brutal kill, so sweep/1 walks these rows and, for every session whose
process is no longer alive, ends any still-open macrostep spans with an
error status and deletes all the session's rows - the design note's
"sweeps entries whose sessions no longer exist rather than trusting the
event alone". This GenServer runs the sweep on a timer; tests call
sweep/1 directly.
The %SpanEntry{} in a :span row gains its macrostep counter after
the fact: the macrostep :start event carries no such measurement, so
the handler stamps it from the first intra-macrostep event that lands
on the span. fetch_open_span_ctx/3 is the keyed
(session_id, macrostep) read that field exists for.
session_id is duplicated into element 2 of every row shape so a
sweeper can find every row for a session with one
:ets.match_object/2 or :ets.match_delete/2, without decoding either
row's structured value.
Summary
Functions
Returns a specification to start this module under a supervisor.
Removes the host-declared enclosing span under ref. The declared span
is not ended - the host that opened it owns its lifetime. Removing
a ref that is not there is :ok, so unregister/1 is idempotent and
safe to call from an after block that may run twice.
Removes every row session_id owns, first ending any still-open
macrostep spans with an error status carrying orphan_message - an
orphan is reported, never silently dropped. Called by the handler's
:terminate clause and by sweep/1 for sessions whose process died
without a :terminate.
The context a span opened in pid right now should nest under: the
innermost thing this bridge has recorded as enclosing that process,
whichever of the two kinds it is, or :error when there is none.
As fetch_innermost_open_span/2, but also returns the span_ref the
entry is keyed under, so a caller that has just read the row can write
an updated one back through put_open_span/3 without a second scan of
the table. OpentelemetryStatifier.Handler uses it on the span-event
path, where every event both lands on the innermost span and carries
the macrostep counter that span is missing.
Fetches the innermost open span for session_id - the entry with the
greatest started_at among the session's open rows, since ADR-0039
re-entry can hold two spans open at once and an intra-macrostep event
belongs to the most recently opened one. Returns :error when the
session has no open span (an effect event racing a crash's cleanup is
contract-legal, not a bug).
Fetches the innermost sibling span open in pid - the entry with the
greatest started_at, since a parent run creating a durable child
inside its own step holds two step spans open on one process. Returns
:error when the process has none, which is the ordinary case for a
host that attached only the sibling setups it uses.
Fetches the last span context recorded for session_id, returning
{:ok, span_ctx} on a hit or :error for an unknown session.
Fetches the span context of the open macrostep span session_id
recorded macrostep on, or :error when there is none - an unknown
session, a macrostep whose span has already closed, or an open span
that has not yet seen an event carrying the counter.
Fetches the process recorded for session_id by put_session_pid/3,
or :error for a session the bridge has not seen. The sibling
handlers use it to check that a session's open macrostep span belongs
to this process before landing a span event on it - a delivery-seam
event on an Oban worker must never write onto a span another process
has open for the same scope.
Creates (or returns, if it already exists under this process) a
:public/:named_table ETS table under name. Tests call this
directly with a unique name per test so they never depend on the
application-started table.
Records the parent macrostep span context a child session's
:initialize macrostep will link to, keyed by the child's session_id.
Records span_ctx as the last span context observed for session_id.
Stores an open span's %SpanEntry{} under its span_ref.
Stores a host-declared enclosing span's %ParentEntry{} under ref,
keyed for lookup by the process whose spans nest under it.
Records pid as the session process behind session_id, for the
sweep's liveness check. Idempotent - the table is a :set, so a
session's row is written once per macrostep at no accumulating cost.
Stores an open sibling span's %SiblingEntry{} under its span_ref,
keyed for lookup by the process that opened it.
Ends the orphans a brutal kill leaves behind: for every :session_pid
row whose process is no longer alive, delegates to delete_session/3,
and for every :sibling_span row whose process is no longer alive,
ends that span with an error status and deletes the row. A live
process's rows are never touched - an open span on a live process is
just a step or a macrostep in flight, whatever its age.
Looks up and removes the invoke-parent span context stored for
session_id, returning {:ok, span_ctx} on a hit or :error when no
parent was recorded - a session that was not invoked by anyone.
Looks up and removes the open span under span_ref, returning
{:ok, entry} on a hit or :error on a miss - a :stop event with no
matching :start is a contract-legal shape, not a bug, so this never
raises.
Looks up and removes the open sibling span under span_ref. Returns
:error on a miss, which - as for take_open_span/2 - is a
contract-legal shape rather than a bug.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
Removes the host-declared enclosing span under ref. The declared span
is not ended - the host that opened it owns its lifetime. Removing
a ref that is not there is :ok, so unregister/1 is idempotent and
safe to call from an after block that may run twice.
Removes every row session_id owns, first ending any still-open
macrostep spans with an error status carrying orphan_message - an
orphan is reported, never silently dropped. Called by the handler's
:terminate clause and by sweep/1 for sessions whose process died
without a :terminate.
@spec fetch_enclosing_ctx(atom(), pid()) :: {:ok, OpenTelemetry.Ctx.t()} | :error
The context a span opened in pid right now should nest under: the
innermost thing this bridge has recorded as enclosing that process,
whichever of the two kinds it is, or :error when there is none.
Two row shapes answer the same question and are ordered against each
other on the one clock they share (System.monotonic_time/0): a
:sibling_span row, which is a span this bridge opened itself from a
sibling package's :start event, and a :parent_span row, which is a
span a host declared through
OpentelemetryStatifier.Parent.register/2. The most recently opened of
them is the innermost, so a bridge-opened step span inside a host's
declared span parents what follows, and so does a declaration made
inside a step span.
A declaration whose registrant is dead is skipped: nobody is left to end that span or to withdraw the declaration, so nesting under it would attach live spans to an abandoned trace. Falling back to the next enclosing row - or, usually, to no parent at all - is the honest answer.
@spec fetch_innermost_open_row(atom(), String.t()) :: {:ok, reference(), OpentelemetryStatifier.SpanEntry.t()} | :error
As fetch_innermost_open_span/2, but also returns the span_ref the
entry is keyed under, so a caller that has just read the row can write
an updated one back through put_open_span/3 without a second scan of
the table. OpentelemetryStatifier.Handler uses it on the span-event
path, where every event both lands on the innermost span and carries
the macrostep counter that span is missing.
@spec fetch_innermost_open_span(atom(), String.t()) :: {:ok, OpentelemetryStatifier.SpanEntry.t()} | :error
Fetches the innermost open span for session_id - the entry with the
greatest started_at among the session's open rows, since ADR-0039
re-entry can hold two spans open at once and an intra-macrostep event
belongs to the most recently opened one. Returns :error when the
session has no open span (an effect event racing a crash's cleanup is
contract-legal, not a bug).
@spec fetch_innermost_sibling_span(atom(), pid()) :: {:ok, OpentelemetryStatifier.SiblingEntry.t()} | :error
Fetches the innermost sibling span open in pid - the entry with the
greatest started_at, since a parent run creating a durable child
inside its own step holds two step spans open on one process. Returns
:error when the process has none, which is the ordinary case for a
host that attached only the sibling setups it uses.
@spec fetch_last_span_ctx(atom(), String.t()) :: {:ok, OpenTelemetry.span_ctx()} | :error
Fetches the last span context recorded for session_id, returning
{:ok, span_ctx} on a hit or :error for an unknown session.
@spec fetch_open_span_ctx(atom(), String.t(), non_neg_integer()) :: {:ok, OpenTelemetry.span_ctx()} | :error
Fetches the span context of the open macrostep span session_id
recorded macrostep on, or :error when there is none - an unknown
session, a macrostep whose span has already closed, or an open span
that has not yet seen an event carrying the counter.
This is a read, and deliberately only a read: it returns a stored
context, never attaches one, so ADR-0003 decision 8's rule that the
bridge neither inherits nor clobbers a process's ambient context is
untouched. OpentelemetryStatifier.SpanContext.lookup/2 is the public
face of it.
Under st-ADR-0039 re-entry a session can hold two macrostep spans open at once; they carry different counters in the ordinary case, and the innermost wins if they do not.
Fetches the process recorded for session_id by put_session_pid/3,
or :error for a session the bridge has not seen. The sibling
handlers use it to check that a session's open macrostep span belongs
to this process before landing a span event on it - a delivery-seam
event on an Oban worker must never write onto a span another process
has open for the same scope.
Creates (or returns, if it already exists under this process) a
:public/:named_table ETS table under name. Tests call this
directly with a unique name per test so they never depend on the
application-started table.
@spec put_invoke_parent(atom(), String.t(), OpenTelemetry.span_ctx()) :: :ok
Records the parent macrostep span context a child session's
:initialize macrostep will link to, keyed by the child's session_id.
@spec put_last_span_ctx(atom(), String.t(), OpenTelemetry.span_ctx()) :: :ok
Records span_ctx as the last span context observed for session_id.
@spec put_open_span(atom(), reference(), OpentelemetryStatifier.SpanEntry.t()) :: :ok
Stores an open span's %SpanEntry{} under its span_ref.
@spec put_parent_span( atom(), reference(), pid(), OpentelemetryStatifier.ParentEntry.t() ) :: :ok
Stores a host-declared enclosing span's %ParentEntry{} under ref,
keyed for lookup by the process whose spans nest under it.
Records pid as the session process behind session_id, for the
sweep's liveness check. Idempotent - the table is a :set, so a
session's row is written once per macrostep at no accumulating cost.
@spec put_sibling_span( atom(), reference(), pid(), OpentelemetryStatifier.SiblingEntry.t() ) :: :ok
Stores an open sibling span's %SiblingEntry{} under its span_ref,
keyed for lookup by the process that opened it.
@spec sweep(atom()) :: :ok
Ends the orphans a brutal kill leaves behind: for every :session_pid
row whose process is no longer alive, delegates to delete_session/3,
and for every :sibling_span row whose process is no longer alive,
ends that span with an error status and deletes the row. A live
process's rows are never touched - an open span on a live process is
just a step or a macrostep in flight, whatever its age.
A :parent_span row is swept when either its registrant or the process
it nests spans for is gone, and its span is never ended: the host
declared a span it owns, and ending someone else's span - possibly in
the middle of the work it covers - would be worse than leaking a
three-tuple until the next sweep.
@spec take_invoke_parent(atom(), String.t()) :: {:ok, OpenTelemetry.span_ctx()} | :error
Looks up and removes the invoke-parent span context stored for
session_id, returning {:ok, span_ctx} on a hit or :error when no
parent was recorded - a session that was not invoked by anyone.
@spec take_open_span(atom(), reference()) :: {:ok, OpentelemetryStatifier.SpanEntry.t()} | :error
Looks up and removes the open span under span_ref, returning
{:ok, entry} on a hit or :error on a miss - a :stop event with no
matching :start is a contract-legal shape, not a bug, so this never
raises.
@spec take_sibling_span(atom(), reference()) :: {:ok, OpentelemetryStatifier.SiblingEntry.t()} | :error
Looks up and removes the open sibling span under span_ref. Returns
:error on a miss, which - as for take_open_span/2 - is a
contract-legal shape rather than a bug.