carlos / platform Public

console: a staged start or restage confirms a write whose reply was lost

Fix round 1, minor 3: offlineAdvanceBound, like offlineBegin, reads the
record once after a failed bound write; a record carrying this
transition's move, state and proposed stamp is the write that landed and
answers 202. Anything else keeps today's answers (the 409 for a lost race,
the write failure otherwise). The read confirms and never retries.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 9eb6c6230e35c96d330842b616581b49f947407c parent 27164dc
1 file changed, +24 −0
  • internal/console/apiinstancemove_offline.go +24 −0
diff --git a/internal/console/apiinstancemove_offline.go b/internal/console/apiinstancemove_offline.go
index 9a775ae7d..57c63db46 100644
--- a/internal/console/apiinstancemove_offline.go
+++ b/internal/console/apiinstancemove_offline.go
@@ -810,6 +810,17 @@ func (s *Server) offlineAdvanceBound(w http.ResponseWriter, r *http.Request, d o
return
}
rec, err := s.offlineApplyBound(ctx, d, m)
+ if err != nil {
+ // A failed write does not prove nothing was written (a commit whose
+ // reply was lost, surfacing as a timeout or as the 412 of the
+ // store's own retry): the record carrying this transition's stamp
+ // is the write that landed. This read confirms; it never retries.
+ rctx, rcancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
+ defer rcancel()
+ if cur, _, rerr := s.readRecord(rctx, d.acct.Sqid, d.app, d.host); rerr == nil && offlineLanded(cur, &probe, id) {
+ rec, err = cur, nil
+ }
+ }
if errors.Is(err, release.ErrCASConflict) {
jsonError(w, http.StatusConflict, offlineChangedMsg)
return
@@ -822,6 +833,19 @@ func (s *Server) offlineAdvanceBound(w http.ResponseWriter, r *http.Request, d o
writeJSON(w, http.StatusAccepted, s.offlineViewOf(ctx, rec))
}
+// offlineLanded reports whether cur carries the transition that turned the
+// observed record into want: the same move, in want's state, at want's
+// stamps (requested_at for a staged start, staged_at for a restage). The
+// stamp is proposed from the observation and is after every earlier stamp
+// on the record (provision.ProposeStamp), so no other write carries it.
+func offlineLanded(cur, want *provision.Record, id string) bool {
+ if cur == nil || cur.Move == nil || want.Move == nil || cur.Move.ID != id || cur.Move.State != want.Move.State {
+ return false
+ }
+ same := func(a, b *time.Time) bool { return (a == nil) == (b == nil) && (a == nil || a.Equal(*b)) }
+ return cur.Move.RequestedAt.Equal(want.Move.RequestedAt) && same(cur.Move.StagedAt, want.Move.StagedAt)
+}
+
// offlineStartWrite bounds the start door's record write.
const offlineStartWrite = 30 * time.Second