OpentelemetryStatifier.SpanTable (OpentelemetryStatifier v0.1.0)

Copy Markdown View Source

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

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

delete_session(table, session_id, orphan_message)

@spec delete_session(atom(), String.t(), String.t()) :: :ok

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.

fetch_innermost_open_span(table, session_id)

@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).

fetch_last_span_ctx(table, session_id)

@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.

new_table(name)

@spec new_table(atom()) :: atom()

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.

put_invoke_parent(table, session_id, span_ctx)

@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.

put_last_span_ctx(table, session_id, span_ctx)

@spec put_last_span_ctx(atom(), String.t(), OpenTelemetry.span_ctx()) :: :ok

Records span_ctx as the last span context observed for session_id.

put_open_span(table, span_ref, entry)

@spec put_open_span(atom(), reference(), OpentelemetryStatifier.SpanEntry.t()) :: :ok

Stores an open span's %SpanEntry{} under its span_ref.

put_session_pid(table, session_id, pid)

@spec put_session_pid(atom(), String.t(), pid()) :: :ok

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.

sweep(table)

@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.

take_invoke_parent(table, session_id)

@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.

take_open_span(table, span_ref)

@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.