Skip to main content

Engine reconciler architecture

The engine reconciler is split into three layers, with instance resolution as a hard prerequisite:

Layer responsibilities

The instance gate runs after the read layer but before the compute layer. It only blocks for phases that may build ConfigMaps containing instance.multi_engine.metadata_endpoint and instance.id: stable, stopped, and creating. stopped is included because if a ConfigMap is missing at zero replicas, the reconciler re-materializes it in place using live instance info. This is the same recovery path as stable. Phases that operate on existing resources (switching, draining, cleaning) skip the gate and proceed normally, ensuring that a transient instance issue does not stall an in-flight rollout. When the gate blocks, it sets the InstanceReady=False condition on the engine status and requeues. The condition update is part of the single updateStatus call. There is no separate status write for conditions. The compute layer is the core of the Firebolt Operator. It is a pure function with no side effects, making it easy to test exhaustively without a running cluster.

State machine

The engine lifecycle is a six-phase state machine stored in .status.phase. Two of the six (stable and stopped) are terminal. The others are transition phases. The terminal phase is chosen by spec.replicas: non-zero resolves to stable, zero resolves to stopped. Every transition phase funnels through a single terminalPhase(spec) helper so the distinction is made in exactly one place.
Both terminal phases route spec-change detection through the same computeStable code path. From the state machine’s perspective stopped is just stable with spec.replicas == 0 and a different surfaced name. The Ready condition distinguishes them: stable with ready pods is Ready=True, Reason=EngineReady. stopped is always Ready=False, Reason=Stopped. See Top-level Ready condition.

Phase descriptions

Key invariant

A spec change during draining or cleaning does not create a new generation. The current transition must complete before a new one begins. This prevents unbounded resource accumulation.

Top-level Ready condition

setReadyCondition derives status.conditions[type=Ready] from the post-reconcile phase and pod state. Its precedence is:
  1. InstanceNotReady: The referenced FireboltInstance is not healthy. Wins over everything else because nothing downstream works without it.
  2. Stopped: Phase == stopped. Ready=False, Reason=Stopped, Message="Engine is stopped (spec.replicas is 0)". Explicitly distinguished from Rolling so GitOps tooling can tell an intentionally parked engine apart from one mid-transition.
  3. Rolling: Phase is any non-terminal phase (creating / switching / draining / cleaning). Ready=False, Reason=Rolling.
  4. PodsNotReady: Phase is stable but the active-generation pods have not all reported Ready yet. Ready=False, Reason=PodsNotReady.
  5. EngineReady: Default. Ready=True. The engine is serving traffic on its active generation.
Reason Stopped is the only Ready=False reason that is not a transient rollout or instance-dependency failure. GitOps tools that key off Ready=True should treat a stopped engine as deliberately not-converged-to-serving rather than retrying it indefinitely.

StatefulSet event propagation

A FireboltEngine can get stuck in creating or in stable with PodsNotReady when its generation StatefulSet exists but the StatefulSet controller cannot create the desired pods. Common causes include a missing ServiceAccount, exceeded ResourceQuota, PodSecurity or admission rejection, RBAC denial, unbindable PVC, and similar issues. The Firebolt Operator owns the StatefulSet, but Kubernetes records the actionable error as a Warning event, typically FailedCreate, on the StatefulSet object. Without this propagation, you would have to run kubectl describe sts <name> to triage. To surface this on the FireboltEngine itself, after computing the Ready condition the reconciler queries the apiserver for Warning events on the current-generation StatefulSet whenever:
  • CurrentSTS != nil: There is an STS to look up events for.
  • CurrentPodTotal < spec.replicas: Pods are missing rather than just unready.
  • Ready.Reason ∈ {Rolling, PodsNotReady}: The existing reason is a generic “stuck” reason that we are allowed to refine. InstanceNotReady, DrainCheckFailing, Stopped, and EngineReady are higher-precedence diagnostics or healthy states and are not overridden.
When a Warning event matches, the Ready condition is rewritten with that event’s Reason, such as FailedCreate, and a message of the form StatefulSet <name>: <event message> (x<count>). The lookup uses the Clientset, not the controller-runtime cache, with field selector involvedObject.uid=<UID>,type=Warning. Events are high-volume cluster-wide, and a watch would inflate the controller’s cache for a signal we consult only on already-stuck engines. Fetch failures are logged and swallowed. The diagnostic is best-effort and must never poison the main reconcile path. Once pods come up the trigger gate stops firing and the next reconcile restores EngineReady.

