Repository navigation
Requeue VolumePopulator reconciles while waiting on external state - #2183
Merged
openshift-merge-bot[bot] merged 3 commits intoOct 27, 2026
Conversation
Contributor
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: JohnStrunk The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
Member
Author
|
/retest |
2 similar comments
Member
Author
|
/retest |
Member
Author
|
/retest |
The volume populator creates a prime PVC from the ReplicationDestination's latest snapshot and then relies on watch events (prime PVC bind, PV bind controller, RD status) to drive the PV claim rebind. Every "not ready" path returned an empty result with no requeue, so a missed event left the target PVC Pending forever. This was the largest single source of e2e flakes. Return a bounded 10s RequeueAfter from every path that waits on external state: missing StorageClass, WaitForFirstConsumer without a selected node, missing ReplicationDestination, RD without latestImage, prime without a volume, missing PV, after patching the PV claimRef, and waiting for the bind acknowledgement. Non-retryable and completed paths are unchanged. Also fix the pvcPrime shadowing in reconcilePVC(). A freshly created prime has no volume, so rebindPVClaim() currently returns early and the bug is latent; the fix keeps the outer value in sync as defense-in-depth. Make compare_pvc_data wait for both PVCs to be Bound with a wall-clock deadline (pvc_bound_timeout, default 120s), then fail with the PVC names, phases, and namespace. This keeps normal asynchronous population working while turning a stuck PVC into a precise failure instead of an opaque ~900s compare-Job wait. Add fake-client regression specs that drive the out-of-band prime through bind, claimRef patch, bind acknowledgement, and cleanup, and that pin the requeue for a missed bind event. Signed-off-by: John Strunk <jstrunk@redhat.com> Co-authored-by: OpenCode - DeepSeek V4.1 Flash <noreply@opencode.ai>
RequeueAfter bypasses controller-runtime's rate limiter, and these waits are for healthy external actors rather than failing operations. Record the rationale so a future reader weighs the recovery-latency trade-off before switching to exponential backoff. Signed-off-by: John Strunk <jstrunk@redhat.com> Co-authored-by: OpenCode - DeepSeek V4.1 Flash <noreply@opencode.ai>
The previous revision required both PVCs to be Bound before creating the compare Job. On OpenShift the default StorageClass (gp3-csi) uses volumeBindingMode: WaitForFirstConsumer, so a PVC cannot bind until a pod consumes it -- and that pod is the compare Job. The check deadlocked and failed all three OpenShift e2e jobs (42 playbooks each, data-dest*=Pending). Create the Job first, then wait for both PVCs to bind with the wall-clock deadline (pvc_bound_timeout, default 300s, since WFFC plus EBS provisioning is slower than the old 120s), then wait for the Job. The Job is deleted on success and on every failure path, and log retrieval tolerates a pod that never scheduled. Signed-off-by: John Strunk <jstrunk@redhat.com> Co-authored-by: OpenCode - DeepSeek V4.1 Flash <noreply@opencode.ai>
JohnStrunk
force-pushed
the
opencode/issue-2178-volumepopulator-rebind-requeue
branch
from
October 27, 2026 18:12
acc950b to
a16ca3b
Compare
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2183 +/- ##
=====================================
Coverage 68.4% 68.4%
=====================================
Files 61 61
Lines 6220 6221 +1
=====================================
+ Hits 4255 4261 +6
+ Misses 1645 1641 -4
+ Partials 320 319 -1
🚀 New features to boost your workflow:
|
Contributor
|
/lgtm |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
Fixes #2178.
A PVC using a
ReplicationDestinationas itsdataSourceRefcould stayPendingforever. The controller creates the temporaryvs-prime-<uid>PVCfrom the latest snapshot, waits for the prime to bind, and then waits for the
bind controller to release the volume to the target PVC. Every "not ready" path
returned an empty
ctrl.Result, so progress depended entirely on watch events(prime PVC bind, PV bind controller, RD status). A missed event had no recovery:
the prime ended up
Bound, the target stayedPending, and pods mounting itcould not schedule. This was the largest single source of e2e flakes (7 of the
10 job failures between 2026-10-08 and 2026-10-23, across unrelated playbooks).
Changes
internal/controller/volumepopulator_controller.go):every path that waits on external state now returns
ctrl.Result{RequeueAfter: 10s}— missing StorageClass,WaitForFirstConsumerwithout the selected-node annotation, missingReplicationDestination, RD without
latestImage, prime PVC without a volume,PV not found, after patching the PV
claimRef, and waiting for the bindacknowledgement. Non-retryable paths (in-tree StorageClass, Direct/None copy
method) and the completed/cleanup path still return an empty result. The
interval is deliberately fixed rather than exponential backoff (recorded in
the code comment): these waits target healthy external actors, not failing
operations;
RequeueAfterbypasses controller-runtime's rate limiter; and aflat interval keeps the worst-case recovery bounded and predictable.
pvcPrimeshadowing fix:reconcilePVC()no longer shadows the outerpvcPrimewhen assigning the result ofreconcilePVCPrime(). A freshlycreated prime always has no volume, so
rebindPVClaim()returns early todayand the bug is latent; the fix keeps the outer value in sync as
defense-in-depth so the wait-for-rebind check cannot be bypassed if that early
return changes.
compare_pvc_datadiagnostics(
test-e2e/roles/compare_pvc_data/tasks/main.yml): the compare Job is createdfirst — it is the first consumer of the PVCs, which
WaitForFirstConsumerstorage requires before any volume can bind — then the role waits for both
PVCs to be
Boundwith a wall-clock deadline ofpvc_bound_timeout(default300s), then waits for the Job. If a PVC is still unbound at the deadline, the
role fails with both PVC names and phases, the namespace, and guidance. The
Job is deleted on success and on every failure path, and log retrieval
tolerates a Job whose pod never scheduled.
An earlier revision checked
Boundbefore creating the Job. That deadlockedthe OpenShift e2e jobs, where the default StorageClass (
gp3-csi) isWaitForFirstConsumer: 42 playbooks per job failed because the only consumerwas the compare Job that the check was blocking. Creating the Job first
restores the previous binding behavior while keeping the bounded diagnostic.
retriesis only a safety cap —now()against a deadline keeps the boundhonest even when a
k8s_infocall is slow.internal/controller/volumepopulator_controller_test.go):fake-client specs drive the exact stuck sequence (prime created out-of-band →
prime binds →
claimRefpatched → bind-controller acknowledgement → cleanup)and assert
RequeueAfterwhile waiting. With the requeues removed, 7 of the 8new specs fail.
Walkthrough
Why
The rebind depends on several external signals. A dropped event had no recovery:
no requeue was scheduled after creating the prime, after patching
claimRef, orwhile waiting for the claim release. No
SyncPeriodis configured, so informerresync defaults to 10 hours, and an idle RD emits no events at all.
See it work (disposable Kind cluster, real operator and API server)
A populator PVC whose ReplicationDestination had no
latestImage. The operatorre-reconciled every 10 seconds with no object changes:
A file was written to the source PVC before snapshotting. After
status.latestImagewas set once, the populator created the prime PVC, patchedthe PV claim, waited for the bind acknowledgement, and cleaned up:
Data written before the snapshot was readable from the populated PVC:
Fail-fast guard
The Job is created first, so the guard works for both binding modes. With one
Bound PVC and one deliberately unbound PVC (
storageClassNamethat does notexist) and
pvc_bound_timeout: 5, the play fails after the deadline with bothphases and deletes the compare Job:
WaitForFirstConsumer reproduction (the OpenShift failure)
Local Kind cluster, destination PVC on a
csi-hostpath-wffcStorageClass(
volumeBindingMode: WaitForFirstConsumer) populated by the VolumePopulator,with the compare role invoked while the destination is still
Pending. Becausethe Job is created first, its pod is the first consumer and binding proceeds:
Evidence and verification
go build ./...,go vet ./internal/controller/...golangci-lint run ./internal/controller/...→ 0 issuesgo test ./internal/controller -args -ginkgo.focus="VolumePopulator")→ 35 passed, 0 failed
completion path, which needs no requeue)
pre-commit runon the changed files (yamllint, markdownlint, whitespace,end-of-file) → passed
hack/setup-kind-cluster.sh, VolSyncfrom this tree,
hack/run-rustfs.sh):test-e2e/test_simple_rsync.yml→ 56 ok, 0 failedtest-e2e/test_volumepopulator.yml→ 142 ok, 0 failedshowboat verify); the only verification diff is the envtest duration line.three OpenShift e2e jobs (42 playbooks each,
data-dest*=Pending). Root causewas the pre-Job
Boundcheck deadlockingWaitForFirstConsumerstorage(
gp3-csi): the compare Job was the only consumer, and the check blocked it.The controller change was unaffected (its 10s requeue is visible in those logs
waiting for the selected-node annotation). This revision creates the Job first
and is validated locally on a WFFC StorageClass; OpenShift CI re-runs on push.
Reproduce and clean up
The cluster was disposable and was deleted after capture. The live run cannot
force the operator to drop a watch event, so missed-event recovery is proven by
the fake-client regression specs (asserting
RequeueAfterand driving the fullsequence); the live cluster shows the requeue cadence and end-to-end population.
To reproduce the OpenShift binding mode locally, use a StorageClass with
volumeBindingMode: WaitForFirstConsumeron the hostpath CSI driver for thepopulator destination PVC.
Scope notes
test-e2e/roles/compare_pvc_data_block/tasks/main.ymlhas the sameJob-mounts-PVCs shape but was intentionally left unchanged; the issue and its
acceptance criteria target
compare_pvc_data.gp3-csiStorageClass isWaitForFirstConsumer. Thecompare role therefore creates its Job before waiting for binding; this also
works for
Immediateclasses, where the wait normally succeeds on the firstattempt.
configuration now emits a warning event per 10s interval (aggregated by the
events API) instead of only on object changes. This was chosen deliberately
for a single, predictable recovery bound.
(AI generated)