provision: prefetch record reads a chunk at a time
Astra review of 20c5ffd5 (accept with fixes): - P2: prefetching every body before judging held the whole record set in memory. The judging loops now prefetch and judge 64 keys at a time, in the same order as before. - P2: ListAllRecords read every record before reporting the first one that failed. It now stops within a chunk of it. A cancelled context reads nothing, and each key reports ctx.Err() as its read failure. - P3: the tests now bound concurrency at eight, count one read per record, mint a fresh record in a pass where another record's read fails, require the error to name the failing record, and cover the chunk bound and cancellation. The loop bodies are re-indented by the added chunk loop; their statements are unchanged apart from reading the body from the chunk.
Agent notes
Mutants checked: one unbounded chunk, no cancellation check, 64 concurrent readers. Each fails the test written for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 files changed,
+301
−202
internal/provision/parallelreads_test.go+78 −5internal/provision/reconcile.go+136 −109internal/provision/sidecar_reconcile.go+87 −88
CI passed — run details
diff --git a/internal/provision/parallelreads_test.go b/internal/provision/parallelreads_test.go| index 07b98022f..199ef3793 100644 |
| --- a/internal/provision/parallelreads_test.go |
| +++ b/internal/provision/parallelreads_test.go |
| @@ -3,6 +3,7 @@ package provision |
| import ( |
| "context" |
| "errors" |
| + "fmt" |
| "strings" |
| "sync" |
| "testing" |
| @@ -18,6 +19,7 @@ type overlapStore struct { |
| mu sync.Mutex |
| inFlight int |
| maxFlight int |
| + reads int |
| failKey string |
| } |
| @@ -25,6 +27,7 @@ func (s *overlapStore) Get(ctx context.Context, key string) ([]byte, string, err |
| if strings.Contains(key, "/instances/") { |
| s.mu.Lock() |
| s.inFlight++ |
| + s.reads++ |
| if s.inFlight > s.maxFlight { |
| s.maxFlight = s.inFlight |
| } |
| @@ -71,8 +74,11 @@ func TestRecordReadersReadRecordsConcurrently(t *testing.T) { |
| if err := c.run(s); err != nil { |
| t.Fatalf("%s: %v", c.name, err) |
| } |
| - if s.maxFlight < 2 { |
| - t.Fatalf("%s read at most %d record at a time, want several", c.name, s.maxFlight) |
| + if s.maxFlight < 2 || s.maxFlight > 8 { |
| + t.Fatalf("%s read up to %d records at a time, want several and never more than 8", c.name, s.maxFlight) |
| + } |
| + if s.reads != 12 { |
| + t.Fatalf("%s made %d record reads for 12 records, want one each", c.name, s.reads) |
| } |
| } |
| } |
| @@ -90,14 +96,81 @@ func TestReconcileStillJudgesAFailedReadPerRecord(t *testing.T) { |
| if err := Reconcile(ctx, s, reg, cfg); err != nil { |
| t.Fatal(err) |
| } |
| - for _, h := range []string{"ra.go.tito.io", "rb.go.tito.io", "rc.go.tito.io", "rd.go.tito.io"} { |
| + // A record created since the first pass must still be minted in a pass |
| + // where another record's read fails: the failure is rb's alone. |
| + fresh := acmeRecord(sqid) |
| + fresh.Host, fresh.Slug = "re.go.tito.io", "re" |
| + if err := CreateRecord(ctx, inner, fresh); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := Reconcile(ctx, s, reg, cfg); err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, h := range []string{"ra.go.tito.io", "rb.go.tito.io", "rc.go.tito.io", "rd.go.tito.io", "re.go.tito.io"} { |
| if rt, err := reg.Get(h); err != nil || rt == nil { |
| - t.Fatalf("%s gone after a pass where only rb's read failed: %+v, %v", h, rt, err) |
| + t.Fatalf("%s missing after a pass where only rb's read failed: %+v, %v", h, rt, err) |
| } |
| } |
| + _, err := ListAllRecords(ctx, s) |
| + if err == nil || !strings.Contains(err.Error(), "rb.go.tito.io") { |
| + t.Fatalf("ListAllRecords with rb unreadable = %v, want an error naming rb", err) |
| + } |
| +} |
| + |
| +// A loop that stops at its first bad read has read at most one chunk ahead, |
| +// and a large record set is never prefetched whole. |
| +func TestListAllRecordsStopsWithinAChunkOfABadRead(t *testing.T) { |
| + inner, sqid := setup(t) |
| + const total = 200 // more than one chunk, fixed so the test does not move with readChunk |
| + for i := 0; i < total; i++ { |
| + rec := acmeRecord(sqid) |
| + rec.Host, rec.Slug = fmt.Sprintf("r%04d.go.tito.io", i), fmt.Sprintf("r%04d", i) |
| + if err := CreateRecord(ctx, inner, rec); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + s := &countingGets{Store: inner, failKey: RecordKey(sqid, "tito", "r0000.go.tito.io")} |
| if _, err := ListAllRecords(ctx, s); err == nil { |
| - t.Fatal("ListAllRecords returned a set with one record unread") |
| + t.Fatal("ListAllRecords read past an unreadable first record without an error") |
| + } |
| + if s.n > readChunk || s.n >= total { |
| + t.Fatalf("read %d of %d records before stopping at the first, want at most one chunk (%d)", s.n, total, readChunk) |
| + } |
| +} |
| + |
| +// A cancelled context reads nothing, and every key reports why. |
| +func TestGetEachOnACancelledContextReadsNothing(t *testing.T) { |
| + inner, sqid := setup(t) |
| + s := &countingGets{Store: inner} |
| + cctx, cancel := context.WithCancel(ctx) |
| + cancel() |
| + key := RecordKey(sqid, "tito", "acme.go.tito.io") |
| + got := getEach(cctx, s, []string{key}) |
| + if s.n != 0 { |
| + t.Fatalf("made %d reads on a cancelled context", s.n) |
| } |
| + if !errors.Is(got[key].err, context.Canceled) { |
| + t.Fatalf("key reported %v, want context.Canceled", got[key].err) |
| + } |
| +} |
| + |
| +type countingGets struct { |
| + release.Store |
| + mu sync.Mutex |
| + n int |
| + failKey string |
| +} |
| + |
| +func (s *countingGets) Get(ctx context.Context, key string) ([]byte, string, error) { |
| + if strings.Contains(key, "/instances/") { |
| + s.mu.Lock() |
| + s.n++ |
| + s.mu.Unlock() |
| + if key == s.failKey { |
| + return nil, "", errors.New("read refused") |
| + } |
| + } |
| + return s.Store.Get(ctx, key) |
| } |
| // appOverlapStore holds each app-level read (an app's instances manifest and |
diff --git a/internal/provision/reconcile.go b/internal/provision/reconcile.go| index c54ceca19..0de893f51 100644 |
| --- a/internal/provision/reconcile.go |
| +++ b/internal/provision/reconcile.go |
| @@ -742,96 +742,95 @@ func Reconcile(ctx context.Context, s release.Store, reg *registry.Registry, cfg |
| recordKeys = append(recordKeys, key) |
| } |
| } |
| - bodies := getEach(ctx, s, recordKeys) |
| - for _, key := range keys { |
| - acct, app, host, ok := ParseRecordKey(key, known) |
| - if !ok { |
| - continue |
| - } |
| - raw, err := bodies[key].body, bodies[key].err |
| - if err != nil { |
| - log.Printf("provision: skipping record %s: read failed: %v", key, err) |
| - // The record's absence from recs below must not read as "the |
| - // bucket retired it" — a transient Get failure disables removal |
| - // for the whole pass, same as an unreadable account manifest. |
| - readFailed = true |
| - continue |
| - } |
| - rec, err := DecodeRecord(raw) |
| - if err != nil { |
| - log.Printf("provision: skipping record %s: malformed: %v", key, err) |
| - protect[host] = true |
| - continue |
| - } |
| - if rec.Host != host || rec.Account != acct || rec.App != app { |
| - log.Printf("provision: skipping record %s: disagrees with its own path (host=%q account=%q app=%q)", |
| - key, rec.Host, rec.Account, rec.App) |
| - protect[host] = true |
| - continue |
| - } |
| - // The record IS the authorization, and this pass turns its host into |
| - // a socket path and a database path (DefaultSocket/DefaultDBPath) |
| - // and a registry primary key. The console's create door enforces |
| - // this same shape — but a door is not a guarantee about what is |
| - // already in the bucket: anything with bucket write access, and any |
| - // record written by a different (older, or future) writer, arrives |
| - // here unexamined. Both doors hold the invariant, neither trusts |
| - // the other. Protected like a corrupted body, and for the same |
| - // reason: a record that cannot be used is not a record that was |
| - // retired. |
| - if !ValidHostShape(rec.Host) { |
| - log.Printf("provision: skipping record %s: host %q is not a hostname", key, rec.Host) |
| - protect[host] = true |
| - continue |
| - } |
| - // ValidateShape is the console create door's policy check — but a |
| - // door is not a guarantee about what is already in the bucket (the |
| - // same reasoning as the host-shape check just above). This pass |
| - // materializes whatever it finds, so a record whose Kind/Addr/Unit/ |
| - // Environment combination is malformed must be caught here too, or |
| - // a bad value becomes a live route. Protected like a corrupted |
| - // body: the record is there, it just cannot be used. |
| - if err := ValidateShape(rec); err != nil { |
| - log.Printf("provision: skipping record %s: invalid shape: %v", key, err) |
| - protect[host] = true |
| - continue |
| - } |
| - // ValidateShape deliberately does not inspect Channel (it validates |
| - // shape fields, and Channel is not one), but a channel-less record |
| - // must not become — or converge — a route pinned to no channel: |
| - // that is issue #130's failure mode arriving through a different |
| - // door than the one Task 3 closed. Protected like a corrupted body: |
| - // the record is there, it just cannot be used. |
| - if rec.Channel == "" { |
| - log.Printf("provision: skipping record %s: empty channel", key) |
| - protect[host] = true |
| - continue |
| - } |
| - // Scope. For a home-boxed record this is the region/pool test it |
| - // always was; for a static or public-addr one the region half widens |
| - // to every platform box in the deployment (everywhereClass, above). |
| - // A replica record narrows it: it belongs ONLY to the box whose |
| - // placement is the record's own Region — the placement the create |
| - // door validated as armed — so a replica record minted on the wrong |
| - // region would mint a route with no mirror behind it and no lease |
| - // speaking to it, a process holding live order data nobody |
| - // authorized into that region (the residence rule, #162). Every |
| - // other box skips it exactly like any other out-of-scope record; |
| - // the replica's own box sees Region == cfg.Region through the |
| - // ordinary test above. |
| - if !cfg.inScope(rec.Region, rec.Pool, everywhereClass(rec)) { |
| - continue // another agent's jurisdiction, not this pass's business |
| - } |
| - // Two accounts writing the same host is a collision this pass |
| - // cannot arbitrate silently: whichever record this listing reaches |
| - // FIRST wins the host, and the loser is skipped loudly rather than |
| - // overwriting the winner in the map with no trace of the conflict. |
| - if prev, dup := recs[rec.Host]; dup { |
| - log.Printf("provision: duplicate host %s: keeping %s, skipping %s", rec.Host, |
| - RecordKey(prev.Account, prev.App, prev.Host), RecordKey(rec.Account, rec.App, rec.Host)) |
| - continue |
| + for _, part := range chunks(recordKeys) { |
| + bodies := getEach(ctx, s, part) |
| + for _, key := range part { |
| + acct, app, host, _ := ParseRecordKey(key, known) |
| + raw, err := bodies[key].body, bodies[key].err |
| + if err != nil { |
| + log.Printf("provision: skipping record %s: read failed: %v", key, err) |
| + // The record's absence from recs below must not read as "the |
| + // bucket retired it" — a transient Get failure disables removal |
| + // for the whole pass, same as an unreadable account manifest. |
| + readFailed = true |
| + continue |
| + } |
| + rec, err := DecodeRecord(raw) |
| + if err != nil { |
| + log.Printf("provision: skipping record %s: malformed: %v", key, err) |
| + protect[host] = true |
| + continue |
| + } |
| + if rec.Host != host || rec.Account != acct || rec.App != app { |
| + log.Printf("provision: skipping record %s: disagrees with its own path (host=%q account=%q app=%q)", |
| + key, rec.Host, rec.Account, rec.App) |
| + protect[host] = true |
| + continue |
| + } |
| + // The record IS the authorization, and this pass turns its host into |
| + // a socket path and a database path (DefaultSocket/DefaultDBPath) |
| + // and a registry primary key. The console's create door enforces |
| + // this same shape — but a door is not a guarantee about what is |
| + // already in the bucket: anything with bucket write access, and any |
| + // record written by a different (older, or future) writer, arrives |
| + // here unexamined. Both doors hold the invariant, neither trusts |
| + // the other. Protected like a corrupted body, and for the same |
| + // reason: a record that cannot be used is not a record that was |
| + // retired. |
| + if !ValidHostShape(rec.Host) { |
| + log.Printf("provision: skipping record %s: host %q is not a hostname", key, rec.Host) |
| + protect[host] = true |
| + continue |
| + } |
| + // ValidateShape is the console create door's policy check — but a |
| + // door is not a guarantee about what is already in the bucket (the |
| + // same reasoning as the host-shape check just above). This pass |
| + // materializes whatever it finds, so a record whose Kind/Addr/Unit/ |
| + // Environment combination is malformed must be caught here too, or |
| + // a bad value becomes a live route. Protected like a corrupted |
| + // body: the record is there, it just cannot be used. |
| + if err := ValidateShape(rec); err != nil { |
| + log.Printf("provision: skipping record %s: invalid shape: %v", key, err) |
| + protect[host] = true |
| + continue |
| + } |
| + // ValidateShape deliberately does not inspect Channel (it validates |
| + // shape fields, and Channel is not one), but a channel-less record |
| + // must not become — or converge — a route pinned to no channel: |
| + // that is issue #130's failure mode arriving through a different |
| + // door than the one Task 3 closed. Protected like a corrupted body: |
| + // the record is there, it just cannot be used. |
| + if rec.Channel == "" { |
| + log.Printf("provision: skipping record %s: empty channel", key) |
| + protect[host] = true |
| + continue |
| + } |
| + // Scope. For a home-boxed record this is the region/pool test it |
| + // always was; for a static or public-addr one the region half widens |
| + // to every platform box in the deployment (everywhereClass, above). |
| + // A replica record narrows it: it belongs ONLY to the box whose |
| + // placement is the record's own Region — the placement the create |
| + // door validated as armed — so a replica record minted on the wrong |
| + // region would mint a route with no mirror behind it and no lease |
| + // speaking to it, a process holding live order data nobody |
| + // authorized into that region (the residence rule, #162). Every |
| + // other box skips it exactly like any other out-of-scope record; |
| + // the replica's own box sees Region == cfg.Region through the |
| + // ordinary test above. |
| + if !cfg.inScope(rec.Region, rec.Pool, everywhereClass(rec)) { |
| + continue // another agent's jurisdiction, not this pass's business |
| + } |
| + // Two accounts writing the same host is a collision this pass |
| + // cannot arbitrate silently: whichever record this listing reaches |
| + // FIRST wins the host, and the loser is skipped loudly rather than |
| + // overwriting the winner in the map with no trace of the conflict. |
| + if prev, dup := recs[rec.Host]; dup { |
| + log.Printf("provision: duplicate host %s: keeping %s, skipping %s", rec.Host, |
| + RecordKey(prev.Account, prev.App, prev.Host), RecordKey(rec.Account, rec.App, rec.Host)) |
| + continue |
| + } |
| + recs[rec.Host] = rec |
| } |
| - recs[rec.Host] = rec |
| } |
| // --- manifest seed cache, per (acct, app), lazily ------------------------- |
| @@ -1406,28 +1405,27 @@ func ListAllRecords(ctx context.Context, s release.Store) ([]Record, error) { |
| recordKeys = append(recordKeys, key) |
| } |
| } |
| - bodies := getEach(ctx, s, recordKeys) |
| var recs []Record |
| - for _, key := range keys { |
| - acct, app, host, ok := ParseRecordKey(key, known) |
| - if !ok { |
| - continue |
| - } |
| - raw, err := bodies[key].body, bodies[key].err |
| - if err != nil { |
| - return nil, fmt.Errorf("provision: read record %s: %w", key, err) |
| - } |
| - rec, err := DecodeRecord(raw) |
| - if err != nil { |
| - log.Printf("provision: skipping record %s: malformed: %v", key, err) |
| - continue |
| - } |
| - if rec.Host != host || rec.Account != acct || rec.App != app { |
| - log.Printf("provision: skipping record %s: disagrees with its own path (host=%q account=%q app=%q)", |
| - key, rec.Host, rec.Account, rec.App) |
| - continue |
| + for _, part := range chunks(recordKeys) { |
| + bodies := getEach(ctx, s, part) |
| + for _, key := range part { |
| + acct, app, host, _ := ParseRecordKey(key, known) |
| + raw, err := bodies[key].body, bodies[key].err |
| + if err != nil { |
| + return nil, fmt.Errorf("provision: read record %s: %w", key, err) |
| + } |
| + rec, err := DecodeRecord(raw) |
| + if err != nil { |
| + log.Printf("provision: skipping record %s: malformed: %v", key, err) |
| + continue |
| + } |
| + if rec.Host != host || rec.Account != acct || rec.App != app { |
| + log.Printf("provision: skipping record %s: disagrees with its own path (host=%q account=%q app=%q)", |
| + key, rec.Host, rec.Account, rec.App) |
| + continue |
| + } |
| + recs = append(recs, rec) |
| } |
| - recs = append(recs, rec) |
| } |
| return recs, nil |
| } |
| @@ -1520,6 +1518,25 @@ func listAppSubtree(ctx context.Context, s release.Store, known map[string]bool, |
| return out, partial, nil |
| } |
| +// readChunk is how many keys a judging loop prefetches at once: enough to keep |
| +// listWalkers busy, few enough that a large record set is never held in |
| +// memory whole, and that a loop stopping at its first bad read has read only |
| +// a chunk ahead of it. |
| +const readChunk = 64 |
| + |
| +// chunks splits keys into runs of at most readChunk, in order. |
| +func chunks(keys []string) [][]string { |
| + var out [][]string |
| + for len(keys) > readChunk { |
| + out = append(out, keys[:readChunk]) |
| + keys = keys[readChunk:] |
| + } |
| + if len(keys) > 0 { |
| + out = append(out, keys) |
| + } |
| + return out |
| +} |
| + |
| // fetched is one prefetched object: its body, or why it could not be read. |
| type fetched struct { |
| body []byte |
| @@ -1536,8 +1553,18 @@ type fetched struct { |
| // visit keys in the same order and judge each one exactly as before, only |
| // reading the body from here rather than from the store. A key's read error |
| // is that key's, as it was. |
| +// |
| +// Callers prefetch one chunk (chunks) at a time and judge it before the next. |
| +// A context already cancelled reads nothing: every key reports ctx.Err(), so |
| +// a judging loop sees a read failure for it, never a missing body. |
| func getEach(ctx context.Context, s release.Store, keys []string) map[string]fetched { |
| out := make(map[string]fetched, len(keys)) |
| + if err := ctx.Err(); err != nil { |
| + for _, k := range keys { |
| + out[k] = fetched{err: err} |
| + } |
| + return out |
| + } |
| var mu sync.Mutex |
| forEach(keys, func(k string) { |
| body, _, err := s.Get(ctx, k) |
diff --git a/internal/provision/sidecar_reconcile.go b/internal/provision/sidecar_reconcile.go| index a861f7cfe..ca021d821 100644 |
| --- a/internal/provision/sidecar_reconcile.go |
| +++ b/internal/provision/sidecar_reconcile.go |
| @@ -261,95 +261,94 @@ func ReconcileSidecars(ctx context.Context, s release.Store, reg *registry.Regis |
| recordKeys = append(recordKeys, key) |
| } |
| } |
| - bodies := getEach(ctx, s, recordKeys) |
| - for _, key := range keys { |
| - acct, app, name, ok := ParseSidecarKey(key, known) |
| - if !ok { |
| - continue |
| - } |
| - // Derived from the KEY, never from the body: the body is what is |
| - // under suspicion in every branch below. |
| - host := SidecarHost(name, app, acct) |
| - raw, err := bodies[key].body, bodies[key].err |
| - if err != nil { |
| - log.Printf("provision: skipping sidecar record %s: read failed: %v", key, err) |
| - // Its absence from recs below must not read as "the bucket retired |
| - // it" — a transient Get failure disables removal for the whole |
| - // pass, same as an unreadable account manifest. |
| - readFailed = true |
| - continue |
| - } |
| - var rec SidecarRecord |
| - if err := json.Unmarshal(raw, &rec); err != nil { |
| - log.Printf("provision: skipping sidecar record %s: malformed: %v", key, err) |
| - protect[host] = true |
| - continue |
| - } |
| - if rec.Name != name || rec.Account != acct || rec.App != app { |
| - log.Printf("provision: skipping sidecar record %s: disagrees with its own path (name=%q account=%q app=%q)", |
| - key, rec.Name, rec.Account, rec.App) |
| - protect[host] = true |
| - continue |
| - } |
| - // ValidateSidecarShape is the console create door's policy check — but |
| - // a door is not a guarantee about what is already in the bucket: |
| - // anything with bucket write access, and any record written by a |
| - // different (older, or future) writer, arrives here unexamined. Both |
| - // doors hold the invariant, neither trusts the other. Protected like a |
| - // corrupted body, and for the same reason: a record that cannot be |
| - // used is not a record that was retired. |
| - if err := ValidateSidecarShape(rec); err != nil { |
| - log.Printf("provision: skipping sidecar record %s: invalid shape: %v", key, err) |
| - protect[host] = true |
| - continue |
| - } |
| - // The host is DERIVED from the identity, never taken on trust: it |
| - // becomes a database path and a registry primary key, and a record |
| - // free to state its own host could name any row on the box. |
| - if rec.Host != host { |
| - log.Printf("provision: skipping sidecar record %s: host %q is not %q", key, rec.Host, host) |
| - protect[host] = true |
| - continue |
| - } |
| - // ValidateSidecarShape deliberately does not inspect Channel (it |
| - // validates shape fields, and Channel is not one), but a channel-less |
| - // record must not become — or converge — a route pinned to no channel: |
| - // such a row resolves no release, so it has no build to exec, and the |
| - // worker boot pass would try to run it against a symlink that does not |
| - // exist. The console stamps a channel at create, so a record without |
| - // one is malformed. Protected like a corrupted body: the record is |
| - // there, it just cannot be used. |
| - if rec.Channel == "" { |
| - log.Printf("provision: skipping sidecar record %s: empty channel", key) |
| - protect[host] = true |
| - continue |
| - } |
| - if rec.Region != cfg.Region || !poolServed(cfg.Pools, rec.Pool) { |
| - continue // another agent's jurisdiction, not this pass's business |
| - } |
| - // Placement is the third scope axis (design §8), and it is applied |
| - // HERE, beside region and pool, precisely so it behaves like them: a |
| - // record this box may not run never enters recs, and the removal phase |
| - // below therefore evacuates any row it left behind. "none" is a |
| - // property of the BOX's network; "no-inbound" is a requirement of the |
| - // WORKLOAD — the point of the two halves being independent is that a |
| - // member gets a mechanical guarantee rather than an operator's promise, |
| - // which a row left running on an inbound box would not be. |
| - if m, _ := manifestFor(acct, app); !placeable(rec, m, cfg) { |
| - log.Printf("provision: sidecar %s is not placeable here: it asks for %q placement and this box accepts inbound work (CARLOS_INBOUND=%q)", |
| - rec.Host, sidecarPlacement(rec, m), cfg.Inbound) |
| - continue |
| - } |
| - // Two records claiming one host is a collision this pass cannot |
| - // arbitrate silently: whichever this listing reaches FIRST wins, and |
| - // the loser is skipped loudly rather than overwriting the winner in |
| - // the map with no trace of the conflict. |
| - if prev, dup := recs[rec.Host]; dup { |
| - log.Printf("provision: duplicate sidecar host %s: keeping %s, skipping %s", rec.Host, |
| - SidecarRecordKey(prev.Account, prev.App, prev.Name), key) |
| - continue |
| + for _, part := range chunks(recordKeys) { |
| + bodies := getEach(ctx, s, part) |
| + for _, key := range part { |
| + acct, app, name, _ := ParseSidecarKey(key, known) |
| + // Derived from the KEY, never from the body: the body is what is |
| + // under suspicion in every branch below. |
| + host := SidecarHost(name, app, acct) |
| + raw, err := bodies[key].body, bodies[key].err |
| + if err != nil { |
| + log.Printf("provision: skipping sidecar record %s: read failed: %v", key, err) |
| + // Its absence from recs below must not read as "the bucket retired |
| + // it" — a transient Get failure disables removal for the whole |
| + // pass, same as an unreadable account manifest. |
| + readFailed = true |
| + continue |
| + } |
| + var rec SidecarRecord |
| + if err := json.Unmarshal(raw, &rec); err != nil { |
| + log.Printf("provision: skipping sidecar record %s: malformed: %v", key, err) |
| + protect[host] = true |
| + continue |
| + } |
| + if rec.Name != name || rec.Account != acct || rec.App != app { |
| + log.Printf("provision: skipping sidecar record %s: disagrees with its own path (name=%q account=%q app=%q)", |
| + key, rec.Name, rec.Account, rec.App) |
| + protect[host] = true |
| + continue |
| + } |
| + // ValidateSidecarShape is the console create door's policy check — but |
| + // a door is not a guarantee about what is already in the bucket: |
| + // anything with bucket write access, and any record written by a |
| + // different (older, or future) writer, arrives here unexamined. Both |
| + // doors hold the invariant, neither trusts the other. Protected like a |
| + // corrupted body, and for the same reason: a record that cannot be |
| + // used is not a record that was retired. |
| + if err := ValidateSidecarShape(rec); err != nil { |
| + log.Printf("provision: skipping sidecar record %s: invalid shape: %v", key, err) |
| + protect[host] = true |
| + continue |
| + } |
| + // The host is DERIVED from the identity, never taken on trust: it |
| + // becomes a database path and a registry primary key, and a record |
| + // free to state its own host could name any row on the box. |
| + if rec.Host != host { |
| + log.Printf("provision: skipping sidecar record %s: host %q is not %q", key, rec.Host, host) |
| + protect[host] = true |
| + continue |
| + } |
| + // ValidateSidecarShape deliberately does not inspect Channel (it |
| + // validates shape fields, and Channel is not one), but a channel-less |
| + // record must not become — or converge — a route pinned to no channel: |
| + // such a row resolves no release, so it has no build to exec, and the |
| + // worker boot pass would try to run it against a symlink that does not |
| + // exist. The console stamps a channel at create, so a record without |
| + // one is malformed. Protected like a corrupted body: the record is |
| + // there, it just cannot be used. |
| + if rec.Channel == "" { |
| + log.Printf("provision: skipping sidecar record %s: empty channel", key) |
| + protect[host] = true |
| + continue |
| + } |
| + if rec.Region != cfg.Region || !poolServed(cfg.Pools, rec.Pool) { |
| + continue // another agent's jurisdiction, not this pass's business |
| + } |
| + // Placement is the third scope axis (design §8), and it is applied |
| + // HERE, beside region and pool, precisely so it behaves like them: a |
| + // record this box may not run never enters recs, and the removal phase |
| + // below therefore evacuates any row it left behind. "none" is a |
| + // property of the BOX's network; "no-inbound" is a requirement of the |
| + // WORKLOAD — the point of the two halves being independent is that a |
| + // member gets a mechanical guarantee rather than an operator's promise, |
| + // which a row left running on an inbound box would not be. |
| + if m, _ := manifestFor(acct, app); !placeable(rec, m, cfg) { |
| + log.Printf("provision: sidecar %s is not placeable here: it asks for %q placement and this box accepts inbound work (CARLOS_INBOUND=%q)", |
| + rec.Host, sidecarPlacement(rec, m), cfg.Inbound) |
| + continue |
| + } |
| + // Two records claiming one host is a collision this pass cannot |
| + // arbitrate silently: whichever this listing reaches FIRST wins, and |
| + // the loser is skipped loudly rather than overwriting the winner in |
| + // the map with no trace of the conflict. |
| + if prev, dup := recs[rec.Host]; dup { |
| + log.Printf("provision: duplicate sidecar host %s: keeping %s, skipping %s", rec.Host, |
| + SidecarRecordKey(prev.Account, prev.App, prev.Name), key) |
| + continue |
| + } |
| + recs[rec.Host] = rec |
| } |
| - recs[rec.Host] = rec |
| } |
| trashed := trashGate(ctx, s, "sidecar") |