reconcile

package
v0.1.3-rc.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 8, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package reconcile is the agent's desired-state execution engine: a small, in-process control loop in the Kubernetes mould (a level-triggered reconciler driven by a deduplicating work queue) but with no cluster, no apiserver, and no etcd. It runs in one binary over the resource store, the event log, and the job queue. The control-theory mechanics are borrowed (they are domain-agnostic and proven); the engine on top, reconciling agent goals toward an LLM-judged stop condition, is the agent's own.

This file is the critical primitive: the work queue. It mirrors the semantics of client-go's controller workqueue (a key is never processed concurrently; adds collapse while a key waits; delayed and rate-limited re-adds avoid hot loops) without importing it, and it schedules delays through a clock.Timing source so every delay is deterministic under a Manual clock.

Index

Constants

View Source
const DefaultResync = 30 * time.Second

DefaultResync is how often the manager re-enqueues every live resource of each registered kind when no interval is configured. Resync is the safety net: even if a change hint is lost (the bus is at-most-once), every resource is reconciled again within this window, so the system always converges.

Variables

This section is empty.

Functions

This section is empty.

Types

type Controller

type Controller[T comparable] struct {
	// contains filtered or unexported fields
}

Controller runs a Reconciler against keys delivered by a Queue. It owns the retry policy: a Transient error backs off and retries, a success forgets the key, an explicit RequeueAfter re-enqueues after a delay, and any other error class (Terminal, NeedsApproval, BudgetExceeded, Cancelled) is not hot-retried - the manager's periodic resync is the safety net that re-enqueues it later.

func NewController

func NewController[T comparable](name string, queue *Queue[T], rec Reconciler[T], opts ...ControllerOption) *Controller[T]

NewController builds a controller pulling from queue and reconciling with rec.

func (*Controller[T]) Queue

func (c *Controller[T]) Queue() *Queue[T]

Queue exposes the controller's queue so a manager can enqueue keys into it.

func (*Controller[T]) Run

func (c *Controller[T]) Run(ctx context.Context)

Run starts the workers and blocks until ctx is cancelled (which shuts the queue down) and every worker has drained. It is the controller's whole lifecycle.

type ControllerOption

type ControllerOption func(*controllerConfig)

ControllerOption configures a Controller.

func WithWorkers

func WithWorkers(n int) ControllerOption

WithWorkers sets the number of concurrent workers (default 1). The queue guarantees one key is never reconciled by two workers at once regardless, so more workers only add cross-key parallelism.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager wires reconcilers to the resource store. It owns one Controller per registered kind, routes change hints to the right controller's queue, and runs the periodic resync that guarantees eventual convergence. It deliberately holds no watch of its own: truth is the store, and the manager only decides WHEN to reconcile, never WHAT the state is.

func NewManager

func NewManager(store resource.Store, opts ...ManagerOption) *Manager

NewManager returns a manager over store. Register kinds before Start.

func (*Manager) Enqueue

func (m *Manager) Enqueue(ref Ref)

Enqueue schedules a reconcile of ref. This is the change-hint entry point: a bus signal, a write callback, or any "this resource may have changed" notice calls it. Hints are advisory (the reconcile re-reads truth), and a hint for an unregistered kind is ignored.

func (*Manager) Register

func (m *Manager) Register(kind string, rec Reconciler[Ref], opts ...ControllerOption)

Register attaches a reconciler for a kind. It must be called before Start. Registering the same kind twice replaces the earlier reconciler.

func (*Manager) Start

func (m *Manager) Start(ctx context.Context)

Start runs every controller and the resync loop, blocking until ctx is cancelled and all of them have drained. Run it in its own goroutine; cancel ctx to stop.

type ManagerOption

type ManagerOption func(*Manager)

ManagerOption configures a Manager.

func WithClock

func WithClock(c clock.Timing) ManagerOption

WithClock sets the time source (default clock.System). A Manual clock makes the resync ticker deterministic in tests.

func WithResync

func WithResync(d time.Duration) ManagerOption

WithResync sets the resync interval (default DefaultResync). A value <= 0 disables periodic resync (only the initial resync and explicit Enqueue drive reconciles), which is useful in tests that want to control every trigger.

func WithoutResync

func WithoutResync() ManagerOption

WithoutResync disables the resync sweep entirely, both the initial pass and the periodic one, so the manager drives only the resources explicitly enqueued (by a completion signal or an Enqueue hint) and never adopts pre-existing resources it finds in the store. A one-shot run uses this so it drives its own submitted work to completion without re-adopting goals left non-terminal by an earlier run; that keeps each run's event stream its own and makes resuming a parked run an explicit act rather than a side effect of starting any run.

