From 76e2439a190d41dd3e938ed5805bf5115de1de85 Mon Sep 17 00:00:00 2001 From: Tiger Kaovilai Date: Tue, 4 Aug 2026 18:00:15 -0400 Subject: [PATCH 1/2] fix: use MergeFrom patch for DPA status update to avoid conflicts Status().Update requires an exact resourceVersion match, so concurrent reconciles racing on the same DPA hit optimistic-lock conflicts ("the object has been modified"). Switch to a Status().Patch with client.MergeFrom(origDpa), which doesn't check resourceVersion, so it no longer fails when another reconcile loop updated the object first. Fixes #2236 Signed-off-by: Tiger Kaovilai --- controllers/dpa_controller.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/controllers/dpa_controller.go b/controllers/dpa_controller.go index 02e6c7c0e91..7398faa1b2d 100644 --- a/controllers/dpa_controller.go +++ b/controllers/dpa_controller.go @@ -87,6 +87,10 @@ func (r *DPAReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.R log.Error(err, "unable to fetch DataProtectionApplication CR") return result, nil } + // origDpa snapshots status before reconciliation mutates it, so the final + // status update is a merge patch (no resourceVersion check) instead of a + // full update, avoiding optimistic-lock conflicts from concurrent reconciles. + origDpa := dpa.DeepCopy() // set client to pkg/client for use in non-reconcile functions oadpClient.SetClient(r.Client) @@ -128,7 +132,7 @@ func (r *DPAReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.R }, ) } - statusErr := r.Client.Status().Update(ctx, &dpa) + statusErr := r.Client.Status().Patch(ctx, &dpa, client.MergeFrom(origDpa)) if err == nil { // Don't mask previous error err = statusErr } From b91f0749f7aa0f9703297f85b4f9c76dd32750f4 Mon Sep 17 00:00:00 2001 From: Tiger Kaovilai Date: Tue, 4 Aug 2026 18:06:46 -0400 Subject: [PATCH 2/2] fix: correct comment on why MergeFrom patch avoids status conflicts The original wording blamed "concurrent reconciles", but controller-runtime serializes reconciles per object key (single leader-elected replica), so two reconciles of the same DPA never run concurrently. The real cause is a stale informer-cache read racing a prior status write. Fable review round 1 caught this. Signed-off-by: Tiger Kaovilai --- controllers/dpa_controller.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/controllers/dpa_controller.go b/controllers/dpa_controller.go index 7398faa1b2d..0b86e55a07c 100644 --- a/controllers/dpa_controller.go +++ b/controllers/dpa_controller.go @@ -89,7 +89,9 @@ func (r *DPAReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.R } // origDpa snapshots status before reconciliation mutates it, so the final // status update is a merge patch (no resourceVersion check) instead of a - // full update, avoiding optimistic-lock conflicts from concurrent reconciles. + // full update, avoiding optimistic-lock conflicts when the informer cache + // still lags behind a status write this controller (or another actor) + // already made to the object. origDpa := dpa.DeepCopy() // set client to pkg/client for use in non-reconcile functions