A persistent, BEAM-native reconciliation controller: a real GenServer
that holds live reconciliation state IN THIS PROCESS across multiple
reconcile/2 calls -- in contrast to mix ggen_igniter.sync's
per-invocation model, where every run is a fresh OS process with zero
in-memory continuity to the previous run (any "what did the last run do"
question can only be answered by re-reading disk, because there is no
process to ask).
Wraps GgenIgniter.Reconcile.run/1 (the one real reconciliation pipeline
implementation, shared with Mix.Tasks.GgenIgniter.Sync) -- this module
adds no new reconciliation LOGIC of its own, only in-process state plus a
small public API around that one shared pipeline call.
State shape
%{
last_reconciliations: %{
pack_key => %{
ontology_path: String.t(),
manifest: %{out_path :: String.t() => outcome :: atom()},
reconciliation_count: pos_integer(),
last_run_at: DateTime.t(),
receipt: map()
}
}
}reconciliation_count is real, process-only knowledge: it is NOT
recoverable by reading the output file alone (the file's content is
byte-identical after reconciliation #1 and #2 once nothing has changed --
that is the whole point of Actuate.write_file!/3's idempotency guard),
yet this controller can still answer "how many times has this key actually
been reconciled, in this process" without touching disk. That is the
concrete, testable economic claim this architecture makes over the CLI's
model (see test/ggen_igniter_controller_test.exs).
Fault isolation
A real failure inside GgenIgniter.Reconcile.run/1 (bad ontology path,
missing template, engine error, ...) is caught in handle_call/3 and
turned into a clean {:error, reason} reply -- it does NOT crash this
GenServer, and it does NOT touch any other pack_key's already-stored good
record. This is a real, tested guarantee, not an assumption.
Scope
This is a bounded, real proof-of-concept slice: no Reactor integration, no distributed/multi-node topology, no crash-SUPERVISION-policy design (this module is about fault ISOLATION between keys within one already-alive GenServer -- a different concern from a Supervisor's restart strategy for the GenServer itself, which this pass does not attempt to design). Legitimate future directions, out of scope here.
Wiring
Started as an OPT-IN child of GgenIgniter.Application's supervision
tree, gated behind Application.get_env(:ggen_igniter, :start_controller, false) (default false) -- a consuming application that wants it sets
config :ggen_igniter, start_controller: true explicitly. In a test (or
any other caller that wants its own instance), call start_link/1
directly instead of relying on the application's supervision tree.
Summary
Functions
Returns a specification to start this module under a supervisor.
Runs the real reconciliation pipeline (GgenIgniter.Reconcile.run/1) for
pack_key, using reconcile_opts (the same option shape
Mix.Tasks.GgenIgniter.Sync accepts, within GgenIgniter.Reconcile.run/1's
own bounded scope -- see that module's moduledoc: :ontology/:query/
:template/:pack/:pack_dir/:engine/:mode/:out/:unless_exists/
:skip_if/:dry_run).
Starts the controller GenServer. opts are plain
GenServer.start_link/3 options (:name, etc.) -- pass name: GgenIgniter.Controller to start the singleton the application's
supervision tree expects; omit :name (as tests do) to start an
independent, unnamed instance addressed by its returned pid.
Returns the real, current in-memory record for pack_key -- {:ok, record()} -- or :never_reconciled if this server has never
successfully reconciled that key. A pure in-process state read: this call
never touches disk.
Types
@type pack_key() :: term()
@type record() :: %{ ontology_path: String.t(), manifest: %{optional(String.t()) => atom()}, reconciliation_count: pos_integer(), last_run_at: DateTime.t(), receipt: map() }
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec reconcile(GenServer.server(), pack_key(), keyword()) :: {:ok, record()} | {:error, term()}
Runs the real reconciliation pipeline (GgenIgniter.Reconcile.run/1) for
pack_key, using reconcile_opts (the same option shape
Mix.Tasks.GgenIgniter.Sync accepts, within GgenIgniter.Reconcile.run/1's
own bounded scope -- see that module's moduledoc: :ontology/:query/
:template/:pack/:pack_dir/:engine/:mode/:out/:unless_exists/
:skip_if/:dry_run).
On success, updates this server's in-process state for pack_key and
returns {:ok, record()}. On a real pipeline failure, returns
{:error, reason} -- this server's state for pack_key (and every other
key) is left completely unchanged, and the server itself does not crash.
@spec start_link(keyword()) :: GenServer.on_start()
Starts the controller GenServer. opts are plain
GenServer.start_link/3 options (:name, etc.) -- pass name: GgenIgniter.Controller to start the singleton the application's
supervision tree expects; omit :name (as tests do) to start an
independent, unnamed instance addressed by its returned pid.
@spec status(GenServer.server(), pack_key()) :: {:ok, record()} | :never_reconciled
Returns the real, current in-memory record for pack_key -- {:ok, record()} -- or :never_reconciled if this server has never
successfully reconciled that key. A pure in-process state read: this call
never touches disk.