An opt-in, level-triggered reconcile loop over a single subsystem.
This is the long-lived control loop that turns Linx's single-shot
reconcile mechanism into continuous convergence: it re-observes and
re-diffs on a timer (the correctness mechanism — manual drift, crashes, and
reboots are corrected on the next pass), and reacts within milliseconds to a
subsystem Monitor when one is available (a latency layer over the timer).
Events are hints; resync is truth.
It is deliberately thin and genuinely opt-in — a primitives library that refuses to become a runtime offers this as a separable convenience, never a default:
- Zero footprint if absent. Nothing here runs unless you start it. There
is no
Applicationboot side effect and no auto-supervised process; thereconcileandMonitorprimitives work fully standalone. - Your tree, not ours. You add
{Linx.Reconcile, opts}to your own supervision tree. Linx ships no supervisor that bundles it in. - No hidden global state. It holds only the desired state and
last_appliedyou give it; there is no singleton or registry. - Single-subsystem by construction. It drives one
Linx.Reconcile.Sourcein one namespace. The cross-subsystem composite ("this container, with this network, in this cgroup") is the consumer's — it is not, and cannot be, expressed here. - Separable. It is implemented entirely on the public
Sourcecontract (single-shotreconcile+ an optional Monitorsubscribe). Deleting it would cost a consumer only a ~15-line timer loop, nothing load-bearing.
It is especially handy for the simplest consumer — a plain host-network or sysctl config app with no supervision tree of its own to roll a loop from — for which it may be the recommended easy path, while still never automatic.
Usage
Add it to your own supervision tree, naming the Linx.Reconcile.Source
adapter for the subsystem, the scope (the namespace, in that subsystem's
shape), and the desired state:
children = [
{Linx.Reconcile,
source: Linx.Sysctl.Reconcile.Source,
scope: :self,
desired: %{"net.ipv4.ip_forward" => 1},
name: :host_sysctls}
]
Supervisor.start_link(children, strategy: :one_for_one)or for rtnl, monitoring a network namespace by pid:
{Linx.Reconcile,
source: Linx.Netlink.Rtnl.Reconcile.Source,
scope: {:pid, container_pid},
desired: %{addresses: [{"eth0", "10.0.0.2", 24}], routes: [{:default, "10.0.0.1"}]},
interval: :timer.seconds(30)}Options
:source(required) — a module implementingLinx.Reconcile.Source.:scope(required) — the namespace handle, in the source's shape.:desired(required) — the desired state, in the source's shape.:reconcile_opts— keyword forwarded verbatim to the source'sreconcile/4(e.g. rtnl's:protocol, sysctl's:revert_on_release).:last_applied— initial ownership state (default%{}).:interval— timer-resync period in ms, or:infinityto disable the timer and rely solely on the Monitor. Default5000.:monitor— whether to subscribe to the source's Monitor for low-latency wakeups (defaulttrue; a source that returns:unsupportedfalls back to timer-only transparently).:debounce— ms to coalesce a burst of Monitor hints (or aput_desired) into one pass. Default100.:owner— a pid to notify after each pass with{:linx_reconcile, :report, report}/{:linx_reconcile, :error, reason}(default: no notifications).:name— aGenServername to register under.
Failure model
When monitor: true, the loop links to the Monitor it starts: if the
Monitor dies the loop dies, and a supervisor restart resubscribes and does a
full resync from scratch (which is correct — resync is truth). A reconcile
pass that returns {:error, _} does not crash the loop; it keeps the previous
last_applied and retries on the next tick.
Summary
Functions
Returns the most recent reconcile report, or nil if no pass has completed yet.
Replaces the desired state and schedules a (debounced) reconcile to converge
on it. Returns :ok immediately.
Forces one reconcile pass now, synchronously, and returns its result
({:ok, report} or {:error, reason}). Useful in tests and for a consumer
that wants to converge on demand without waiting for the next tick.
Starts the loop. See the moduledoc for options.
Stops the loop (and, via the link, its Monitor).
Types
@type option() :: {:source, module()} | {:scope, Linx.Reconcile.Source.scope()} | {:desired, Linx.Reconcile.Source.desired()} | {:reconcile_opts, keyword()} | {:last_applied, Linx.Reconcile.Source.last_applied()} | {:interval, pos_integer() | :infinity} | {:monitor, boolean()} | {:debounce, non_neg_integer()} | {:owner, pid()} | {:name, GenServer.name()}
Functions
@spec last_report(GenServer.server()) :: Linx.Reconcile.Source.report() | nil
Returns the most recent reconcile report, or nil if no pass has completed yet.
@spec put_desired(GenServer.server(), Linx.Reconcile.Source.desired()) :: :ok
Replaces the desired state and schedules a (debounced) reconcile to converge
on it. Returns :ok immediately.
@spec reconcile(GenServer.server(), timeout()) :: {:ok, Linx.Reconcile.Source.report()} | {:error, term()}
Forces one reconcile pass now, synchronously, and returns its result
({:ok, report} or {:error, reason}). Useful in tests and for a consumer
that wants to converge on demand without waiting for the next tick.
@spec start_link([option()]) :: GenServer.on_start()
Starts the loop. See the moduledoc for options.
@spec stop(GenServer.server()) :: :ok
Stops the loop (and, via the link, its Monitor).