Reconciliation: restore an invariant from the state you can observe
Design repeatable controller actions that converge toward desired state despite missed events, stale reads, and uncertain external outcomes.
On this page
The question for today
What you’ll understand
- Write a desired-versus-observed invariant and a no-op fixed point.
- Distinguish repeated reconciliation from replaying event commands.
- Explain idempotent external effects and truthful status reporting.
A controller repeatedly compares declared intent with observed reality and takes bounded steps to reduce the difference. The event that wakes it is a hint to inspect a resource, not an instruction to repeat a historical side effect. This module uses Kubernetes v1.34 API documentation, client-go v0.34.0 where library behavior matters, and explicitly labeled toy models. The central engineering task is preserving the intended invariant through retries, partial failure, and changes that arrive while reconciliation is already running.
Describe the invariant before the event handler
For a fictional worker-group resource, the desired specification may request five workers while observation shows three current owned workers. The controller should inspect identity, ownership, deletion state, and readiness before deciding which action is missing. A watch notification that a worker disappeared does not mean blindly create one more worker: another reconciliation may already have replaced it, or the user may have reduced the desired count. Read current state and compute the required difference instead of accumulating event-derived commands.
A converged state is a fixed point for the controller’s intended actions while the desired state and relevant dependencies remain unchanged. Reconciliation may still refresh observations, but should avoid unnecessary writes and duplicate external resources. Creating a worker is also different from that worker becoming ready; observed existence, scheduling, startup, and application readiness are separate milestones. A desired count is not a promise that capacity is available or that every attempted action succeeds within one pass.
Retries must discover an earlier uncertain outcome
Suppose an external create request times out after the remote service may already have accepted it. Retrying with a fresh random identity can leak a second resource. Use a stable operation identity or deterministic external name, then query or ensure the intended resource on a retry. The remote API must actually support the chosen idempotency contract. Recording a local success flag before the external effect can instead lose the effect after a crash, while recording only afterward leaves an uncertainty window that the protocol must handle.
Status should report observed facts about the relevant desired generation, rather than claim success because an API write returned. Many resource types use generation-related conventions, but the exact generation and observedGeneration semantics belong to their API definition. A Ready condition for an older desired generation is not evidence that the new configuration is ready. Avoid unconditional status updates that trigger another reconcile without changing information. Distinguish retryable dependency failures from invalid specifications that need a user-visible condition and a later change.
Bound each pass and preserve recovery evidence
A reconciliation loop should limit work per pass, respect cancellation, and leave enough durable evidence to resume after a crash. External resources need an identity tied to the object’s immutable UID rather than only a reusable namespace/name. Deletion follows a different branch from ordinary provisioning, so inspect the deletion timestamp before creating more work. Protect updates against competing writers, and make every retry re-evaluate whether the desired operation still applies. Progress requires eventual useful reads and successful effects, not merely an infinite loop.
The optimization lab supplies a small fixed-point analogy. Reset, set step size η=0.5 and initial position x₀=2, predict x₁=1, then Step once. A second Step gives 0.5. Its exact gradient-descent recurrence moves toward zero under this step size; it does not reconcile API objects, count workers, or handle external failures. Changing controls preserves elapsed time, so Reset before a fresh experiment. Use the analogy to ask whether repeated application approaches the intended state, then test the actual controller’s invariants separately.
Work it through
Bounded scale-up after a duplicate notification
A teaching controller desires five workers and observes two valid owned workers. It creates at most two per successful pass. Assume no other changes and that created workers become visible before the next pass.
The first pass computes deficit 5−2=3 and creates min(2,3)=2 workers. Observation then reaches four existing workers, without yet claiming those workers are application-ready.
The second pass recomputes the deficit from current observation: 5−4=1. It creates one, bringing existence count to five. It does not replay the first pass’s two-create action.
A duplicate notification triggers a third pass with deficit zero, so no create occurs. If the observation were stale, identity checks and safe create semantics would still be needed; the simple count recurrence assumes that uncertainty away.
Two productive passes create three workers; subsequent unchanged passes are no-ops for provisioning. Readiness still requires its own observed condition.
The living notebook
Explore the model
Interactive lab · optimization
A repeatable transition approaches a fixed point
Reset, set η=0.5 and x₀=2, predict x₁=1, then Step; another Step gives 0.5. This exact descent on x²/2 is only a convergence analogy, not a Kubernetes controller, provisioning loop, or failure simulator.
- Position x0
- 2
- Objective x²/2
- 2
- Iteration behavior
- Stable step
Gradient descent on f(x) = x²/2: xₙ = (1 − η)ⁿx₀. Playback advances two discrete iterations per second. Values outside |x| ≤ 4 are explicitly reported. Parameter changes pause playback and recompute the model at the current elapsed time. Reset restores both parameters and returns time to zero. Playback starts paused. Adjust either parameter, then choose Play or Step.
Look forA fixed-point argument must name the transition and its assumptions before being applied to a real controller.
Pause & reason
Check your understanding
Your turn
Try it yourself
A fixed desired count is eight, observation starts at three, and a toy controller adds at most two successful workers per pass. How many productive passes are needed and what are the counts?
Take a moment to make a prediction or work it out on paper. Open a hint when you need one.
Give me a hint
Recompute the remaining deficit after each pass; the final pass may add fewer than two.
Compare with the solution
The counts progress 3→5→7→8, requiring three productive passes and five creations. A later unchanged pass creates none. This assumes successful immediate observation and no deletions or new intent; real readiness and stale-read handling require additional checks.
Keep the idea
Take it with you
- Reconcile present state and intent instead of replaying event commands.
- Stable identities and supported idempotency make uncertain effects recoverable.
- Converged provisioning and truthful readiness are distinct invariants.
Read further
Original sources and deeper explanations to follow at your own pace.
Optional progress, saved in this browser when available. Revisit any lesson at any time.
Lesson not marked complete.