component

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: 5 Imported by: 0

README

pkg/controller/component

Shared event-loop scaffold consumed by every controller component that subscribes on construction and dispatches one event at a time.

Overview

The pattern most controller components share — subscribe to the EventBus during New(...) so events buffered during startup aren't lost, run a single goroutine that dispatches one event at a time, recover from panics inside the handler, and shut down cleanly when the context is cancelled — used to be duplicated in pkg/controller/resourceloader.BaseLoader and pkg/controller/validator.BaseValidator. This package consolidates it. The two Base* types still exist as thin wrappers for familiarity, but new components should embed *Base directly.

*ReadySignal (in ready.go) is a small one-shot helper for components that need to signal "I'm ready" exactly once — used by the deployer, coordinator, config publisher, and a few others.

Quick Start

import (
    "log/slog"

    "gitlab.com/haproxy-haptic/haptic/pkg/controller/component"
    busevents "gitlab.com/haproxy-haptic/haptic/pkg/events"
)

type MyComponent struct {
    *component.Base
}

func (m *MyComponent) HandleEvent(event busevents.Event) {
    // process exactly one event; panics are caught and logged by Base
}

func New(bus *busevents.EventBus, logger *slog.Logger) *MyComponent {
    c := &MyComponent{}
    c.Base = component.New(&component.Config{
        EventBus:   bus,
        Logger:     logger,
        Name:       "my-component",
        BufferSize: 100,
        Handler:    c,
        EventTypes: []string{events.EventTypeFoo, events.EventTypeBar}, // empty = all events
    })
    return c
}

// Then in iteration.go:
c := New(bus, logger)
go c.Start(ctx)

EventTypes is the typed-subscription filter — leave empty for components like the commentator that consume everything; populate it for everyone else so the bus filters at the source instead of dispatching every event to your channel.

Key Interfaces

// Required: dispatch one event at a time.
type EventHandler interface {
    HandleEvent(event busevents.Event)
}

// Optional: a scatter-gather responder that wants to publish a failure
// response if the handler panicked, instead of just logging.
type PanicHandler interface {
    HandlePanic(recovered any, event busevents.Event)
}

The base always logs the panic and keeps the loop alive regardless of whether the component implements PanicHandler — the interface is purely an opt-in extension point.

// Optional: declare event types with latest-wins semantics to run in
// MAILBOX mode.
type CoalescingHandler interface {
    CoalescesOn() []string
}

Returning a non-empty list switches Start into mailbox mode: a dedicated intake goroutine moves events off the subscription channel the instant they arrive into an internal unbounded queue, so the bus-side buffer can never fill and the bus never drops this subscriber's events — no matter how slow HandleEvent is. Uninterrupted runs of coalescible events (per busevents.CoalescibleEvent) of a declared type collapse to their latest element; everything else preserves arrival order. Backlog growth is surfaced via a warning at power-of-two queue lengths from 256.

Two rules, both load-bearing:

  1. Declaring a type asserts that ONLY the latest queued event of that type matters to THIS component. Never declare a type whose every instance carries per-event bookkeeping (the deployer must see every deployment.completed to clear its in-flight flag, so it declares only deployment.scheduled). Coalescing is strictly per-subscriber — your declaration never affects other components' copies.
  2. Across restarts of the same instance (leadership terms), queued mailbox events are discarded at the next Start — same semantics as FlushPending for buffered channel events.

Lifecycle

Method Purpose
New(*Config) Subscribes to the EventBus and returns a *Base. Subscription happens in the constructor so the component is ready to receive events the moment bus.Start() is called.
Start(ctx) Runs the event loop until ctx is cancelled or Stop() is called. Blocks.
Stop() Idempotent shutdown signal. Useful for tests; production code typically just cancels the iteration context.

See Also

  • pkg/controller/resourceloaderBaseLoader thin wrapper used by configloader / credentialsloader
  • pkg/controller/validatorBaseValidator thin wrapper used by the scatter-gather validators
  • pkg/events — the bus this scaffold subscribes to
  • ready.go in this package — ReadySignal helper for one-shot ready signalling

License

Apache-2.0 — see root LICENSE.

Documentation

Overview

Package component provides a shared event-loop scaffold for controller components that subscribe on construction and dispatch one event at a time. The two domain-flavoured wrappers in the controller tree (pkg/controller/resourceloader.BaseLoader and pkg/controller/validator.BaseValidator) embed *Base for the actual subscribe/dispatch/panic-recovery loop and add their own domain-specific dispatch on top.

Consumers embed *Base and implement EventHandler. Components that need a domain-specific response to a panic (e.g. scatter-gather responders) additionally implement PanicHandler.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func SafeDispatch

func SafeDispatch(logger *slog.Logger, name string, event busevents.Event, handle func())

SafeDispatch invokes handle, recovering and logging any panic so a single bad event cannot tear down a component's event loop. Components that embed Base get this protection automatically via (*Base).dispatch; components that cannot embed Base — because they use a lossy subscription or add extra ticker/timer arms to their select — call this directly around their per-event handling instead, getting the same recover-and-keep-alive guarantee.

Types

type Base

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