Generation model

Each spec change (while in stable or stopped) increments status.currentGeneration. Resources for each generation are named with a -g<N> suffix:
At most two generations exist simultaneously: the active one serving traffic and the new one being created (or the old one being drained/cleaned). stsMatchesSpec is the central drift detector. It compares the live StatefulSet against the resolved engine spec field-by-field. Any mismatch returns false and the reconciler bumps currentGeneration. Two annotations on the StatefulSet act as content hashes for inputs that don’t have a clean direct comparison:

Error handling

The Firebolt Operator follows strict error propagation rules to ensure failures are always visible. No swallowed errors. Every error from an I/O operation is either:
  1. Returned to the caller (causing a retry via requeue), or
  2. Logged and aggregated when multiple independent cleanup operations must all be attempted (e.g. reconcileDelete).
Specific policies:

Status update strategy

Status updates use r.Status().Update() with a single retry on conflict. If a resource version conflict occurs because a concurrent spec update changed the object, the Firebolt Operator re-fetches the latest object, applies the new status, and retries once. This avoids unnecessary reconcile-loop failures from optimistic concurrency.

Crash recovery

The Firebolt Operator is crash-safe at every phase boundary. If the process terminates:
  • During stable → creating transition: The stable phase writes only the status intent (Phase=creating, bumped currentGeneration) in one pass, then requeues. Resources are not created until the status update is persisted. If the Firebolt Operator crashes before the status write, no resources were created and the next reconcile retries from stable. If it crashes after, the next reconcile enters creating and creates the resources normally.
  • During creating: The next reconcile sees an existing StatefulSet with not-ready pods and waits. All ensure calls are idempotent, so partial resource creation is safe. If the spec changed and the Firebolt Operator crashed after deleting the old generation’s resources but before bumping currentGeneration, the next reconcile finds no resources for the current generation and recreates them fresh, converging to the correct state.
  • During switching: the next reconcile checks the service selector and either updates it or proceeds.
  • During draining: the next reconcile re-runs the drain check.
  • During cleaning: the next reconcile re-deletes any remaining old resources (delete is idempotent).
  • During stable: no work needed.
No persistent state outside of the Kubernetes API server is required.

Pod template merge

Per-engine pod-template overrides live under spec.template. When spec.engineClassRef is set, the Firebolt Operator composes the rendered StatefulSet pod template from three layers (top wins on conflict for scalar and struct fields):
  1. Firebolt Operator defaults (terminationGracePeriodSeconds=60, hardened runAsUser / fsGroup, firebolt.io/engine and firebolt.io/generation labels, operator-owned volumes).
  2. FireboltEngineClass.spec.template (shared by every engine that references the class).
  3. FireboltEngine.spec.template on this engine.
List-typed fields (tolerations, initContainers, sidecars, env, envFrom, volumeMounts, imagePullSecrets, volumes) concatenate class-then-engine. The validating webhook applies the same allowlist to class and engine templates. Rejected paths are enumerated in the FireboltEngine CRD reference and FireboltEngineClass CRD reference. For merge rules the reconciler uses, see FireboltEngineClass design. Changes to spec.template or the resolved class content trigger a new blue-green generation.

Firebolt Operator-managed resources

Do not modify these resources manually. For an engine named my-engine:

Admission resource bounds

The validating webhook can be configured with per-dimension maxima for the engine container’s resources.requests and resources.limits. When configured, any FireboltEngine create or update whose engine container cpu, memory, or ephemeral-storage value exceeds the matching maximum is rejected at admission with a spec.template.spec.containers[engine].resources.{requests,limits}.{name} field error. Bounds are opt-in (defaults are empty). Configure via Helm when the webhook is enabled:
Or via Firebolt Operator flags:
Bounds apply independently per dimension. Resource names without a configured maximum pass through unchecked. Both requests and limits are checked against the same per-dimension maximum.

Resource ownership

All per-engine resources have:
  • An ownerReference pointing to the FireboltEngine CR (for garbage collection on CR deletion).
  • A firebolt.io/engine label (for listing/filtering).
  • A firebolt.io/generation label (for generation-based selection).
  • A finalizer on the CR itself to ensure cleanup runs before the CR is removed.