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}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.
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 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.
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 last span context recorded for session_id, returning
{:ok, span_ctx} on a hit or :error for an unknown session.
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.
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.
Ends the orphans a brutal kill leaves behind: for every :session_pid
row whose process is no longer alive, delegates to delete_session/3.
A live session's rows are never touched - an open span on a live
session is just 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.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
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_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_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.
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.
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 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.
A live session's rows are never touched - an open span on a live
session is just a macrostep in flight, whatever its age.
@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.