reconciler

package
v0.2.0-alpha.1 Latest Latest
Warning

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

Go to latest
Published: Jul 5, 2026 License: Apache-2.0 Imports: 13 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"

    "gitlab.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-only) renderer produces fresh TemplateRenderedEvent instead of relying on a stale replay

The Reconciler adds zero latency: every event it handles fires a reconciliation immediately. Coalescing of rapid changes is the per-watcher debounce window's job (default 2s, pkg/k8s/types.DefaultDebounceInterval; EndpointSlice watchers use debounceInterval: "0" so pod-IP rotations react instantly during rolling restarts). Reload throttling is the deployer's minDeploymentInterval (bypassed by the runtime-eligible fast path). This split keeps single ingress flips and rolling-restart endpoint rotations both fast without a reconciler-level refractory.

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) synchronously — render + validate + build render context in one atomic step.
  4. Publish the results: TemplateRenderedEvent + ValidationCompletedEvent 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.

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/pipelinePipeline.Execute implementation driven by this coordinator
  • pkg/controller/renderer / validator — pure stages composed into the pipeline
  • pkg/controller/deployer — downstream consumer of TemplateRenderedEvent + ValidationCompletedEvent
  • 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 2s, 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 is the size of the event subscription buffer.
	// ReconciliationTriggeredEvents are tiny (a reason string + correlation),
	// so a large buffer is cheap, and it must be large: under churn the
	// Reconciler fires one trigger per resource change and a StandardSubscriber-
	// Buffer (50) overflows, dropping triggers (and thus renders). The Start
	// loop drains this buffer to a single trigger per render (coalesceQueuedTriggers),
	// so it only ever needs to hold the triggers that arrive during one
	// render+validate cycle.
	CoordinatorEventBufferSize = busevents.DebugSubscriberBuffer
)
View Source
const ComponentName = "reconciler"

ComponentName is the unique identifier for this component.

EventBufferSize is the size of the event subscription buffer. High-volume component that receives resource change events from every configured watcher, sized to handle bursts when many resources change simultaneously.

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 + ValidationCompletedEvent
  5. If failure: Publish ReconciliationFailedEvent

The DeploymentScheduler still operates event-driven, receiving TemplateRenderedEvent and ValidationCompletedEvent 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

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

CoordinatorConfig contains configuration for creating a Coordinator.

type PipelineExecutor

type PipelineExecutor interface {
	Execute(ctx context.Context, provider stores.StoreProvider) (*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