GgenIgniter.Reactors.CompensationTelemetryMiddleware (ggen_igniter v26.9.8)

Copy Markdown View Source

A real Reactor.Middleware (behaviour confirmed by reading deps/reactor/lib/reactor/middleware.ex directly, not guessed) that counts compensation/undo lifecycle events for GgenIgniter.Reactors.ReconcileReactor runs into a real, named, public ETS table (@table, :ggen_igniter_compensation_counters) -- Chicago-style: real ETS state, read back via counters/0, never an interaction-based "was event/3 called" assertion.

Wired alongside Reactor.Middleware.Telemetry in ReconcileReactor's own middlewares do ... end block (added in commit 2d758db for the :telemetry.execute/3 wiring) -- see that module's moduledoc "Telemetry" section. Both middlewares receive the exact same real Reactor lifecycle calls; this one turns three of them into durable, cross-run ETS counts instead of ephemeral :telemetry events, so a caller (or a test) can ask "how many times has this reactor actually compensated/undone something" without attaching a :telemetry handler.

Real event/error shapes matched (cited from Reactor.Middleware.step_event()

and the error/2 callback in deps/reactor/lib/reactor/middleware.ex, never invented)

Reactor.Middleware.step_event()'s real union includes (among others):

| {:compensate_start, any}
| {:compensate_error, error_or_errors}
| :undo_start

event/3 below matches exactly these three real shapes (note {:compensate_start, any} is a real 2-element tuple -- {tag, reason} -- not a 3-element one; this module matches the type as actually defined in middleware.ex, not a guessed arity) and increments one real ETS counter per shape: :compensate_start, :compensate_error, :undo_start.

error/2's real signature is error(error_or_errors, context) :: :ok | {:error, any}, where error_or_errors is the SAME real Reactor.Error-class term ReconcileReactor.run/1's own case Reactor.run(...) do {:error, error} -> ... branch already knows how to read (confirmed by tracing deps/reactor/lib/reactor/executor.ex's handle_undo/3 -- Reactor.Error.to_class(state.errors) -- into Executor.Hooks.error/3, which is exactly what invokes every configured middleware's error/2). Rather than re-deriving a second, parallel search over that nested Splode error shape, this module reuses ReconcileReactor's OWN real, public (@doc false, not defp) helpers:

  • ReconcileReactor.find_compensation_failure/1 -- the exact function run/1 uses to detect the real :compensation_failed catastrophic standing (revert_all/1's tagged {:error, %{paths:, restored:, failed:}} result). A real match increments the :compensation_failed counter.
  • ReconcileReactor.find_step_error/2, given a predicate matching {:compile_failed, _output} -- the exact real reason ReconcileReactor.standing_for_failure/2 maps to the :build_broken standing specifically (a :verify step failure because mix compile itself failed, as opposed to any other post-:actuate failure, which maps to the more generic :compensated). A real match increments the :build_broken counter.

These are the two real standing atoms standing_for_failure/2/describe_compensation_failure/1 produce that this module's task explicitly asks to count -- reusing the SAME functions run/1 itself uses to derive them, rather than re-implementing a second, possibly-diverging classification.

Per-run scoping (run_id, added post-v26.9.1-gap-#7)

The ETS table's real key shape is {run_id, counter_atom}, not a bare counter_atom -- a bare-atom key would make counters/0 answer "how many times ever, across every run since BEAM boot" instead of "how many times THIS run", which is genuinely useless for a caller that wants to know whether the run it just made produced a compensation event.

run_id is generated fresh in init/1 (via System.unique_integer/1 + self() of the calling process -- the real, honest signal available at that point) and stored into the real Reactor.context() map this middleware returns from init/1. This is not a guess about Reactor's own internals: tracing deps/reactor/lib/reactor/executor.ex's run/4 (calls Executor.Hooks.init/2, then does execute(%{reactor | context: context}, state) with THAT returned context) and deps/reactor/lib/reactor/executor/hooks.ex's error/3 (called with reactor.context, the same context threaded from init/1 through the whole run) confirms the context map init/1 returns is the exact same map event/3 and error/2 receive for the rest of that one real run -- no Reactor-provided reactor_id/run-identifier exists in Reactor.context() (confirmed absent from deps/reactor/lib/reactor.ex's @type context :: %{optional(atom) => any}), so this module mints its own rather than inventing a fictitious one Reactor doesn't actually provide.

counters/1 reads back just one run's counts via this real per-run key. counters/0 is kept for backward compatibility and now explicitly documented as a cross-run aggregate (folds every {_run_id, counter} key currently in the table into one map) -- not a per-run answer.

Summary

Functions

Cross-run aggregate: real, current contents of the @table ETS table summed across EVERY run_id currently present -- "how many times ever, across every run since BEAM boot", not "how many times THIS run" (use counters/1 with a specific run_id for that). Kept for backward compatibility with callers that only need a global sanity count.

Real, current contents of the @table ETS table for ONE run, as a plain map (e.g. %{compensate_start: 2, undo_start: 1}) -- what a test asserts on directly, per this module's own moduledoc (Chicago-style: real state, not "was event/3 called").

Functions

counters()

@spec counters() :: %{optional(atom()) => non_neg_integer()}

Cross-run aggregate: real, current contents of the @table ETS table summed across EVERY run_id currently present -- "how many times ever, across every run since BEAM boot", not "how many times THIS run" (use counters/1 with a specific run_id for that). Kept for backward compatibility with callers that only need a global sanity count.

counters(run_id)

@spec counters(term()) :: %{optional(atom()) => non_neg_integer()}

Real, current contents of the @table ETS table for ONE run, as a plain map (e.g. %{compensate_start: 2, undo_start: 1}) -- what a test asserts on directly, per this module's own moduledoc (Chicago-style: real state, not "was event/3 called").

run_id is the value stored under context[:compensation_telemetry_run_id] by init/1 for that run -- a test/caller reads it back from the same context map it passed into Reactor.run/4 (or, for ReconcileReactor, from the run's own returned context if exposed) after the run completes.

Never raises for an empty/not-yet-created table -- ensure_table!/0 is called first, so a fresh/unknown run_id with zero counters comes back as %{}.