Linx.Reconcile.Source behaviour (Linx v0.1.0)

Copy Markdown View Source

The plug-in contract that lets the generic Linx.Reconcile loop drive any reconcilable subsystem — deliberately minimal.

Each reconcilable subsystem keeps its own rich, type-bearing surface (Linx.Sysctl.Reconcile, Linx.Netlink.Rtnl.Reconcile, …) for consumers that want direct control. This behaviour is the narrow seam the loop drives, implemented by a small adapter that delegates to that surface — so the loop stays subsystem-agnostic and the per-subsystem APIs stay free of a lowest-common-denominator façade.

Two layers, then:

  • the per-subsystem reconcile/3,4 a consumer calls directly when it wants control, and
  • this uniform contract the Linx.Reconcile loop drives.

The scope

A scope names the namespace the source acts in, in whatever shape that subsystem already accepts. It is an opaque value to the loop, threaded verbatim into every callback:

  • sysctl — the :in target (:self, {:pid, n}, {:path, p});
  • rtnl — a Linx.Netlink.Socket.netns/0 (:host, {:pid, n}, {:path, p}), the source opening a short-lived socket per pass.

The report contract

reconcile/4 returns {:ok, report} on a completed pass (even a partial one — per-op failures live inside the report). The loop relies on two fields being present on that report, by convention shared across every subsystem's report struct:

  • :converged?boolean(), whether the pass left nothing undone;
  • :last_applied — the updated ownership state to thread into the next pass.

Both Linx.Sysctl.Reconcile.Report and Linx.Netlink.Rtnl.Reconcile.Report carry these (alongside :applied/:failed/:pending). {:error, reason} is reserved for a pass that could not run at all (could not observe the kernel, an invalid desired state) — the loop keeps the previous last_applied and retries on the next tick.

Litmus test

This contract is also the go/no-go test for whether the opt-in loop is worth shipping: if subsystems implement these callbacks by clean delegation, the loop is trivially worth it; if atomicity or ownership force contortions, we stop at the single-shot reconcile + Monitor primitives and let consumers wrap them directly. An out-of-tree PoC and the sysctl/rtnl adapters cleared it.

Summary

Types

The desired state, in the subsystem's own desired-state shape.

Reconciler-held ownership, threaded between passes. %{} is empty.

Observed kernel state, in the subsystem's own observed shape.

Subsystem-specific options, forwarded verbatim to the per-pass verb.

A reconcile report. Must expose at least :converged? (boolean) and :last_applied (the next ownership state) — see the moduledoc.

Opaque namespace handle, in the shape the subsystem already accepts.

Callbacks

Observes current kernel state in scope. Optional — the loop does not call it (it drives reconcile/4, which observes internally); it rounds out the contract for consumers and diagnostics.

Runs one reconcile pass in scope against desired, given the prior last_applied. Returns {:ok, report} on a completed (possibly partial) pass, or {:error, reason} if the pass could not run at all.

Starts a Monitor delivering change hints to owner, for the loop's low-latency wakeups. Returns {:ok, monitor_pid}, :unsupported (the loop runs timer-only), or {:error, reason}.

Types

desired()

@type desired() :: term()

The desired state, in the subsystem's own desired-state shape.

last_applied()

@type last_applied() :: term()

Reconciler-held ownership, threaded between passes. %{} is empty.

observed()

@type observed() :: term()

Observed kernel state, in the subsystem's own observed shape.

opts()

@type opts() :: keyword()

Subsystem-specific options, forwarded verbatim to the per-pass verb.

report()

@type report() :: %{
  :converged? => boolean(),
  :last_applied => last_applied(),
  optional(atom()) => term()
}

A reconcile report. Must expose at least :converged? (boolean) and :last_applied (the next ownership state) — see the moduledoc.

scope()

@type scope() :: term()

Opaque namespace handle, in the shape the subsystem already accepts.

Callbacks

observe(scope, opts)

(optional)
@callback observe(scope(), opts()) :: {:ok, observed()} | {:error, term()}

Observes current kernel state in scope. Optional — the loop does not call it (it drives reconcile/4, which observes internally); it rounds out the contract for consumers and diagnostics.

reconcile(scope, desired, last_applied, opts)

@callback reconcile(scope(), desired(), last_applied(), opts()) ::
  {:ok, report()} | {:error, term()}

Runs one reconcile pass in scope against desired, given the prior last_applied. Returns {:ok, report} on a completed (possibly partial) pass, or {:error, reason} if the pass could not run at all.

subscribe(scope, owner)

@callback subscribe(scope(), owner :: pid()) ::
  {:ok, pid()} | {:error, term()} | :unsupported

Starts a Monitor delivering change hints to owner, for the loop's low-latency wakeups. Returns {:ok, monitor_pid}, :unsupported (the loop runs timer-only), or {:error, reason}.

The loop treats every message it then receives as a level-triggered "look now" hint — it never acts on an event payload — so the monitor's exact message shape is its own concern.