provision: sorted fallback listing; a real sidecar through the walk
Astra round 2 (accept with fixes): - P3: the fallback path returned Store.List's order, and ListAllRecords had lost its own sort. It is sorted again, with a test over a store that lists in reverse. - P3: the sidecar reader is now driven over the walk with a real sidecar record, and must mint and keep it across two passes. Declined from the same round: request cost. The walk is about 1 + accounts + apps small listings per reader, against one full listing of every object. On the flagship that is a few hundred one-page requests in place of sixty pages per reader; a deployment of a thousand one-app accounts would pay more requests for fewer objects, which is noted on #231 rather than designed for now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+57
−0
internal/provision/applisting_test.go+56 −0internal/provision/reconcile.go+1 −0
CI passed — run details
diff --git a/internal/provision/applisting_test.go b/internal/provision/applisting_test.go| index 135eea120..71a61c3af 100644 |
| --- a/internal/provision/applisting_test.go |
| +++ b/internal/provision/applisting_test.go |
| @@ -4,6 +4,7 @@ import ( |
| "context" |
| "errors" |
| "reflect" |
| + "slices" |
| "sort" |
| "strings" |
| "sync" |
| @@ -279,3 +280,58 @@ func TestListAllRecordsRefusesAPartialWalk(t *testing.T) { |
| t.Fatalf("ListAllRecords over a failed app listing = %+v, nil; want an error", recs) |
| } |
| } |
| + |
| +// The sidecar reader over the walk mints the sidecar it mints over the full |
| +// listing, and keeps it on the next pass. Returning no sidecar keys at all |
| +// would otherwise pass every other test here. |
| +func TestSidecarReconcileOverTheWalkMintsAndKeeps(t *testing.T) { |
| + f := sidecarSetup(t) |
| + f.putManifest(t, "eleven", SidecarManifest{ |
| + Schema: 1, App: "eleven", Args: []string{"sidecar", "run"}, Channel: "stable"}) |
| + f.putRecord(t, SidecarRecord{Schema: 1, Name: "bot", App: "eleven", Channel: "stable"}) |
| + s := &bucketShaped{Store: f.store} |
| + cfg := ReconcileConfig{DataDir: f.dir} |
| + for pass := 1; pass <= 2; pass++ { |
| + if err := ReconcileSidecars(ctx, s, f.reg, cfg); err != nil { |
| + t.Fatalf("pass %d: %v", pass, err) |
| + } |
| + hasRoute(t, f.reg, f.host("bot", "eleven"), "the walk lost the sidecar") |
| + } |
| + if s.listedAllOfApps() { |
| + t.Fatalf("the sidecar reader listed the whole of apps/: %v", s.lists) |
| + } |
| +} |
| + |
| +// reversedList answers List in reverse order and makes no bucket-shape |
| +// promise: Store.List does not promise an order. |
| +type reversedList struct{ release.Store } |
| + |
| +func (s reversedList) List(ctx context.Context, prefix string) ([]string, error) { |
| + keys, err := s.Store.List(ctx, prefix) |
| + slices.Reverse(keys) |
| + return keys, err |
| +} |
| + |
| +// ListAllRecords returns records in a stable order whatever order the store |
| +// lists in, on the fallback path as on the walk. |
| +func TestListAllRecordsIsSortedOnTheFallback(t *testing.T) { |
| + inner, sqid := setup(t) |
| + for _, slug := range []string{"acme", "zeta", "beta"} { |
| + rec := acmeRecord(sqid) |
| + rec.Host, rec.Slug = slug+".go.tito.io", slug |
| + if err := CreateRecord(ctx, inner, rec); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + recs, err := ListAllRecords(ctx, reversedList{inner}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + var hosts []string |
| + for _, r := range recs { |
| + hosts = append(hosts, r.Host) |
| + } |
| + if !slices.IsSorted(hosts) { |
| + t.Fatalf("ListAllRecords over a reverse-listing store = %v, want sorted", hosts) |
| + } |
| +} |
diff --git a/internal/provision/reconcile.go b/internal/provision/reconcile.go| index 0551541bb..b77b5e50e 100644 |
| --- a/internal/provision/reconcile.go |
| +++ b/internal/provision/reconcile.go |
| @@ -1398,6 +1398,7 @@ func ListAllRecords(ctx context.Context, s release.Store) ([]Record, error) { |
| func listAppSubtree(ctx context.Context, s release.Store, known map[string]bool, sub string) (keys []string, partial bool, err error) { |
| if !release.HasBucketListDirs(s) { |
| keys, err := s.List(ctx, "apps/") |
| + slices.Sort(keys) // Store.List promises no order; the walk's is sorted |
| return keys, false, err |
| } |
| accts, err := s.ListDirs(ctx, "apps/") |