reconciler

package
v0.6.0-rc.1 Latest Latest
Warning

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

Go to latest
Published: Oct 17, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

README

pkg/controller/reconciler

Entry point to the reconciliation pipeline. Two components:

  • Reconciler — fires immediately on every watched-resource / HTTP-resource change and on whole-index events. No reconciler-level debounce. Publishes ReconciliationTriggeredEvent.
  • Coordinator — leader-only adapter that consumes ReconciliationTriggeredEvent and drives the synchronous render/validate pipeline via a PipelineExecutor.

Reconciler

import (
    "context"

    "github.com/haproxy-haptic/HAPTIC/pkg/controller/reconciler"
)

// No configuration — the reconciler fires immediately on every event.
r := reconciler.New(bus, logger)

go r.Start(ctx)
Triggering Rules
Incoming event Behaviour
ResourceIndexUpdatedEvent (real change) Immediate — fire a reconciliation now
ResourceIndexUpdatedEvent (initial sync) Ignored — the initial bulk load is covered by IndexSynchronizedEvent
HTTPResourceUpdatedEvent Immediate
IndexSynchronizedEvent Immediate — first reconciliation always runs with a complete store
HTTPResourceAcceptedEvent Immediate — content is only promoted from pending to accepted after validation
DriftPreventionTriggeredEvent Immediate — periodic redeploy path
BecameLeaderEvent Immediate — bootstraps the new leader's pipeline so the leader coordinator produces fresh TemplateRenderedEvent instead of relying on a stale replay

The Reconciler emits a trigger without adding a timer. Watchers debounce resource changes (100 ms by default); the bundled EndpointSlice watches disable that delay. The coordinator coalesces queued triggers while rendering, and the deployer paces reloads with minDeploymentInterval. Runtime-eligible updates bypass reload pacing.

The initial-sync filter exists because ResourceIndexUpdatedEvent fires for every object as stores hydrate. Early reconciliations there would run against an incomplete store, so IndexSynchronizedEvent (which fires once every watcher finishes its initial list) is the correct first-reconciliation trigger.

Coordinator

coord := reconciler.NewCoordinator(&reconciler.CoordinatorConfig{
    EventBus:      bus,
    Pipeline:      pipeline,       // implements PipelineExecutor
    StoreProvider: storeProvider,  // pkg/stores.StoreProvider
    Logger:        logger,
})
go coord.Start(ctx)

Leader-only adapter around pkg/controller/pipeline:

  1. Subscribe in Start (leader-only subscription pattern — not in the constructor, because followers must not subscribe).
  2. On ReconciliationTriggeredEvent, publish ReconciliationStartedEvent.
  3. Call Pipeline.Execute(ctx, storeProvider, rendercontext.RenderModeReconcile) to render, validate new HTTP inputs, and check auxiliary files before publication.
  4. Publish the results: TemplateRenderedEvent on success, ReconciliationFailedEvent (with a PipelineError carrying the failing phase via errors.As) on error. Either path ends with ReconciliationCompletedEvent so the metrics adapter can close its histogram observation. HAProxy's verdict on the render follows asynchronously as RenderGateCompletedEvent, which the Coordinator consumes to settle the term's auxiliary baseline.

The pipeline is called directly, not through another event hop. That's deliberate: from the controller's perspective a reconciliation is one atomic stage, so making it a function call keeps error propagation straightforward and avoids inter-stage synchronization events.

See Also

  • pkg/controller/pipeline — Pipeline.Execute implementation driven by this coordinator
  • pkg/controller/renderer / validator — pure stages composed into the pipeline
  • pkg/controller/deployer — downstream consumer of TemplateRenderedEvent + RenderGateCompletedEvent
  • pkg/controller/deployer — the DriftPreventionMonitor here is the actual DriftPreventionTriggeredEvent source (see drift_monitor.go); pkg/controller/timers is just a SafeTimer wrapper, not an event publisher
  • pkg/controller/reconciler/CLAUDE.md — developer context (immediate-triggering design, leadership-transition patterns)

