Linx.Reconcile (Linx v0.1.0)

Copy Markdown View Source

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 Application boot side effect and no auto-supervised process; the reconcile and Monitor primitives 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_applied you give it; there is no singleton or registry.
  • Single-subsystem by construction. It drives one Linx.Reconcile.Source in 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 Source contract (single-shot reconcile + an optional Monitor subscribe). 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 implementing Linx.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's reconcile/4 (e.g. rtnl's :protocol, sysctl's :revert_on_release).
  • :last_applied — initial ownership state (default %{}).
  • :interval — timer-resync period in ms, or :infinity to disable the timer and rely solely on the Monitor. Default 5000.
  • :monitor — whether to subscribe to the source's Monitor for low-latency wakeups (default true; a source that returns :unsupported falls back to timer-only transparently).
  • :debounce — ms to coalesce a burst of Monitor hints (or a put_desired) into one pass. Default 100.
  • :owner — a pid to notify after each pass with {:linx_reconcile, :report, report} / {:linx_reconcile, :error, reason} (default: no notifications).
  • :name — a GenServer name 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

option()

@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

last_report(server)

@spec last_report(GenServer.server()) :: Linx.Reconcile.Source.report() | nil

Returns the most recent reconcile report, or nil if no pass has completed yet.

put_desired(server, desired)

@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.

reconcile(server, timeout \\ 5000)

@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.

start_link(opts)

@spec start_link([option()]) :: GenServer.on_start()

Starts the loop. See the moduledoc for options.

stop(server)

@spec stop(GenServer.server()) :: :ok

Stops the loop (and, via the link, its Monitor).