@@ -18,6 +18,7 @@ import (
1818 "github.com/argoproj/argo-workflows/v4/config"
1919 wfv1 "github.com/argoproj/argo-workflows/v4/pkg/apis/workflow/v1alpha1"
2020 "github.com/argoproj/argo-workflows/v4/util/logging"
21+ "github.com/argoproj/argo-workflows/v4/util/sqldb"
2122 syncdb "github.com/argoproj/argo-workflows/v4/util/sync/db"
2223)
2324
@@ -465,18 +466,16 @@ func (sm *Manager) TryAcquire(ctx context.Context, wf *wfv1.Workflow, nodeName s
465466 var updated bool
466467 var already bool
467468 var msg string
468- // Backoff bounds: sm.lock is held for the whole loop, so cap each sleep
469- // modestly. Jitter prevents a fleet of replicas from retrying in lockstep
470- // after a shared conflict burst.
471- backoff := wait.Backoff {
472- Steps : 5 ,
473- Duration : 10 * time .Millisecond ,
474- Factor : 2.0 ,
475- Jitter : 0.5 ,
476- Cap : 600 * time .Millisecond ,
477- }
469+ backoff := dbRetryBackoff
470+ // tryAcquireImpl mutates wf.Status.Synchronization in memory before the
471+ // transaction commits. Snapshot it and roll each failed attempt back, so
472+ // that attempts are independent and an error return leaves the caller's
473+ // status exactly as it was - otherwise an abort leaves a Holding entry
474+ // for a row the database rolled back, which would be persisted and then
475+ // failed as a stale hold on the next controller restart.
476+ syncStatus := wf .Status .Synchronization .DeepCopy ()
478477 attempt := 0
479- err = retry .OnError (backoff , isRetryableSyncError , func () error {
478+ err = retry .OnError (backoff , IsRetryableSyncError , func () error {
480479 attempt ++
481480 sm .log .WithFields (logging.Fields {
482481 "holderKey" : holderKey ,
@@ -489,11 +488,12 @@ func (sm *Manager) TryAcquire(ctx context.Context, wf *wfv1.Workflow, nodeName s
489488 return implErr
490489 }, & sql.TxOptions {Isolation : sql .LevelSerializable , ReadOnly : false })
491490 if txErr != nil {
491+ wf .Status .Synchronization = syncStatus .DeepCopy ()
492492 sm .log .WithFields (logging.Fields {
493493 "holderKey" : holderKey ,
494494 "attempt" : attempt ,
495495 "error" : txErr ,
496- "retryable" : isRetryableSyncError (txErr ),
496+ "retryable" : IsRetryableSyncError (txErr ),
497497 }).Info (ctx , "TryAcquire - transaction failed" )
498498 }
499499 return txErr
@@ -506,14 +506,32 @@ func (sm *Manager) TryAcquire(ctx context.Context, wf *wfv1.Workflow, nodeName s
506506 return sm .tryAcquireImpl (ctx , wf , nil , holderKey , failedLockName , syncItems , lockKeys )
507507}
508508
509- // isRetryableSyncError reports whether a TryAcquire transaction failure should
510- // be retried. Matches PostgreSQL SERIALIZABLE conflict (40001), deadlock
511- // (40P01), and explicit rollback messages by substring against the driver's
512- // text.
513- func isRetryableSyncError (err error ) bool {
509+ // dbRetryBackoff bounds the in-place retries of the TryAcquire transaction.
510+ // sm.lock is held for the whole loop, so each sleep is capped modestly. Jitter
511+ // prevents a fleet of replicas from retrying in lockstep after a shared
512+ // conflict burst. An error that survives these retries is surfaced to the
513+ // caller, which classifies it (sqldb.IsSerializationConflict, IsTransientErr)
514+ // and requeues the workflow rather than failing it.
515+ var dbRetryBackoff = wait.Backoff {
516+ Steps : 5 ,
517+ Duration : 10 * time .Millisecond ,
518+ Factor : 2.0 ,
519+ Jitter : 0.5 ,
520+ Cap : 600 * time .Millisecond ,
521+ }
522+
523+ // IsRetryableSyncError reports whether a TryAcquire transaction failure should
524+ // be retried. A conflict reported by the database driver itself is always
525+ // retryable; the substring match against the driver's text (serialization
526+ // conflicts, deadlocks, explicit rollbacks) remains as a fallback for errors
527+ // that arrive with the driver type stringified away by an intermediate layer.
528+ func IsRetryableSyncError (err error ) bool {
514529 if err == nil {
515530 return false
516531 }
532+ if sqldb .IsSerializationConflict (err ) {
533+ return true
534+ }
517535 s := strings .ToLower (err .Error ())
518536 return strings .Contains (s , "serialization" ) ||
519537 strings .Contains (s , "dependencies" ) ||
0 commit comments