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_startevent/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 functionrun/1uses to detect the real:compensation_failedcatastrophic standing (revert_all/1's tagged{:error, %{paths:, restored:, failed:}}result). A real match increments the:compensation_failedcounter.ReconcileReactor.find_step_error/2, given a predicate matching{:compile_failed, _output}-- the exact real reasonReconcileReactor.standing_for_failure/2maps to the:build_brokenstanding specifically (a:verifystep failure becausemix compileitself failed, as opposed to any other post-:actuatefailure, which maps to the more generic:compensated). A real match increments the:build_brokencounter.
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
@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.
@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
%{}.