Base is a reusable event-loop implementation. It subscribes on construction (so components are guaranteed to receive events published after EventBus.Start()), wraps each dispatch in a recover, and supports graceful shutdown via either a cancelled context or an explicit Stop.

func New

func New(cfg *Config) *Base

New subscribes to the EventBus and returns a Base ready to Start. The logger is annotated with `component=<name>` before being stored. Config is taken by pointer because the struct is large enough that the linter flags by-value passing.

func (*Base) EventBus

func (b *Base) EventBus() *busevents.EventBus

EventBus returns the bus the component subscribed to, for use in handler implementations that need to publish response events.

func (*Base) FlushPending

func (b *Base) FlushPending()

FlushPending discards every event currently buffered on the subscription channel without dispatching it. Leader-only components that embed Base — and are therefore subscribed for the whole process lifetime, not per leadership term — call this at Start entry so events buffered during a previous leadership term (or while not leader) are not replayed into the new term. Events that arrive after the flush are dispatched normally.

func (*Base) Logger

func (b *Base) Logger() *slog.Logger

Logger returns the component-annotated logger.

func (*Base) Name

func (b *Base) Name() string

Name returns the component name supplied at construction.

func (*Base) Start

func (b *Base) Start(ctx context.Context) error

Start drives the event loop until the context is cancelled or Stop is called. Returns nil on graceful shutdown. Handlers implementing CoalescingHandler (non-empty CoalescesOn) run in mailbox mode — see CoalescingHandler for the semantics and why.

func (*Base) Stop

func (b *Base) Stop()

Stop signals the event loop to exit. Safe to call multiple times.

type CoalescingHandler

type CoalescingHandler interface {
	CoalescesOn() []string
}

CoalescingHandler is an optional interface implemented by handlers whose events of the declared types have latest-wins semantics. When it returns a non-empty list, Base runs in MAILBOX mode: a dedicated intake goroutine drains the subscription channel immediately into an internal unbounded queue, so the bus-side buffer can never fill and the bus never drops this subscriber's events — no matter how slow the handler is. Uninterrupted runs of coalescible events of a declared type (i.e. event.(busevents.CoalescibleEvent).Coalescible() == true) collapse to their latest element at the queue tail; any other event is appended, preserving arrival order across event types. The worker dispatches from the queue head at its own pace.

This exists because slow handlers (e.g. status appliers doing SSA round-trips per event) otherwise stall the channel long enough under burst for the bus to overflow the subscriber buffer and drop events — including non-coalescible ones and the final event of a burst, whose loss leaves stale state until the next external trigger.

Declaring a type is a per-component statement that ONLY the latest queued event of that type matters to THIS component. Never declare a type whose every instance carries per-event bookkeeping for the component (e.g. the deployer must see every deployment.completed to clear its in-flight flag, so it declares only deployment.scheduled).

An empty list disables coalescing — handlers that conditionally need it can return nil to opt out at runtime (plain channel loop, no mailbox).

type Config

type Config struct {
	EventBus   *busevents.EventBus
	Logger     *slog.Logger
	Name       string
	BufferSize int
	Handler    EventHandler
	EventTypes []string
}

Config wires up a new Base.

If EventTypes is empty the component receives every event on the bus; if it is non-empty the component receives only the listed types (preferred for components that only react to a handful of events).

type EventHandler

type EventHandler interface {
	HandleEvent(event busevents.Event)
}

EventHandler dispatches a single event received on the subscription channel. Implementations should not block the goroutine for longer than the processing budget documented on the event.

type PanicHandler

type PanicHandler interface {
	HandlePanic(recovered any, event busevents.Event)
}

PanicHandler is an optional interface that a component implements when it needs to publish a domain-specific response if event handling panics. The base always logs the panic and keeps the loop alive regardless of whether this interface is implemented.

type ReadySignal

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

ReadySignal is a one-shot signal used by leader-only components to let callers wait until the component has finished subscribing to the event bus. Leader-only components subscribe in Start() (not in their constructor), and the controller start-up sequence waits on SubscriptionReady() before proceeding so published events are not lost.

Embed *ReadySignal in a leader-only component to pick up the accessor and Mark helpers instead of repeating the channel plumbing in each file.

Example:

type Component struct {
    *component.ReadySignal
    // ... other fields
}

func New(...) *Component {
    return &Component{ReadySignal: component.NewReadySignal(), ...}
}

func (c *Component) Start(ctx context.Context) error {
    c.eventChan = bus.SubscribeTypesLeaderOnly(...)
    c.MarkReady()
    // ... event loop
}

func NewReadySignal

func NewReadySignal() *ReadySignal

NewReadySignal constructs an un-signalled ReadySignal.

func (*ReadySignal) MarkReady

func (r *ReadySignal) MarkReady()

MarkReady closes the underlying channel exactly once. Subsequent calls are no-ops, so components can invoke it defensively at the end of their subscription step without caring whether Start has already run.

func (*ReadySignal) SubscriptionReady

func (r *ReadySignal) SubscriptionReady() <-chan struct{}

SubscriptionReady returns a channel that is closed when MarkReady is called. It implements lifecycle.SubscriptionReadySignaler (checked via interface satisfaction at call sites rather than by import to avoid a cyclic dependency on pkg/lifecycle).

Jump to

Keyboard shortcuts

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