Behaviour for run lifecycle event recorders.
A recorder is a single-writer process spawned per run, before the
CouncilEx.RunServer starts, that subscribes to the run's
PubSub topic and reacts to lifecycle events with side effects:
- persist to a database (e.g.
CouncilEx.Recorder.Ecto) - ship to Kafka / NATS / S3
- emit OpenTelemetry spans
- append to an offline eval harness
Lifecycle
The runtime guarantees:
init/2is called with the assignedrun_idand userargs. The recorder process is fully subscribed to the run topic beforeinit/2returns, so no:run_startedevent is ever missed.handle_event/2is called for each PubSub event broadcast by theRunServer(seeCouncilEx.Eventsfor the catalog and payload shapes), in broadcast order.handle_finalize/2is called exactly once with the terminal outcome:{:ok, %CouncilEx.Result{}}on:run_completed:cancelledwhen the run was terminated viaCouncilEx.cancel/2(or:terminate_run/2){:error, term()}for any other failure path Afterhandle_finalize/2returns, the recorder process exits normally.
Recorders are spawned unsupervised (GenServer.start/3 semantics).
A recorder crash does not kill the run — the run continues without a
recorder. If the RunServer crashes without broadcasting a terminal
event, the recorder is notified via Process.monitor/1 and
handle_finalize/2 is invoked with
{:error, {:runserver_crashed, reason}}.
Wiring
CouncilEx.start(council, input,
recorder: {MyApp.EctoRecorder, %{repo: MyApp.Repo}}
)See also CouncilEx.Recorder.Ecto for the default Ecto-backed
implementation.
Summary
Callbacks
Handle a single run lifecycle event.
Handle the terminal outcome.
Initialise recorder state.
Types
@type outcome() :: {:ok, CouncilEx.Result.t()} | {:error, term()} | :cancelled
Terminal outcome passed to handle_finalize/2.
@type state() :: term()
Callbacks
Handle a single run lifecycle event.
event is the raw tuple as broadcast by the RunServer — see
CouncilEx.Events for the catalog. Implementations may pattern
match on event tags (:run_started, :round_completed,
:member_completed, etc.) and persist a subset.
Must return {:ok, state}. The recorder is unsupervised; a crash
exits the recorder process without affecting the run.
Handle the terminal outcome.
Called exactly once after the run reaches a terminal state. The recorder process exits after this returns.
Initialise recorder state.
Called once, in the recorder process, before it begins consuming
events. Receives the run_id chosen by the runtime (or supplied via
the :run_id option to CouncilEx.start/3) and the user-supplied
args from the {module, args} tuple passed in :recorder.
Return {:ok, state} to begin consuming events. Return {:stop, reason} to abort recorder startup; the run will still proceed
without a recorder.