carlos / platform Public

provision: SetRecordMoveAt, the write bound to the record its caller observed

Expect{Incarnation, Seq} names the record a write was decided on.
SetRecordMoveAt reads the record (judged through JudgeFrom(ctx) when the
context carries a judge), refuses with ErrCASConflict before the mutation
runs when the incarnation or record_seq differs, and otherwise makes one
WriteRecord against the read's ETag: a lost race is ErrCASConflict, with no
fresh read and no retry. It shares updateRecordSeeding's read and write
(an expect parameter, nil for every other caller), so the sequence
discipline and the judged read are the same code. SetRecordMove and its
retry stay for their callers.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 3852ccc1a42b54eb19480bce4f5a8de7bbb07a6b parent 7ceefea
2 files changed, +38 −3
  • internal/provision/provision.go +37 −2
  • internal/provision/seed.go +1 −1
diff --git a/internal/provision/provision.go b/internal/provision/provision.go
index 9f687a486..b384a7ac8 100644
--- a/internal/provision/provision.go
+++ b/internal/provision/provision.go
@@ -948,7 +948,7 @@ func SetRecordRegion(ctx context.Context, s release.Store, acct, app, host, regi
// re-applying a narrow mutation on a lost race changes nothing about what
// it meant. Returns the record as written.
func updateRecord(ctx context.Context, s release.Store, acct, app, host string, mut func(*Record) error) (*Record, error) {
- return updateRecordSeeding(ctx, s, acct, app, host, mut, false)
+ return updateRecordSeeding(ctx, s, acct, app, host, mut, false, nil)
}
type lifetimeJudgeKey struct{}
@@ -981,7 +981,11 @@ func WithLifetimes(ctx context.Context, j *LifetimeJudge) context.Context {
// updateRecordSeeding is updateRecord's scaffold. seedWrite is true only for
// the seed transitions: every other caller is refused a change to Seed.
-func updateRecordSeeding(ctx context.Context, s release.Store, acct, app, host string, mut func(*Record) error, seedWrite bool) (*Record, error) {
+// expect, when not nil, binds the write to the record its caller observed
+// (SetRecordMoveAt): a read whose incarnation or record_seq differs is
+// ErrCASConflict before the mutation runs, and there is exactly one attempt
+// (no retry on a lost race, no wait for a migration).
+func updateRecordSeeding(ctx context.Context, s release.Store, acct, app, host string, mut func(*Record) error, seedWrite bool, expect *Expect) (*Record, error) {
key := RecordKey(acct, app, host)
update := func() (*Record, error) {
var raw []byte
@@ -1008,6 +1012,9 @@ func updateRecordSeeding(ctx context.Context, s release.Store, acct, app, host s
return nil, ErrMaintenanceProjection
}
}
+ if expect != nil && (rec.Incarnation != expect.Incarnation || rec.RecordSeq != expect.Seq) {
+ return nil, fmt.Errorf("provision: record %s: incarnation %d record_seq %d, expected %d and %d: %w", key, rec.Incarnation, rec.RecordSeq, expect.Incarnation, expect.Seq, release.ErrCASConflict)
+ }
before := rec
before.Maintenance = append(MaintenanceProjection(nil), rec.Maintenance...)
before.Seed = cloneSeed(rec.Seed) // deep: mut edits rec.Seed through its pointer
@@ -1034,6 +1041,9 @@ func updateRecordSeeding(ctx context.Context, s release.Store, acct, app, host s
rec.RecordSeq = seq
return &rec, nil
}
+ if expect != nil {
+ return update()
+ }
// One retry on a lost race; a second loss surfaces. A record a migration
// has marked refuses the write until the migration's final write: it is
// read again every poll, up to the wait, without spending that retry.
@@ -1422,6 +1432,31 @@ func SetRecordMove(ctx context.Context, s release.Store, acct, app, host string,
})
}
+// Expect is the record a bound write was decided on: its incarnation and
+// its record_seq as the caller observed them.
+type Expect struct{ Incarnation, Seq uint64 }
+
+// SetRecordMoveAt is SetRecordMove bound to the record its caller observed:
+// it reads the record (judged through JudgeFrom(ctx) when the context
+// carries a judge), refuses with ErrCASConflict and writes nothing when the
+// read's incarnation or record_seq is not expect's, and otherwise makes
+// exactly one WriteRecord against the read's ETag. A lost race is
+// ErrCASConflict too; there is no fresh read and no retry, so the mutation
+// only ever applies to the record the caller decided on (the self-serve
+// job's persisted request, an operator door's observation). A mutation
+// returning errRecordUnchanged writes nothing, as under SetRecordMove.
+func SetRecordMoveAt(ctx context.Context, s release.Store, acct, app, host string, expect Expect, mut func(*Record) error) (*Record, error) {
+ return updateRecordSeeding(ctx, s, acct, app, host, func(rec *Record) error {
+ if err := mut(rec); err != nil {
+ return err
+ }
+ if rec.Move != nil {
+ rec.Move.UpdatedAt = time.Now().UTC()
+ }
+ return nil
+ }, false, &expect)
+}
+
// The five transitions, each a mut for SetRecordMove. They are the ONLY
// writers of Record.Move.
diff --git a/internal/provision/seed.go b/internal/provision/seed.go
index e8276fb5c..919a9f796 100644
--- a/internal/provision/seed.go
+++ b/internal/provision/seed.go
@@ -125,7 +125,7 @@ var ErrSeedConflict = errors.New("provision: seed transition refused")
// etag, retrying once on a lost race (updateRecord's scaffold). It is the
// only writer of Record.Seed after creation.
func SetRecordSeed(ctx context.Context, s release.Store, acct, app, host string, mut func(*Record) error) (*Record, error) {
- return updateRecordSeeding(ctx, s, acct, app, host, mut, true)
+ return updateRecordSeeding(ctx, s, acct, app, host, mut, true, nil)
}
// seedIn checks one row of the transition table: the record's seed must be