License

Apache-2.0 — see root LICENSE.

Documentation

Overview

Package reconciler implements the Reconciler component that triggers reconciliation events on resource changes.

The Reconciler is a Stage 5 controller component. It subscribes to resource change events and publishes a ReconciliationTriggeredEvent IMMEDIATELY for each one — there is no reconciler-level refractory/debounce. Batching of rapid changes happens upstream, per watched-resource kind (the pkg/k8s/watcher leading-edge debouncer; default 100ms, EndpointSlice "0"), and reload throttling happens downstream (the deployer's minDeploymentInterval, which the runtime-eligible fast path bypasses). Keeping the reconciler immediate means a runtime-eligible endpoint change reaches the deployer with no latency added by this layer.

Index

Constants

View Source
const (
	// CoordinatorComponentName is the unique identifier for the ReconciliationCoordinator.
	CoordinatorComponentName = "reconciliation-coordinator"

	// CoordinatorEventBufferSize bounds transport to the independently drained mailbox.
	CoordinatorEventBufferSize = busevents.ResourceChurnSubscriberBuffer
)
View Source
const ComponentName = "reconciler"

ComponentName is the unique identifier for this component.

EventBufferSize is the size of the event subscription buffer.

One resource-index update per watcher per debounce window, which a bulk apply or a namespace teardown delivers in the hundreds. The handler only logs and publishes a trigger, so it is not slow — the burst is simply wider than 100, and this subscriber is critical, so one drop ends the controller iteration and the fleet loses its routing until it rebuilds (measured in e2e). Same input volume as the Coordinator downstream, which has been sized for the burst all along.

Variables

This section is empty.

Functions

This section is empty.

Types

type Coordinator

type Coordinator struct {
	*component.ReadySignal
	// contains filtered or unexported fields
}

Coordinator orchestrates reconciliation by calling the Pipeline directly.

Render and validate run synchronously inside Pipeline.Execute() (ADR-0001 — no event hop), and the Coordinator publishes the appropriate events for downstream consumers based on the result.

Flow:

  1. ReconciliationTriggeredEvent received
  2. Publish ReconciliationStartedEvent
  3. Call Pipeline.Execute() (renders and validates)
  4. If success: Publish TemplateRenderedEvent
  5. If failure: Publish ReconciliationFailedEvent

The DeploymentScheduler still operates event-driven, receiving TemplateRenderedEvent and the render gate's RenderGateCompletedEvent to schedule deployments.

func NewCoordinator

func NewCoordinator(cfg *CoordinatorConfig) *Coordinator

NewCoordinator creates a new ReconciliationCoordinator.

Note: eventChan is NOT subscribed here - subscription happens in Start(). This is a leader-only component that subscribes when Start() is called (after leadership is acquired). All-replica components replay their state on BecameLeaderEvent to ensure leader-only components receive current state.

Parameters:

  • cfg: Configuration for the coordinator

Returns:

  • A new Coordinator instance ready to be started

func (*Coordinator) Name

func (c *Coordinator) Name() string

Name returns the unique identifier for this component.

func (*Coordinator) Start

func (c *Coordinator) Start(ctx context.Context) error

Start begins the coordinator's event loop.

This method blocks until the context is cancelled.

NOTE: deliberately NOT converted to embed component.Base. Base subscribes at construction, but the coordinator is a leader-only component whose input (ReconciliationTriggeredEvent) is published by the all-replica Reconciler on EVERY replica — a constructor-time subscription would have follower replicas fill the buffer and log critical drops continuously. Subscribing here, on leadership, keeps followers unsubscribed entirely.

type CoordinatorConfig

type CoordinatorConfig struct {
	// EventBus is the event bus for subscribing to events and publishing results.
	EventBus *busevents.EventBus

	// Pipeline is the render-validate pipeline to execute.
	// Must implement PipelineExecutor interface.
	Pipeline PipelineExecutor

	// StoreProvider provides access to resource stores.
	StoreProvider stores.StoreProvider

	// CurrentFiles owns the last accepted auxiliary output for each leader term.
	CurrentFiles CurrentFilesAuthority

	// HTTPAcceptance is the HTTP store's reversible content acceptance; nil
	// without an HTTP store.
	HTTPAcceptance HTTPContentAcceptance

	// GraphWarm reports whether the render service holds a committed graph;
	// it labels failed renders. Nil counts them as warm.
	GraphWarm func() bool

	// Metrics receives one render count per reconcile; optional.
	Metrics *metrics.Metrics

	// Logger is the structured logger.
	Logger *slog.Logger
}

CoordinatorConfig contains configuration for creating a Coordinator.

type CurrentFilesAuthority

type CurrentFilesAuthority interface {
	BeginTerm() uint64
	EndTerm(generation uint64)
	Snapshot(generation uint64) (map[string]string, error)
	AcceptOutput(generation uint64, output *renderoutput.Snapshot) error
	ConfirmOutput(generation uint64, output *renderoutput.Snapshot) error
	RollbackOutput(generation uint64, output *renderoutput.Snapshot) error
}

CurrentFilesAuthority binds exact render outputs to a leader term.

type HTTPContentAcceptance

type HTTPContentAcceptance interface {
	AcceptanceSequence() uint64
	ConfirmAcceptances(observations []httpstore.ObservationToken)
	RevokeAcceptances(sequence uint64) int
}

HTTPContentAcceptance is the HTTP store's reversible acceptance (ADR-0030).

type PipelineExecutor

type PipelineExecutor interface {
	Execute(ctx context.Context, provider stores.StoreProvider, mode rendercontext.RenderMode, extraOpts ...rendercontext.Option) (*pipeline.PipelineResult, error)
}

PipelineExecutor defines the interface for executing the render-validate pipeline. This allows mocking in tests.

type Reconciler

type Reconciler struct {
	*component.Base
	// contains filtered or unexported fields
}

Reconciler triggers reconciliation immediately on every resource/HTTP change.

There is NO reconciler-level debounce. Rapid changes are batched upstream by the per-watcher debouncer (pkg/k8s/watcher), and reload-inducing structural deploys are throttled downstream by the deployer's minDeploymentInterval (which the runtime-eligible fast path skips). Firing immediately here ensures runtime-eligible endpoint changes (e.g. EndpointSlice watchers with debounceInterval: "0") propagate with no added latency. All trigger paths fire immediately:

  • ResourceIndexUpdatedEvent → "resource_change"
  • HTTPResourceUpdatedEvent → "http_resource_change"
  • IndexSynchronizedEvent → "index_synchronized" (initial reconciliation)
  • HTTPResourceAcceptedEvent / DriftPreventionTriggeredEvent / BecameLeaderEvent → immediate command triggers

The component publishes ReconciliationTriggeredEvent to signal the Coordinator to begin a reconciliation cycle. Coalescible state-update triggers (resource_change, http_resource_change) may be superseded downstream by a newer trigger; command triggers are always processed.

func New

func New(eventBus *busevents.EventBus, logger *slog.Logger) *Reconciler

New creates a new Reconciler component.

Parameters:

  • eventBus: The EventBus for subscribing to events and publishing triggers
  • logger: Structured logger for component logging

Returns:

  • A new Reconciler instance ready to be started

func (*Reconciler) HandleEvent

func (r *Reconciler) HandleEvent(event busevents.Event)

HandleEvent implements component.EventHandler: it dispatches events to their handlers, tracking processing time for the health check. Each handler triggers reconciliation immediately — there is no reconciler-level debounce.

func (*Reconciler) HealthCheck

func (r *Reconciler) HealthCheck() error

HealthCheck implements the lifecycle.HealthChecker interface. Returns an error if the component appears to be stalled (processing for > timeout). Returns nil when idle (not processing) - idle is always healthy for event-driven components.

Jump to

Keyboard shortcuts

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