| 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. |
| |