Reconcile flow
The code path of one Release reconcile in the controller-manager, and the observer loops that work with it.
This page follows one Release key through the Release controller of the controller-manager.
It names the function of each step. Then it shows how the two observer loops wake the
controller, and how the controller wakes them. For the behavior of each step, follow the links
to the concept pages.
Entry: syncHandler#
The work queue calls (*Controller).syncHandler with a namespace/name key. The function does
these steps in this order:
- It reads the Release from the informer cache. If the Release does not exist, it returns.
- Shard gate. If this replica does not own the shard of the target
Cluster, it returns. It does not finalize the Release either. See Sharding. - If the Release has a deletion timestamp, it calls
finalizeand returns. - Else it calls
ensureFinalizer. This addsrelease.core.convoy.kumodo.io/prune-workloadswith anUpdateif it is missing. - It calls
reconcile, which returns areconcileOutcomeand an error. - It builds the new status and writes it. The section “Status assembly” below shows the steps.
- It schedules the next reconcile. The section “Requeue rules” below shows the rules.
finalize#
finalize runs only when the finalizer is present. It does these steps:
- It resolves the target
ClusterwithclusterForFinalize, if it must prune or run delete hooks. A missing or unreachable cluster disables both steps. WithblockOnClusterUnreachable, an unreachable cluster returns an error and the deletion waits. - It runs the
preDeletehooks withrunHookPhase. A hook that runs or fails requeues the key after 10 seconds. - It deletes the inventoried objects with
pruneRefs, unlessdeletionPolicy.keepWorkloadsistrue. - It runs the
postDeletehooks. A running hook requeues after 10 seconds. A failed hook is only logged. - It deletes the
ReleaseInventory. - It deletes the render store prefix of the Release, if a render store is configured.
- It clears the finalizer.
See Deletion.
reconcile#
reconcile first calls newReconcileState. This records the attempt time and the sync mode. It
also reads the pending /sync and /sync?force=true requests from the annotations. Then it runs
the stages below. Each stage can stop the pipeline with an outcome.
1. stageRender#
stageRender does these steps in this order:
- Fetch.
coordinator.Ensureresolves and fetches the source. An error is transient. A result that is not ready stops the reconcile with reasonUnsupportedand no error. - Cluster. It reads the target
Clusterfrom the cache. A missing or unreachable cluster stops the reconcile with reasonClusterNotReadyand an error. - Values.
resolveValueLayersreads the Helm value sources. Only thehelmcontent type has values. - Render.
render.Renderrenders the content. See Rendering. - Mutations.
render.ApplyMutationsruns the post-render mutations. See Mutations. - Render reference.
renderRefcalculates the SHA-256 hash of the mutated objects. The result goes tolastAttempted.renderRef. - Pin.
resolvePinnedRenderreplaces the objects with the pinned render from the render store, if the pin points to a different render. A missing store or a missing render stops the reconcile with reasonPinned. See Pin and history.
2. planRollout#
If the Release has no pin, reconcile calls planRollout. This function makes at most one
transition of the rollout state machine. It returns the new rollout state and an action:
actionApplySource or actionApplyLastHealthy. A pin skips the state machine and applies the
pinned or current render. See Rollout state machine and
Progressive rollout.
3. applyLastHealthy#
For actionApplyLastHealthy, reconcile calls applyLastHealthy and returns. This function
reads the last healthy render from the render store, applies it, prunes and writes the
inventory. It skips the gates, the dry run and the hooks. If a step fails, it logs the error.
Then reconcile returns an outcome with reason RolledBack and does not apply the source render.
4. stageGates#
stageGates checks two holds. Neither hold applies anything.
- Manual gate. In manual sync mode without a pending
/sync, the stage stops. The reason isAppliedwhenlastSuccessful.renderRefequals the new render. Else the reason isWaitingForSync. See Sync policy. - Cluster wave gate.
clusterWaveGatereads the other Releases on the same cluster from thereleaseByClusterindex. A Release with a lower cluster wave that is notAppliedor notHealthyholds this Release with reasonWaitingForClusterWave. A bounded wait that times out givesClusterWaveFailed. See Sync waves.
5. stageSync#
stageSync does these steps in this order. See Apply and prune.
- Dry run.
dryRunGatesends all objects as a server-side apply dry run. A rejection stops the sync with reasonUnappliableand no error. A forced sync skips this step. - Persist.
persistRenderwrites the render to the render store. Without a render store, it does nothing. See Render storage. - preSync hooks.
runHookPhaseforpreSync. A running hook holds with reasonWaitingForHook. A failed hook withonFailure: Failstops with reasonHookFailed. See Lifecycle hooks. - Waves.
waves.Groupgroups the objects by sync wave. For each wave in ascending order:- If the wave needs health, and the previous wave is not healthy in the
ReleaseInventory,waveHealthGateholds or fails the sync. deployer.Deployapplies the objects of the wave.inventory.Putwrites the union of the previous inventory and the objects applied so far.
- If the wave needs health, and the previous wave is not healthy in the
- Prune.
prunedeletes the objects of the previous inventory that are not in the applied set. - Inventory.
inventory.Putwrites the exact applied set. - postSync hooks.
runHookPhaseforpostSync. A failed hook does not change the result. - History.
recordHistoryappends the render to the history log of the render store, if it is not there yet. - GC.
runGCdeletes old history entries and render blobs. It keeps the renders thatlastSuccessful,lastAttempted, the pin andlastHealthyrefer to.
Step 8 and step 9 never stop the sync. They only log their errors.
6. syncFailed hooks#
After stageSync, reconcile checks the outcome with isSyncingFailure. If the attempt failed
at the Syncing stage, runSyncFailedHooks arms the syncFailed hooks. It does not wait for
them. A dry-run rejection, a failed wave and a failed prune are Syncing failures.
Status assembly#
syncHandler builds the new status from the cached status and the outcome:
computeStatussets theAppliedcondition from the outcome and setsobservedGeneration.pinStatussetsstatus.pin.- The attempt goes to
lastAttempted. If the outcome applied a render, it also goes tolastSuccessful. - In manual mode, a handled
/syncgoes tolastHandledSyncAt. - The outcome replaces
status.waves. resolvedRetentionStatussetsresolvedRetentionPolicy.lastHealthyadvances when the verdict for the attempted render is healthy.- A pending forced sync goes to
lastHandledForceSyncAt. - The rollout of the outcome goes to
status.rollout. If the outcome has no rollout, the previous rollout stays. Without a failure policy, convoy clears the rollout.rolloutConditionsetsRolloutSucceeded. applyLifecycleRollupreads the hook entries of theReleaseInventoryfrom the API server and setsstatus.lifecycleandLifecycleSucceeded.- If the status changed, the controller sets
reconciledAtand writes the status with SSA under the field managerconvoy-release.
See Status ownership.
Requeue rules#
After the status write, syncHandler decides the next reconcile:
Result of reconcile | Action |
|---|---|
Error, and syncPolicy.retryBackoff.initial is more than 0 | Increments the failure count of the key. Requeues after backoffDelay. Returns no error. |
Error, and no retryBackoff | Returns the error. The default rate limiter of the work queue retries. |
| No error | Resets the failure count. Requeues after requeueAfter. |
backoffDelay is initial × factor^(n−1). The default factor is 2. The default maximum is
1000 seconds. requeueAfter is syncPolicy.interval. While the rollout is Progressing, it is
at most 10 seconds. If the result is 0, the controller does not requeue the key. Then only
events and the informer resync start the next reconcile.
An error from the status write, from ensureFinalizer or from applyLifecycleRollup is
returned directly. The work queue retries it.
Each hold and each terminal failure returns no error. Examples are WaitingForSync,
WaitingForHook, Unappliable and HookFailed. These outcomes wait for the interval or for an
event.
Observer side#
The observer runs two loops on the same Release informer. Each loop has its own shard gate. The drift loop ignores a Release that has a deletion timestamp. The health loop then only polls the delete-phase hooks.
Drift loop#
(*observer.Controller).syncHandler does these steps:
renderDesiredselects the desired objects. A pinned render in the store comes first. Then the last healthy render, if the rollout isRolledBack. Else it renders the source withrenderReleaseObjects, which also runs the mutations.diffObjectsreads each live object and sends a server-side apply dry run.writeInSyncwritesInSyncwith SSA underconvoy-observe. If the drift is not known, the condition isUnknown.- In automatic mode with drift,
requestSyncsendsPOST /sync. scheduleRecheckrequeues aftersyncPolicy.interval, if it is set.
An undetermined drift does not cause a retry. Only a failed status write or a failed /sync
request returns an error to the work queue. See Drift.
Health loop#
The health loop assesses the ReleaseInventory and writes Healthy. See
Health loop.
How the loops wake each other#
The loops do not share memory. They communicate through API objects:
| Event | Sender | Receiver | Mechanism |
|---|---|---|---|
The controller writes a new renderRef or changes the membership of the inventory | controller-manager | drift loop, health loop | Observer handler on the ReleaseInventory informer (RenderChanged) |
| The controller arms a hook or changes the membership | controller-manager | health loop | Observer handler (MembershipChanged, HooksArmChanged) |
The health loop writes a new state or assessedRenderRef | health loop | controller | Controller-manager handler (HealthChanged) |
| A wave completes, the assessment reaches a new render, or a hook finishes | health loop | controller | POST /sync sets the annotation |
| Drift in automatic mode | drift loop | controller | POST /sync sets the annotation |
Applied or Healthy of a Release changes | any status writer | controller, for the Releases of a higher cluster wave | Controller-manager handler (ClusterWaveSignalChanged) |
A POST /sync changes only an annotation. The two observer loops do not react to this
annotation. A status write wakes no loop for the same Release. See
Status ownership.
In the code
convoy v0.9.0
internal/controller/release.(*Controller).syncHandlerinternal/controller/release/controller.go:199internal/controller/release.(*Controller).reconcileinternal/controller/release/controller.go:824internal/controller/release.(*Controller).stageRenderinternal/controller/release/controller.go:901internal/controller/release.(*Controller).stageGatesinternal/controller/release/controller.go:1027internal/controller/release.(*Controller).stageSyncinternal/controller/release/controller.go:1092internal/controller/release.(*Controller).finalizeinternal/controller/release/controller.go:553internal/controller/release.requeueAfterinternal/controller/release/controller.go:325internal/observer.(*Controller).syncHandlerinternal/observer/observer.go:121