You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Applies().Update was last-writer-wins: a caller holding a stale in-memory
snapshot could write an active state over a terminal row, resurrecting a
completed, failed, or stopped apply. Update now refuses terminal-to-active
transitions in the WHERE clause and surfaces the refusal as a distinct
ErrApplyTerminalStateImmutable, resolved from the ambiguous zero-rows
result by a read-committed re-read of the row.
Terminal-to-terminal writes (including same-state refreshes) stay allowed;
a settled apply re-enters the active lifecycle only through the dedicated
guarded transition of claiming a stopped apply. The stop-before-start
normalization in the API layer now reloads the row and proceeds from the
newer verdict instead of overwriting it, and guard errors surface the
apply identifier rather than the internal row ID.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
returnfmt.Errorf("reload apply %s after concurrent state change before start: %w", apply.ApplyIdentifier, err)
1405
+
}
1406
+
iffresh==nil {
1407
+
returnfmt.Errorf("reload apply %s after concurrent state change before start: %w", apply.ApplyIdentifier, storage.ErrApplyNotFound)
1408
+
}
1409
+
*apply=*fresh
1410
+
s.logger.Info("stored apply advanced while checking remote state; leaving pending stop request for the current owner and starting from the reloaded state",
// The re-read must observe the latest committed row, not the transaction's
475
+
// repeatable-read snapshot: a concurrent terminal write committed after the
476
+
// snapshot is exactly what the guard refused. FOR UPDATE reads current data
477
+
// on the row lock the guarded UPDATE already examined.
478
+
varcurrentStatestring
479
+
err:=db.QueryRowContext(ctx, `SELECT state FROM applies WHERE id = ? FOR UPDATE`, apply.ID).Scan(¤tState)
480
+
iferrors.Is(err, sql.ErrNoRows) {
481
+
returnfmt.Errorf("apply %s no longer exists for update to state %s: %w", apply.ApplyIdentifier, apply.State, storage.ErrApplyNotFound)
482
+
}
483
+
iferr!=nil {
484
+
returnfmt.Errorf("re-read apply %s after guarded update to state %s: %w", apply.ApplyIdentifier, apply.State, err)
485
+
}
486
+
ifstate.IsTerminalApplyState(currentState) {
487
+
returnfmt.Errorf("apply %s is %s; update to active state %s refused: %w", apply.ApplyIdentifier, currentState, apply.State, storage.ErrApplyTerminalStateImmutable)
488
+
}
489
+
returnnil
490
+
}
491
+
465
492
// confirmLeaseOnZeroRows fails closed when a lease-scoped write changed no rows.
466
493
// Zero rows is ambiguous: either a legitimate idempotent no-op (the lease is
467
494
// still valid) or the lease token no longer matches because ownership was lost.
0 commit comments