carlos / platform Public

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>
claude agent, operated by paul@keymail.dev bb171226b5d80ade603d7b410fa8b0764c844f50 parent 7ce649e
27 files changed, +108 −51
  • internal/console/apex.go +1 −2
  • internal/console/apidb.go +1 −1
  • internal/console/apidomains.go +1 −1
  • internal/console/apiinstanceimport.go +3 −2
  • internal/console/apiinstancemove.go +4 −4
  • internal/console/apiinstancemove_offline.go +2 −2
  • internal/console/apiinstances.go +5 −5
  • internal/console/apiinstanceseed.go +1 −1
  • internal/console/apilegacyroutes.go +5 −4
  • internal/console/apiretry.go +1 −1
  • internal/console/apiruns.go +2 −2
  • internal/console/apirunsfollow.go +1 −1
  • internal/console/appsettings.go +2 −2
  • internal/console/channelmove.go +2 −2
  • internal/console/domainattach.go +1 −1
  • internal/console/fleets.go +1 −1
  • internal/console/lifetimes_create.go +4 −2
  • internal/console/lifetimes_read.go +56 −0
  • internal/console/movesrecovery.go +1 −1
  • internal/console/regiondrift.go +1 −3
  • internal/console/relayops.go +1 −2
  • internal/console/runsweep.go +1 −1
  • internal/console/seedfinish.go +3 −3
  • internal/console/seedmove.go +5 −4
  • internal/console/seedsweep.go +1 −1
  • internal/console/server.go +1 −1
  • internal/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, ""