fix: add retry logic to updateRecoveryWindow for concurrent status updates

When backup completion and retention policy enforcement run concurrently,
both call updateRecoveryWindow to update the ObjectStore status. This can
cause "object has been modified" errors due to Kubernetes optimistic
concurrency control.

This change wraps the status update in retry.RetryOnConflict, matching
the pattern already used in setLastFailedBackupTime in the same file.
The retry logic fetches a fresh copy of the ObjectStore before each
update attempt, ensuring the resourceVersion is current.

Fixes #758

Signed-off-by: Gabriel Mouallem <gabriel@latitude.sh>
This commit is contained in:
Gabriel Jose Mouallem Rodrigues 2026-02-03 15:01:30 -03:00
parent 5001fe7831
commit eefb45ca5a

View File

@ -33,7 +33,8 @@ import (
) )
// updateRecoveryWindow updates the recovery window inside the object // updateRecoveryWindow updates the recovery window inside the object
// store status subresource // store status subresource. It uses retry logic to handle concurrent
// updates from backup completion and retention policy enforcement.
func updateRecoveryWindow( func updateRecoveryWindow(
ctx context.Context, ctx context.Context,
c client.Client, c client.Client,
@ -41,6 +42,14 @@ func updateRecoveryWindow(
objectStore *barmancloudv1.ObjectStore, objectStore *barmancloudv1.ObjectStore,
serverName string, serverName string,
) error { ) error {
objectStoreKey := client.ObjectKeyFromObject(objectStore)
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
var freshObjectStore barmancloudv1.ObjectStore
if err := c.Get(ctx, objectStoreKey, &freshObjectStore); err != nil {
return err
}
// Set the recovery window inside the barman object store object // Set the recovery window inside the barman object store object
convertTime := func(t *time.Time) *metav1.Time { convertTime := func(t *time.Time) *metav1.Time {
if t == nil { if t == nil {
@ -49,16 +58,17 @@ func updateRecoveryWindow(
return ptr.To(metav1.NewTime(*t)) return ptr.To(metav1.NewTime(*t))
} }
recoveryWindow := objectStore.Status.ServerRecoveryWindow[serverName] recoveryWindow := freshObjectStore.Status.ServerRecoveryWindow[serverName]
recoveryWindow.FirstRecoverabilityPoint = convertTime(backupList.GetFirstRecoverabilityPoint()) recoveryWindow.FirstRecoverabilityPoint = convertTime(backupList.GetFirstRecoverabilityPoint())
recoveryWindow.LastSuccessfulBackupTime = convertTime(backupList.GetLastSuccessfulBackupTime()) recoveryWindow.LastSuccessfulBackupTime = convertTime(backupList.GetLastSuccessfulBackupTime())
if objectStore.Status.ServerRecoveryWindow == nil { if freshObjectStore.Status.ServerRecoveryWindow == nil {
objectStore.Status.ServerRecoveryWindow = make(map[string]barmancloudv1.RecoveryWindow) freshObjectStore.Status.ServerRecoveryWindow = make(map[string]barmancloudv1.RecoveryWindow)
} }
objectStore.Status.ServerRecoveryWindow[serverName] = recoveryWindow freshObjectStore.Status.ServerRecoveryWindow[serverName] = recoveryWindow
return c.Status().Update(ctx, objectStore) return c.Status().Update(ctx, &freshObjectStore)
})
} }
// setLastFailedBackupTime sets the last failed backup time in the // setLastFailedBackupTime sets the last failed backup time in the