console: every instance-record read goes through a judged wrapper that takes its judge from the context
readRecord, observeRecord and the five listings read under provision.JudgeFrom(ctx), so a unit admitted on is judged and a before or claims-on unit reads as today. placeSeedBlock and importInFlight take the judge as a parameter.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
27 files changed,
+108
−51
internal/console/apex.go+1 −2internal/console/apidb.go+1 −1internal/console/apidomains.go+1 −1internal/console/apiinstanceimport.go+3 −2internal/console/apiinstancemove.go+4 −4internal/console/apiinstancemove_offline.go+2 −2internal/console/apiinstances.go+5 −5internal/console/apiinstanceseed.go+1 −1internal/console/apilegacyroutes.go+5 −4internal/console/apiretry.go+1 −1internal/console/apiruns.go+2 −2internal/console/apirunsfollow.go+1 −1internal/console/appsettings.go+2 −2internal/console/channelmove.go+2 −2internal/console/domainattach.go+1 −1internal/console/fleets.go+1 −1internal/console/lifetimes_create.go+4 −2internal/console/lifetimes_read.go+56 −0internal/console/movesrecovery.go+1 −1internal/console/regiondrift.go+1 −3internal/console/relayops.go+1 −2internal/console/runsweep.go+1 −1internal/console/seedfinish.go+3 −3internal/console/seedmove.go+5 −4internal/console/seedsweep.go+1 −1internal/console/server.go+1 −1internal/console/updatescore.go+1 −1
diff --git a/internal/console/apex.go b/internal/console/apex.go| index daea6a80f..a03681a62 100644 |
| --- a/internal/console/apex.go |
| +++ b/internal/console/apex.go |
| @@ -20,7 +20,6 @@ import ( |
| "strings" |
| "github.com/carlosframework/platform/internal/domains" |
| - "github.com/carlosframework/platform/internal/provision" |
| ) |
| // apexHost is the platform apex app would have in the account with this |
| @@ -150,7 +149,7 @@ func (s *Server) hostHolder(ctx context.Context, acct *Account, host, own string |
| // Judged when the request was admitted with lifetimes on (a nil |
| // judge is GetRecord): a body its slot no longer vouches for holds |
| // nothing. |
| - rec, _, err := provision.ReadRecordJudged(ctx, s.cfg.Releases, provision.JudgeFrom(ctx), acct.Sqid, c.app, host) |
| + rec, _, err := s.readRecord(ctx, acct.Sqid, c.app, host) |
| if errors.Is(err, os.ErrNotExist) { |
| continue |
| } |
diff --git a/internal/console/apidb.go b/internal/console/apidb.go| index f1121beee..f0a615b1b 100644 |
| --- a/internal/console/apidb.go |
| +++ b/internal/console/apidb.go |
| @@ -202,7 +202,7 @@ func (s *Server) resolveDBSource(ctx context.Context, acct *Account, actor, app, |
| if !provision.ValidHostShape(host) { |
| return dbSource{}, fmt.Errorf("%w: %q", ErrBadHost, host) |
| } |
| - rec, err := provision.GetRecord(ctx, s.cfg.Releases, acct.Sqid, app, host) |
| + rec, _, err := s.readRecord(ctx, acct.Sqid, app, host) |
| if err != nil { |
| return dbSource{}, err // os.ErrNotExist passes through as itself |
| } |
diff --git a/internal/console/apidomains.go b/internal/console/apidomains.go| index a17674e4a..7073eec26 100644 |
| --- a/internal/console/apidomains.go |
| +++ b/internal/console/apidomains.go |
| @@ -212,7 +212,7 @@ func (s *Server) resolveAliasRoute(ctx context.Context, w http.ResponseWriter, a |
| } |
| return route, true |
| } |
| - recs, _, err := provision.ListRecords(ctx, s.cfg.Releases, acct.Sqid, app, instanceListMax) |
| + recs, _, err := s.listRecords(ctx, acct.Sqid, app, instanceListMax) |
| if err != nil { |
| log.Printf("console: api domains route default %s/%s: %v", acct.Sqid, app, err) |
| jsonError(w, http.StatusBadGateway, "could not read this app's instances") |
diff --git a/internal/console/apiinstanceimport.go b/internal/console/apiinstanceimport.go| index 7906e1e1b..d1c54fa99 100644 |
| --- a/internal/console/apiinstanceimport.go |
| +++ b/internal/console/apiinstanceimport.go |
| @@ -309,7 +309,7 @@ func (s *Server) importGate(w http.ResponseWriter, r *http.Request) (principal s |
| } |
| ctx, cancel := context.WithTimeout(r.Context(), instanceStoreTimeout) |
| defer cancel() |
| - rec, err := provision.GetRecord(ctx, s.cfg.Releases, acct.Sqid, app, host) |
| + rec, _, err := s.readRecord(ctx, acct.Sqid, app, host) |
| if errors.Is(err, os.ErrNotExist) { |
| jsonError(w, http.StatusNotFound, notInstance) |
| return "", nil, "", "", nil, false |
| @@ -455,7 +455,8 @@ func (s *Server) handleAPIImportUpload(w http.ResponseWriter, r *http.Request) { |
| func (s *Server) readImportRecord(ctx context.Context, sqid, app, host string) (*provision.Record, error) { |
| ctx, cancel := context.WithTimeout(ctx, instanceStoreTimeout) |
| defer cancel() |
| - return provision.GetRecord(ctx, s.cfg.Releases, sqid, app, host) |
| + rec, _, err := s.readRecord(ctx, sqid, app, host) |
| + return rec, err |
| } |
| // awaitingUpload reports whether rec's seed is the import id, still |
diff --git a/internal/console/apiinstancemove.go b/internal/console/apiinstancemove.go| index a8c747fb2..2ac1ca6b4 100644 |
| --- a/internal/console/apiinstancemove.go |
| +++ b/internal/console/apiinstancemove.go |
| @@ -61,7 +61,7 @@ func (s *Server) moveGate(w http.ResponseWriter, r *http.Request) (acct *Account |
| return nil, "", "", nil, false |
| } |
| ctx := r.Context() |
| - rec, err := provision.GetRecord(ctx, s.cfg.Releases, acct.Sqid, app, host) |
| + rec, _, err := s.readRecord(ctx, acct.Sqid, app, host) |
| if errors.Is(err, os.ErrNotExist) { |
| http.NotFound(w, r) |
| return nil, "", "", nil, false |
| @@ -975,7 +975,7 @@ func (s *Server) settleFailedMoveWrite(w http.ResponseWriter, r *http.Request, c |
| log.Printf("console: api instance move %s/%s/%s: move %s: %s: %v; the claim is left moving", acct.Sqid, app, host, id, why, err) |
| jsonError(w, http.StatusBadGateway, "could not confirm whether the move was recorded. read its state before trying again") |
| } |
| - rec, err := provision.GetRecord(ctx, s.cfg.Releases, acct.Sqid, app, host) |
| + rec, _, err := s.readRecord(ctx, acct.Sqid, app, host) |
| if err != nil { |
| unknown("record reread", err) |
| return nil |
| @@ -1207,7 +1207,7 @@ func (s *Server) claimMoveLoss(w http.ResponseWriter, r *http.Request, acct *Acc |
| return |
| } |
| s.bumpSerial(ctx) |
| - got, err := provision.GetRecord(ctx, s.cfg.Releases, acct.Sqid, app, host) |
| + got, _, err := s.readRecord(ctx, acct.Sqid, app, host) |
| if err != nil { |
| log.Printf("console: api instance move %s/%s/%s: declared loss: record reread: %v", acct.Sqid, app, host, err) |
| jsonError(w, http.StatusBadGateway, "could not read the instance") |
| @@ -1710,7 +1710,7 @@ func (s *Server) refuseDeleteDuringMove(ctx context.Context, w http.ResponseWrit |
| if !s.cfg.MovesEnabled { |
| return false |
| } |
| - rec, err := provision.GetRecord(ctx, s.cfg.Releases, acct, app, host) |
| + rec, _, err := s.readRecord(ctx, acct, app, host) |
| if errors.Is(err, os.ErrNotExist) { |
| return false |
| } |
diff --git a/internal/console/apiinstancemove_offline.go b/internal/console/apiinstancemove_offline.go| index dc51ef24d..103ac3927 100644 |
| --- a/internal/console/apiinstancemove_offline.go |
| +++ b/internal/console/apiinstancemove_offline.go |
| @@ -731,7 +731,7 @@ func (s *Server) offlineBegin(w http.ResponseWriter, r *http.Request, d offlineD |
| // lock goes stale and frees itself. |
| rctx, rcancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second) |
| defer rcancel() |
| - if cur, rerr := provision.GetRecord(rctx, s.cfg.Releases, d.acct.Sqid, d.app, host); rerr == nil && cur.Move.Offline() && cur.Move.ID == id { |
| + if cur, _, rerr := s.readRecord(rctx, d.acct.Sqid, d.app, host); rerr == nil && cur.Move.Offline() && cur.Move.ID == id { |
| got, err = cur, nil |
| } |
| } |
| @@ -823,7 +823,7 @@ func (s *Server) offlineTakeLock(ctx context.Context, d offlineDoor, id string) |
| if time.Since(held.At) < offlineLockGrace { |
| return busy |
| } |
| - other, err := provision.GetRecord(ctx, s.cfg.Releases, held.Account, held.App, held.Host) |
| + other, _, err := s.readRecord(ctx, held.Account, held.App, held.Host) |
| switch { |
| case errors.Is(err, os.ErrNotExist): |
| case err != nil: |
diff --git a/internal/console/apiinstances.go b/internal/console/apiinstances.go| index 65efe8c61..52eea7e84 100644 |
| --- a/internal/console/apiinstances.go |
| +++ b/internal/console/apiinstances.go |
| @@ -753,7 +753,7 @@ func (s *Server) handleAPIInstanceList(w http.ResponseWriter, r *http.Request) { |
| defer cancel() |
| // Judged when the request was admitted with lifetimes on: a record its |
| // slot no longer vouches for is not listed. Unjudged (nil) as main. |
| - recs, truncated, skipped, err := provision.ListRecordsCompleteJudged(ctx, s.cfg.Releases, provision.JudgeFrom(ctx), acct.Sqid, app, instanceListMax) |
| + recs, truncated, skipped, err := s.listRecordsComplete(ctx, acct.Sqid, app, instanceListMax) |
| if err != nil { |
| log.Printf("console: api instances list %s/%s: %v", acct.Sqid, app, err) |
| jsonError(w, http.StatusBadGateway, "could not list instances") |
| @@ -877,7 +877,7 @@ func (s *Server) handleAPIInstanceGet(w http.ResponseWriter, r *http.Request) { |
| defer cancel() |
| // Judged when the request was admitted with lifetimes on: a stale body |
| // reads as absent. Unjudged (nil) as main. |
| - rec, _, err := provision.ReadRecordJudged(ctx, s.cfg.Releases, provision.JudgeFrom(ctx), acct.Sqid, app, host) |
| + rec, _, err := s.readRecord(ctx, acct.Sqid, app, host) |
| if errors.Is(err, os.ErrNotExist) { |
| http.NotFound(w, r) |
| return |
| @@ -1574,7 +1574,7 @@ func (s *Server) handleAPIInstanceCreate(w http.ResponseWriter, r *http.Request) |
| exists: func(ctx context.Context) (bool, error) { |
| // Judged when admitted on (a nil judge is GetRecord): a stale |
| // body is no record, and the create replaces it. |
| - _, _, err := provision.ReadRecordJudged(ctx, s.cfg.Releases, provision.JudgeFrom(ctx), acct.Sqid, app, host) |
| + _, _, err := s.readRecord(ctx, acct.Sqid, app, host) |
| if errors.Is(err, os.ErrNotExist) { |
| return false, nil |
| } |
| @@ -1796,7 +1796,7 @@ func (s *Server) instanceChannel(ctx context.Context, sqid, app string, m *provi |
| if m.Channel != "" { |
| return m.Channel, nil |
| } |
| - recs, _, err := provision.ListRecords(ctx, s.cfg.Releases, sqid, app, instanceListMax) |
| + recs, _, err := s.listRecords(ctx, sqid, app, instanceListMax) |
| if err != nil { |
| log.Printf("console: api instances create %s/%s: list siblings: %v", sqid, app, err) |
| return "", &doorRefusal{http.StatusBadGateway, |
| @@ -2040,7 +2040,7 @@ func (s *Server) handleAPIInstanceUpdate(w http.ResponseWriter, r *http.Request) |
| } |
| ctx, cancel := context.WithTimeout(r.Context(), instanceStoreTimeout) |
| defer cancel() |
| - rec, etag, err := provision.GetRecordETag(ctx, s.cfg.Releases, acct.Sqid, app, host) |
| + rec, etag, err := s.readRecord(ctx, acct.Sqid, app, host) |
| if errors.Is(err, os.ErrNotExist) { |
| http.NotFound(w, r) |
| return |
diff --git a/internal/console/apiinstanceseed.go b/internal/console/apiinstanceseed.go| index b379c455a..60916eca1 100644 |
| --- a/internal/console/apiinstanceseed.go |
| +++ b/internal/console/apiinstanceseed.go |
| @@ -157,7 +157,7 @@ var errSeedEnvironmentUnread = errors.New("console: the instances naming the env |
| // records, or a record that will not decode is an error, since any of them |
| // may name env. |
| func (s *Server) seedEnvironmentUser(ctx context.Context, sqid, app, env, seedID string) (string, error) { |
| - recs, unreadable, err := provision.ListRecordsStrict(ctx, s.cfg.Releases, sqid, app, instanceListMax) |
| + recs, unreadable, err := s.listRecordsStrict(ctx, sqid, app, instanceListMax) |
| if err != nil { |
| return "", err |
| } |
diff --git a/internal/console/apilegacyroutes.go b/internal/console/apilegacyroutes.go| index 8c07e9336..b92a2d7ee 100644 |
| --- a/internal/console/apilegacyroutes.go |
| +++ b/internal/console/apilegacyroutes.go |
| @@ -156,7 +156,7 @@ func assignLegacyRoute(ctx context.Context, rs release.Store, name string, want |
| if cur == want { |
| return false, seeds.LegacyOwner{}, nil |
| } |
| - busy, err := importInFlight(ctx, rs, cur) |
| + busy, err := importInFlight(ctx, rs, provision.JudgeFrom(ctx), cur) |
| if err != nil { |
| return false, seeds.LegacyOwner{}, fmt.Errorf("%w: in-flight check of %s/%s: %w", errLegacyRoutesUnread, cur.Account, cur.App, err) |
| } |
| @@ -178,16 +178,17 @@ func assignLegacyRoute(ctx context.Context, rs release.Store, name string, want |
| // importInFlight reports whether any record of o's account and app holds an |
| // import between ready and committed (ready, swapped or placed; spec §6.4). |
| -// It reads strictly (provision.ListRecordsStrict, preflight M7): a listing |
| +// It reads strictly (provision.ListRecordsStrictJudged under judge, which |
| +// leaves out a record its slot no longer vouches for; preflight M7): a listing |
| // it cannot complete, or one past instanceListMax, is an error, and a record |
| // that reads but will not decode counts as busy, since it could be such an |
| // import. An entry the strict reader let through with a name no record path |
| // can hold is an error too: no listing of it means anything. |
| -func importInFlight(ctx context.Context, rs release.Store, o seeds.LegacyOwner) (bool, error) { |
| +func importInFlight(ctx context.Context, rs release.Store, judge *provision.LifetimeJudge, o seeds.LegacyOwner) (bool, error) { |
| if !controlname.ValidAccount(o.Account) || !release.ValidApp(o.App) { |
| return false, fmt.Errorf("the assignment names %q/%q, which is not an account and app", o.Account, o.App) |
| } |
| - recs, unreadable, err := provision.ListRecordsStrict(ctx, rs, o.Account, o.App, instanceListMax) |
| + recs, unreadable, err := provision.ListRecordsStrictJudged(ctx, rs, judge, o.Account, o.App, instanceListMax) |
| if err != nil { |
| return false, err |
| } |
diff --git a/internal/console/apiretry.go b/internal/console/apiretry.go| index 8eef8b185..96e6d7e4c 100644 |
| --- a/internal/console/apiretry.go |
| +++ b/internal/console/apiretry.go |
| @@ -80,7 +80,7 @@ var ( |
| // book owes a save, and no grant clears anything until it lands |
| // (errRetryUnsaved). |
| func (s *Server) retryHostBuild(ctx context.Context, acct *Account, app, host string) (heldRestriction, error) { |
| - if _, err := provision.GetRecord(ctx, s.cfg.Releases, acct.Sqid, app, host); err != nil { |
| + if _, _, err := s.readRecord(ctx, acct.Sqid, app, host); err != nil { |
| if errors.Is(err, os.ErrNotExist) { |
| return heldRestriction{}, errRetryNoRecord |
| } |
diff --git a/internal/console/apiruns.go b/internal/console/apiruns.go| index 19e6921a2..98754cdd7 100644 |
| --- a/internal/console/apiruns.go |
| +++ b/internal/console/apiruns.go |
| @@ -180,7 +180,7 @@ func (s *Server) handleAPIRunCreate(w http.ResponseWriter, r *http.Request) { |
| jsonError(w, http.StatusNotFound, notInstance) |
| return |
| } |
| - rec, err := provision.GetRecord(ctx, s.cfg.Releases, acct.Sqid, app, host) |
| + rec, _, err := s.readRecord(ctx, acct.Sqid, app, host) |
| if errors.Is(err, os.ErrNotExist) { |
| jsonError(w, http.StatusNotFound, notInstance) |
| return |
| @@ -277,7 +277,7 @@ func (s *Server) handleAPIRunCreate(w http.ResponseWriter, r *http.Request) { |
| if err != nil && !runAdmitRefused(err) { |
| // The outcome is unknown (a lost response, a cancelled request, a |
| // transport error after the write): ask the record whose run it holds. |
| - cur, rerr := provision.GetRecord(wctx, s.cfg.Releases, acct.Sqid, app, host) |
| + cur, _, rerr := s.readRecord(wctx, acct.Sqid, app, host) |
| switch { |
| case rerr != nil: |
| // Still unknown. Keep the objects: if the run was admitted a box |
diff --git a/internal/console/apirunsfollow.go b/internal/console/apirunsfollow.go| index 74620c72c..06c896604 100644 |
| --- a/internal/console/apirunsfollow.go |
| +++ b/internal/console/apirunsfollow.go |
| @@ -64,7 +64,7 @@ func (s *Server) runGate(w http.ResponseWriter, r *http.Request) (runCall, bool) |
| // runOf is the record's run when it is c.id, nil otherwise (the record is |
| // gone, or names another run). A read failure is an error. |
| func (s *Server) runOf(ctx context.Context, c runCall) (*provision.RunState, error) { |
| - rec, err := provision.GetRecord(ctx, s.cfg.Releases, c.acct.Sqid, c.app, c.host) |
| + rec, _, err := s.readRecord(ctx, c.acct.Sqid, c.app, c.host) |
| if errors.Is(err, os.ErrNotExist) { |
| return nil, nil |
| } |
diff --git a/internal/console/appsettings.go b/internal/console/appsettings.go| index 3a0f37384..005739ac9 100644 |
| --- a/internal/console/appsettings.go |
| +++ b/internal/console/appsettings.go |
| @@ -388,7 +388,7 @@ func (s *Server) settingsGates(ctx context.Context, acct *Account, app string) ( |
| if s.cfg.Releases == nil { |
| return nil, "No deployment bucket on this host, so instance records cannot be read." |
| } |
| - recs, _, err := provision.ListRecords(ctx, s.cfg.Releases, acct.Sqid, app, instanceListMax) |
| + recs, _, err := s.listRecords(ctx, acct.Sqid, app, instanceListMax) |
| if err != nil { |
| log.Printf("console: settings gates %s/%s: %v", acct.Sqid, app, err) |
| return nil, "Instance records could not be read just now." |
| @@ -969,7 +969,7 @@ func (s *Server) handleAppGate(w http.ResponseWriter, r *http.Request) { |
| } |
| ctx, cancel := context.WithTimeout(r.Context(), instanceStoreTimeout) |
| defer cancel() |
| - rec, etag, err := provision.GetRecordETag(ctx, s.cfg.Releases, acct.Sqid, app, host) |
| + rec, etag, err := s.readRecord(ctx, acct.Sqid, app, host) |
| if errors.Is(err, os.ErrNotExist) { |
| http.NotFound(w, r) |
| return |
diff --git a/internal/console/channelmove.go b/internal/console/channelmove.go| index be2ec0b3d..c41024a4a 100644 |
| --- a/internal/console/channelmove.go |
| +++ b/internal/console/channelmove.go |
| @@ -123,7 +123,7 @@ func (s *Server) handleAPIInstanceChannel(w http.ResponseWriter, r *http.Request |
| } |
| ctx, cancel := context.WithTimeout(r.Context(), instanceStoreTimeout) |
| defer cancel() |
| - rec, etag, err := provision.GetRecordETag(ctx, s.cfg.Releases, acct.Sqid, app, host) |
| + rec, etag, err := s.readRecord(ctx, acct.Sqid, app, host) |
| switch { |
| case errors.Is(err, os.ErrNotExist): |
| // The family's byte-identical 404: a record's existence is not a |
| @@ -208,7 +208,7 @@ func (s *Server) executePendingRouteChannel(ctx context.Context, acct *Account, |
| if err := json.Unmarshal(pc.Payload, &p); err != nil { |
| return "", errors.New("this proposal's payload cannot be read — reject it and propose again") |
| } |
| - rec, etag, err := provision.GetRecordETag(ctx, s.cfg.Releases, acct.Sqid, app, p.Host) |
| + rec, etag, err := s.readRecord(ctx, acct.Sqid, app, p.Host) |
| if err != nil { |
| log.Printf("console: execute route-channel %s/%s/%s: %v", acct.Sqid, app, p.Host, err) |
| return "", errors.New("could not read the instance") |
diff --git a/internal/console/domainattach.go b/internal/console/domainattach.go| index 6c8e2d536..bb7b7d46b 100644 |
| --- a/internal/console/domainattach.go |
| +++ b/internal/console/domainattach.go |
| @@ -252,7 +252,7 @@ func (s *Server) attachDomainAs(ctx context.Context, acct *Account, app, host, k |
| // re-attach (claim untouched, record standing) stores nothing and |
| // announces nothing. |
| if errors.Is(err, release.ErrCASConflict) { |
| - existing, gerr := provision.GetRecord(ctx, s.cfg.Releases, acct.Sqid, app, host) |
| + existing, _, gerr := s.readRecord(ctx, acct.Sqid, app, host) |
| if gerr == nil && attachWrote(*existing) { |
| if changed { |
| s.auditActivity(acct, app, principal, Activity{ |
diff --git a/internal/console/fleets.go b/internal/console/fleets.go| index dd445ddbc..a9fd2bf77 100644 |
| --- a/internal/console/fleets.go |
| +++ b/internal/console/fleets.go |
| @@ -609,7 +609,7 @@ func (s *Server) placeAppTarget(ctx context.Context, acct *Account, app, target |
| // yet (spec §5.8) refuses it, before any write. A record that cannot be |
| // read refuses it too. SetRecordRegion refuses such a seed again under |
| // its CAS, for a record created after this read. |
| - switch block, err := placeSeedBlock(ctx, s.cfg.Releases, acct.Sqid, app); { |
| + switch block, err := placeSeedBlock(ctx, s.cfg.Releases, provision.JudgeFrom(ctx), acct.Sqid, app); { |
| case err != nil: |
| log.Printf("console: place %s/%s: read instances: %v", acct.Sqid, app, err) |
| return http.StatusBadGateway, placeSeedReadFailed |
diff --git a/internal/console/lifetimes_create.go b/internal/console/lifetimes_create.go| index e88aa2b58..fe87aee6c 100644 |
| --- a/internal/console/lifetimes_create.go |
| +++ b/internal/console/lifetimes_create.go |
| @@ -308,8 +308,10 @@ func (s *Server) allocRecordLocked(ctx context.Context, a Allocation) (*provisio |
| log.Printf("console: lifetimes: allocation %s of %s: write the record: %v (pending)", a.OpID, a.Host, err) |
| return nil, errLifetimePending |
| } |
| - landed, _, _, rerr := provision.GetRecordRaw(ctx, s.cfg.Releases, a.Account, a.App, a.Host) |
| - if rerr != nil || landed.Retired || landed.Incarnation != a.Incarnation { |
| + landed, _, rerr := s.readRecord(ctx, a.Account, a.App, a.Host) |
| + // Judged (a tombstone or a body its slot no longer vouches for reads |
| + // as absent): only this allocation's own lifetime counts as landed. |
| + if rerr != nil || landed.Incarnation != a.Incarnation { |
| log.Printf("console: lifetimes: allocation %s of %s: the record write lost to another body (%v; pending)", a.OpID, a.Host, rerr) |
| return nil, errLifetimePending |
| } |
diff --git a/internal/console/lifetimes_read.go b/internal/console/lifetimes_read.go| new file mode 100644 |
| index 000000000..8b62d57d6 |
| --- /dev/null |
| +++ b/internal/console/lifetimes_read.go |
| @@ -0,0 +1,56 @@ |
| +package console |
| + |
| +import ( |
| + "context" |
| + |
| + "github.com/carlosframework/platform/internal/provision" |
| +) |
| + |
| +// The console's instance-record readers (self-serve moves plan 1, Task 6). |
| +// Every instance-record read in the console goes through one of these, and |
| +// each takes its judge from the context: recordCtx installs s.judge on a unit |
| +// admitted on, and nothing on a unit admitted before or on a host-claim |
| +// console. So the cutover gate is the one place that decides judging. Under a |
| +// judge a record its slot no longer vouches for reads as absent; with no |
| +// judge each wrapper is today's unjudged reader exactly. |
| +// |
| +// TestNoUnjudgedRecordReads (lifetimes_audit_test.go) holds the console, host |
| +// and provision packages to this. |
| + |
| +// readRecord is GetRecordETag under the context's judge: a stale record, a |
| +// tombstone or an absent one is os.ErrNotExist, a corrupt body |
| +// provision.ErrRecordCorrupt, and a read that could not be judged its error. |
| +func (s *Server) readRecord(ctx context.Context, acct, app, host string) (*provision.Record, string, error) { |
| + return provision.ReadRecordJudged(ctx, s.cfg.Releases, provision.JudgeFrom(ctx), acct, app, host) |
| +} |
| + |
| +// observeRecord is one observation of acct/app/host's record under the |
| +// context's judge, for a caller that acts on the exact bytes it read. |
| +func (s *Server) observeRecord(ctx context.Context, acct, app, host string) provision.Observation { |
| + return provision.JudgeFrom(ctx).Observe(ctx, s.cfg.Releases, acct, app, host) |
| +} |
| + |
| +// listRecords is ListRecords under the context's judge. |
| +func (s *Server) listRecords(ctx context.Context, acct, app string, max int) ([]provision.Record, bool, error) { |
| + return provision.ListRecordsJudged(ctx, s.cfg.Releases, provision.JudgeFrom(ctx), acct, app, max) |
| +} |
| + |
| +// listRecordsStrict is ListRecordsStrict under the context's judge. |
| +func (s *Server) listRecordsStrict(ctx context.Context, acct, app string, max int) ([]provision.Record, map[string]bool, error) { |
| + return provision.ListRecordsStrictJudged(ctx, s.cfg.Releases, provision.JudgeFrom(ctx), acct, app, max) |
| +} |
| + |
| +// listRecordsComplete is ListRecordsComplete under the context's judge. |
| +func (s *Server) listRecordsComplete(ctx context.Context, acct, app string, max int) ([]provision.Record, bool, []string, error) { |
| + return provision.ListRecordsCompleteJudged(ctx, s.cfg.Releases, provision.JudgeFrom(ctx), acct, app, max) |
| +} |
| + |
| +// listAllRecordsStrict is ListAllRecordsStrict under the context's judge. |
| +func (s *Server) listAllRecordsStrict(ctx context.Context) ([]provision.Record, map[string]bool, error) { |
| + return provision.ListAllRecordsStrictJudged(ctx, s.cfg.Releases, provision.JudgeFrom(ctx)) |
| +} |
| + |
| +// listAllRecordsTolerant is ListAllRecordsTolerant under the context's judge. |
| +func (s *Server) listAllRecordsTolerant(ctx context.Context) ([]provision.Record, bool, error) { |
| + return provision.ListAllRecordsTolerantJudged(ctx, s.cfg.Releases, provision.JudgeFrom(ctx)) |
| +} |
diff --git a/internal/console/movesrecovery.go b/internal/console/movesrecovery.go| index 3527f3d71..2a4512f20 100644 |
| --- a/internal/console/movesrecovery.go |
| +++ b/internal/console/movesrecovery.go |
| @@ -791,7 +791,7 @@ func (s *Server) declareMoveLossLogged(ctx context.Context, account, app, host s |
| t moves.Transition |
| logged bool |
| ) |
| - rec, err := provision.GetRecord(ctx, s.cfg.Releases, account, app, host) |
| + rec, _, err := s.readRecord(ctx, account, app, host) |
| if err != nil { |
| // Fail closed (final re-review 1, I4): a declaration whose entry |
| // cannot be queued is not made. |
diff --git a/internal/console/regiondrift.go b/internal/console/regiondrift.go| index 6fd103735..8e2d2d372 100644 |
| --- a/internal/console/regiondrift.go |
| +++ b/internal/console/regiondrift.go |
| @@ -4,8 +4,6 @@ import ( |
| "context" |
| "log" |
| "time" |
| - |
| - "github.com/carlosframework/platform/internal/provision" |
| ) |
| // discrepancyScanCap bounds one app's record listing. ListRecords treats |
| @@ -47,7 +45,7 @@ func (s *Server) logPlacementDiscrepancies(ctx context.Context, scanCap int) { |
| return |
| } |
| for _, a := range apps { |
| - recs, truncated, err := provision.ListRecords(ctx, s.cfg.Releases, a.Sqid, a.App, scanCap) |
| + recs, truncated, err := s.listRecords(ctx, a.Sqid, a.App, scanCap) |
| if err != nil { |
| log.Printf("console: placement discrepancy check %s/%s: %v", a.Sqid, a.App, err) |
| continue |
diff --git a/internal/console/relayops.go b/internal/console/relayops.go| index 2ce983e0f..e17c119f0 100644 |
| --- a/internal/console/relayops.go |
| +++ b/internal/console/relayops.go |
| @@ -53,7 +53,6 @@ import ( |
| "time" |
| "github.com/carlosframework/platform/internal/appconfig" |
| - "github.com/carlosframework/platform/internal/provision" |
| "github.com/carlosframework/platform/internal/registry" |
| "github.com/carlosframework/platform/internal/relay" |
| "github.com/carlosframework/platform/internal/release" |
| @@ -802,7 +801,7 @@ type relayRoute struct { |
| // Records first, registry second, deduped on host: a route that exists in |
| // both is one route, and the record is the authority on its shape. |
| func (s *Server) relayAppRoutes(ctx context.Context, acct, app string) ([]relayRoute, error) { |
| - recs, truncated, err := provision.ListRecords(ctx, s.cfg.Releases, acct, app, relayRouteMax) |
| + recs, truncated, err := s.listRecords(ctx, acct, app, relayRouteMax) |
| if err != nil { |
| return nil, err |
| } |
diff --git a/internal/console/runsweep.go b/internal/console/runsweep.go| index 6cf2249c0..5a929f9ff 100644 |
| --- a/internal/console/runsweep.go |
| +++ b/internal/console/runsweep.go |
| @@ -65,7 +65,7 @@ func (s *Server) runSweepPass(ctx context.Context, now time.Time, housekeeping b |
| } |
| ctx, cancel := context.WithTimeout(ctx, runSweepTimeout) |
| defer cancel() |
| - recs, complete, err := provision.ListAllRecordsTolerant(ctx, s.cfg.Releases) |
| + recs, complete, err := s.listAllRecordsTolerant(ctx) |
| if err != nil { |
| log.Printf("console: run sweep: list records: %v", err) |
| return |
diff --git a/internal/console/seedfinish.go b/internal/console/seedfinish.go| index 811e3b6f6..13978059d 100644 |
| --- a/internal/console/seedfinish.go |
| +++ b/internal/console/seedfinish.go |
| @@ -74,7 +74,7 @@ func (s *Server) finishSeed(ctx context.Context, sqid, app, host, seedID string) |
| } |
| func (s *Server) finishSeedOnce(ctx context.Context, sqid, app, host, seedID string) error { |
| - rec, etag, err := provision.GetRecordETag(ctx, s.cfg.Releases, sqid, app, host) |
| + rec, etag, err := s.readRecord(ctx, sqid, app, host) |
| if errors.Is(err, os.ErrNotExist) { |
| return nil |
| } |
| @@ -195,7 +195,7 @@ func (s *Server) seedStoreToTear(ctx context.Context, sqid, app, host, seedID, e |
| // or disagrees with its path, counts as sharing: it is named, with unread |
| // true, since what it says about env is not known. |
| func (s *Server) otherInstanceInEnvironment(ctx context.Context, sqid, app, host, env string) (other string, unread bool, err error) { |
| - recs, unreadable, err := provision.ListRecordsStrict(ctx, s.cfg.Releases, sqid, app, instanceListMax) |
| + recs, unreadable, err := s.listRecordsStrict(ctx, sqid, app, instanceListMax) |
| if errors.Is(err, provision.ErrRecordsTruncated) { |
| return "", false, fmt.Errorf("list the instances of %s/%s: more than %d, so the finish cannot tell who uses environment %q", sqid, app, instanceListMax, env) |
| } |
| @@ -292,7 +292,7 @@ func (s *Server) deleteSeedReplica(ctx context.Context, rec *provision.Record, e |
| // record that names anything else is an error, not "done": the finish only |
| // says done when the seed's own record is gone. |
| func (s *Server) seedRecordETag(ctx context.Context, sqid, app, host, seedID string) (etag string, gone bool, err error) { |
| - rec, etag, err := provision.GetRecordETag(ctx, s.cfg.Releases, sqid, app, host) |
| + rec, etag, err := s.readRecord(ctx, sqid, app, host) |
| switch { |
| case errors.Is(err, os.ErrNotExist): |
| return "", true, nil |
diff --git a/internal/console/seedmove.go b/internal/console/seedmove.go| index c400308d9..f4412dcde 100644 |
| --- a/internal/console/seedmove.go |
| +++ b/internal/console/seedmove.go |
| @@ -33,19 +33,20 @@ const placeSeedReadFailed = "could not update this app's instances. Try again." |
| // placeSeedBlock is carlos apps place's seed rule (preflight M8(a)): a |
| // place re-homes every record of the app, so it is a move of each. It reads |
| -// the app's records strictly and returns the first record's MoveBlock, "" |
| +// the app's records strictly, under judge (a record its slot no longer |
| +// vouches for is not one of them; nil reads as today), and returns the first record's MoveBlock, "" |
| // when none blocks. A record it cannot read could be anything, so it is an |
| // error and the place is refused. A record deleted between the listing and |
| // its read (the race the place's own walk skips) is read again once. |
| // |
| // No cap: the place walks every record of the app, and a cap would refuse |
| // a large app's place for good. |
| -func placeSeedBlock(ctx context.Context, st release.Store, acct, app string) (string, error) { |
| +func placeSeedBlock(ctx context.Context, st release.Store, judge *provision.LifetimeJudge, acct, app string) (string, error) { |
| ctx, cancel := context.WithTimeout(ctx, instanceStoreTimeout) |
| defer cancel() |
| - recs, unreadable, err := provision.ListRecordsStrict(ctx, st, acct, app, math.MaxInt) |
| + recs, unreadable, err := provision.ListRecordsStrictJudged(ctx, st, judge, acct, app, math.MaxInt) |
| if errors.Is(err, os.ErrNotExist) { |
| - recs, unreadable, err = provision.ListRecordsStrict(ctx, st, acct, app, math.MaxInt) |
| + recs, unreadable, err = provision.ListRecordsStrictJudged(ctx, st, judge, acct, app, math.MaxInt) |
| } |
| if err != nil { |
| return "", err |
diff --git a/internal/console/seedsweep.go b/internal/console/seedsweep.go| index 16ff8fe05..f7be2f2de 100644 |
| --- a/internal/console/seedsweep.go |
| +++ b/internal/console/seedsweep.go |
| @@ -154,7 +154,7 @@ func (s *Server) seedsPass(ctx context.Context, now time.Time) { |
| if seedsPassBetweenListings != nil { |
| seedsPassBetweenListings() |
| } |
| - recs, unreadable, err := provision.ListAllRecordsStrict(ctx, s.cfg.Releases) |
| + recs, unreadable, err := s.listAllRecordsStrict(ctx) |
| if err != nil { |
| log.Printf("console: seeds pass: list records: %v (nothing swept this pass)", err) |
| return |
diff --git a/internal/console/server.go b/internal/console/server.go| index fe2d5f9ac..8cb9fb06a 100644 |
| --- a/internal/console/server.go |
| +++ b/internal/console/server.go |
| @@ -3668,7 +3668,7 @@ func (s *Server) handleAppLogs(w http.ResponseWriter, r *http.Request) { |
| } |
| if s.cfg.Releases != nil { |
| ctx, cancel := context.WithTimeout(r.Context(), instanceStoreTimeout) |
| - recs, _, err := provision.ListRecords(ctx, s.cfg.Releases, acct.Sqid, p.AppName, instanceListMax) |
| + recs, _, err := s.listRecords(ctx, acct.Sqid, p.AppName, instanceListMax) |
| cancel() |
| if err == nil { |
| for _, rec := range recs { |
diff --git a/internal/console/updatescore.go b/internal/console/updatescore.go| index 79cba76ff..e867968c7 100644 |
| --- a/internal/console/updatescore.go |
| +++ b/internal/console/updatescore.go |
| @@ -39,7 +39,7 @@ func answerUpdates(w http.ResponseWriter, r *http.Request, status int, msg strin |
| // readUpdatesTarget reads the record and the app policy for a settings door. |
| // status is 0 on success; a host with no record is 404 with no message. |
| func (s *Server) readUpdatesTarget(ctx context.Context, acct *Account, app, host string) (*provision.Record, string, *provision.UpdatePolicy, int, string) { |
| - rec, etag, err := provision.GetRecordETag(ctx, s.cfg.Releases, acct.Sqid, app, host) |
| + rec, etag, err := s.readRecord(ctx, acct.Sqid, app, host) |
| switch { |
| case errors.Is(err, os.ErrNotExist): |
| return nil, "", nil, http.StatusNotFound, "" |