type Queue

type Queue[T comparable] struct {
	// contains filtered or unexported fields
}

Queue is a deduplicating, level-triggered work queue keyed by T. A key added any number of times before it is fetched is processed once; while a key is being processed, re-adds are remembered and the key is re-queued exactly once when processing finishes (Done). AddAfter and AddRateLimited delay a re-add without blocking, so a failing item backs off instead of spinning. T is the item's identity (for the reconciler, a resource Ref), so the queue carries no payload: the reconciler always re-reads current state, never trusting a stale enqueued value.

func NewQueue

func NewQueue[T comparable](clk clock.Timing) *Queue[T]

NewQueue returns an empty queue that schedules delays on clk.

func (*Queue[T]) Add

func (q *Queue[T]) Add(item T)

Add enqueues item for processing. It is a no-op if item is already waiting (dedup) or already being processed (it is remembered and re-queued on Done).

func (*Queue[T]) AddAfter

func (q *Queue[T]) AddAfter(item T, d time.Duration)

AddAfter enqueues item once d has elapsed on the queue's clock, without blocking the caller. A d <= 0 adds immediately. Repeated AddAfter for the same item are harmless: each fires an Add, and Add dedups. The timer is created synchronously (registered on the clock before AddAfter returns) so a Manual clock advanced past d fires it deterministically.

Rather than one goroutine per delayed item, all delayed adds are handed to a single waiting loop (started on first use) that holds them in a deadline ordered heap and enqueues each when the clock reaches its deadline. The timer is created here under q.mu so a concurrent ShutDown either observes it (and the loop stops it) or has already set shuttingDown (and this add is dropped). Only the loop ever stops a timer it is waiting on, so there is no cross-goroutine race on the timer channel.

func (*Queue[T]) AddRateLimited

func (q *Queue[T]) AddRateLimited(item T)

AddRateLimited enqueues item after its current backoff delay, growing the delay on each call (per-item exponential backoff) until Forget resets it. This is how a reconcile that errors is retried without a hot loop.

func (*Queue[T]) Done

func (q *Queue[T]) Done(item T)

Done marks item as finished. If item was re-added while it was being processed, it is re-queued now (and only now), preserving the "never concurrent per key" guarantee.

func (*Queue[T]) Forget

func (q *Queue[T]) Forget(item T)

Forget clears item's backoff so its next AddRateLimited starts from baseDelay again. Call it after a successful reconcile.

func (*Queue[T]) Get

func (q *Queue[T]) Get() (item T, shutdown bool)

Get blocks until an item is ready or the queue shuts down. The returned item is marked as processing; the caller MUST call Done(item) when finished so a re-add that arrived during processing is re-queued. shutdown is true only when the queue is draining and empty.

func (*Queue[T]) Len

func (q *Queue[T]) Len() int

Len is the number of items ready to be fetched (excludes in-flight items).

func (*Queue[T]) NumRequeues

func (q *Queue[T]) NumRequeues(item T) int

NumRequeues is how many times item has been rate-limited since the last Forget.

func (*Queue[T]) ShutDown

func (q *Queue[T]) ShutDown()

ShutDown stops the queue: Get returns shutdown once the ready items drain, pending AddAfter timers are abandoned, and the call waits for the waiting loop to exit (if it was ever started). It is idempotent.

type Reconciler

type Reconciler[T comparable] interface {
	Reconcile(ctx context.Context, key T) (Result, error)
}

Reconciler drives one resource of a kind toward its desired state. It is given only the key (identity), never an event or a cached object, which forces it to re-read the current observed state every time: the level-triggered discipline that makes the loop self-healing and crash-resumable. Reconcile must be idempotent and should return promptly; long or stochastic work (an LLM step) is dispatched elsewhere and observed, not run inline.

type ReconcilerFunc

type ReconcilerFunc[T comparable] func(ctx context.Context, key T) (Result, error)

ReconcilerFunc adapts a function to a Reconciler.

func (ReconcilerFunc[T]) Reconcile

func (f ReconcilerFunc[T]) Reconcile(ctx context.Context, key T) (Result, error)

Reconcile calls f.

type Ref

type Ref = resource.Key

Ref identifies the resource a reconcile acts on: kind + scope + name, the logical key of the resource store. It is the only thing a Reconciler is handed, so the reconciler must re-read the live resource itself (level-triggered).

type Result

type Result struct {
	RequeueAfter time.Duration
}

Result tells the controller what to do after a reconcile. The zero Result with a nil error means "success, nothing more to do": the key settles and is not touched again until it changes or the next resync. A positive RequeueAfter asks to be reconciled again after that delay (the idempotent way to poll long-running work), independent of backoff.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL