carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1# Final whole-branch review, fix wave R1: report
2
3Branch `s3-polling-336`, base of this wave `4512018a` (the review findings
4commit). Findings: `docs/superpowers/specs/CODEX-FINDINGS-S3-CHANGE-INDEX-CODE-R1.md`.
5All seven findings are fixed under the controller's binding rulings. Nothing
6pushed.
7
8## Commits
9
10| Commit | Finding(s) | Subject |
11|---|---|---|
12| `cd418ebf` | F3 | release, provision, host: any failed index poll leaves the watch unknown; record view polls are bounded; no canary gate on a tick that could not read the index |
13| `c1d29378` | F2, F4 | provision: a snapshot pass decides removals from the bucket's own walk |
14| `0c3e3ebc` | F1 | host: the moves pass acts only on its own walk of the bucket; the record view only says whether there is move work |
15| `46a738be` | F5 | host: with no change index armed the telemetry pass is main's, one resolve per app |
16| `6ca3bf3c` | F6 | host: a canary group is settled only by its own pointer read, at the generation and age the gate asks |
17| `5e0af511` | F7 | edge: an unchanged cert poll retries a failed local copy from the snapshot it holds |
18| `25eac78c` | F2, F4, census | provision: Reconcile and ReconcileSidecars stay whole units (scan and retire closures); census overlays re-pinned for the review fixes |
19
20`25eac78c` reshapes `c1d29378`: the first version split the record walk and
21the rest of each pass into new functions, which renamed legacy-accepted
22census units (B8 and plan14) and would have made the census owner re-rule
23them. The final version keeps `Reconcile` and `ReconcileSidecars` whole, with
24the walk and the removal phase as closures; behaviour and tests are
25identical, and only digests and basis move in the census.
26
27## F1 (P1): the moves pass acts only on live data
28
29Ruling applied as written. The snapshot is used only to decide whether there
30is any move work.
31
32- `internal/host/movespass.go:1077` `idle := w.movesIdle(ctx, s)`, computed
33 inside `w.pass` before the outbox flushes; `:1092` returns after the
34 flushes (they still run every pass, as on main) when idle; otherwise
35 `:1097-1098` walks `provision.ListAllRecordsStrict(ctx, s)` over live, and
36 the pass runs exactly as on main.
37- `:1296` `walkedIdle` is set at the end of a walked pass: no record with a
38 move, nothing undecodable, and `localMoveState()` false. Every early return
39 leaves it false (set false before the walk).
40- `:1307 movesIdle`: idle only when the view vouches (`viewStore` ok), the
41 last pass walked and was idle, the snapshot's strict listing has no record
42 with `Move` set and no undecodable record, and the worker holds no local
43 state.
44- `:1324 localMoveState`: wake fences (`fenced`), the strict set, the hold
45 view (unbuilt, or any claim/unknown), the offline keep view (unbuilt,
46 failed, or any move/unreadable host), T5 archive verdicts, landing leases,
47 and `:1356 movesDirHolds` on disk: anything under `<DataDir>/.moves/<x>/`
48 (op journals; parked journals leave the tree) or the page outbox. An
49 unreadable directory counts as holding something. Not counted, on purpose:
50 the move-log memo, `nudged`, `trouble`, `notices` (dedup and log
51 throttles; they never need a walk to be correct).
52- Epoch guard: **removed** (field `lastEpoch` and its refusal), since no move
53 step reads the snapshot any more. The view's monotonic publication is
54 still asserted by the simulation (invariant c).
55- `refreshOfflineView` always reads live: `internal/host/agent.go:1636` now
56 passes `s`, not the view store (start-up call at `agent.go:2985` was
57 already live).
58- Spec §4.3 "Moves" bullet rewritten to match; `viewStore`'s doc comment in
59 `internal/host/changeindex.go` updated.
60
61Tests (`internal/host/changeindex_test.go`):
62- `TestMovesPassNeverActsOnAnOlderSnapshot` (the regression Astra asked for,
63 legacy-abort flavour): an abort-complete write whose bump fails; an
64 unrelated scope fails to re-read so the pass walks live (fallback, sees no
65 move); a new move of the host is requested with its bump failing and
66 staging fills; the unrelated scope recovers, so the view is Complete again
67 holding the old `aborting` state (asserted). The pass must not delete the
68 new move's staging. RED on HEAD's moves pass: `the pass acted on the
69 snapshot's old abort and deleted the new move's staging: [delete
70 data-uk/moves/a.go.example.com/generations/g/snapshot delete
71 data-ie/move-receipts/a.go.example.com.json]`. GREEN after.
72- `TestMovesPassSkipsTheWalkOnlyWithNoMoveWork`: an idle armed pass makes no
73 record-family listing; a worker holding a wake fence walks. RED on HEAD:
74 `a worker holding a wake fence skipped its walk`. GREEN after.
75- `TestMovesPassDecidesFromItsOwnRefresh` replaces
76 `TestMovesPassTakesSnapshotUnderPass` (it asserted the removed epoch): pass
77 A paused after its reconcile, a move requested, A's moves leg sees it from
78 its own refresh under `w.pass` and walks. GREEN.
79- `TestMovesPassRefusesOlderEpoch` deleted with the guard.
80- `TestReconcileUsesViewWhenArmed` updated: the unbuilt offline keep view now
81 lists the bucket once (from live), and two later passes list nothing.
82
83## F2 + F4 (P1): removals over a snapshot are decided by the bucket's walk
84
85Ruling applied: per-row confirmations replaced by one live view of the facts
86per pass, built on the first removal candidate, by the walk's own logic.
87
88- `internal/provision/reconcile.go:835` `scan := func(st release.Store)`:
89 the record-collection phase, unchanged, over a store argument. `:1120`
90 `trashOver(st, pre)`: the trash lookup over a store (the pass's own over
91 `s` with its prefetch; the live one over the bucket with none). `:1542`
92 `retire(recs, protect, trashFor, stopAtFirst)`: the removal phase,
93 unchanged except that with `stopAtFirst` it removes nothing and reports
94 whether any row would go. Over a snapshot (`onSnapshot(s)`) the pass calls
95 `retire(..., true)`; on the first candidate it runs `scan(freshStore(s))`
96 (`:1614`) and, if that walk read cleanly, `retire` over the live facts and
97 `trashOver(live, nil)`, which decides every removal that pass (including
98 rows that were not candidates on the snapshot). A live walk that errs or
99 sets `readFailed` keeps every row that pass.
100- `internal/provision/sidecar_reconcile.go:210` `scan`, which also returns
101 the per-app sidecars manifest lookup (the mint loop keeps using the
102 snapshot's), so live placement uses the bucket's record and manifest
103 against `cfg.Inbound`. `:499 retire`, `:538` the live re-scan.
104- Snapshot passes with no candidate read nothing more; non-snapshot stores
105 walk once, as before.
106- `confirmRecordGone`, `confirmSidecarGone`, `confirmTrashed` deleted from
107 `internal/provision/recordview_store.go`; their reasoning now sits on
108 `onSnapshot` (`:331`); `walkErrSuffix` (`:339`) formats the log line.
109- Spec §4.4 paragraph "Removals are confirmed in the bucket" rewritten as
110 "Removals are decided by the bucket's walk".
111
112Tests (`internal/provision/recordview_test.go`, new `removalWorld` fixture
113over a bumping store plus a non-bumping handle on the same directory):
114- `TestSnapshotSidecarPlacementChangeEvacuates/record` and `/manifest`:
115 placement becomes no-inbound in the record or in the sidecars manifest,
116 bumps succeed, inbound box. RED: `a no-inbound sidecar stayed on an inbound
117 box over the snapshot` (both). GREEN. The same test asserts a snapshot pass
118 with nothing to remove makes no bucket listing.
119- `TestSnapshotRemovalProtectsAnInvalidOutOfScopeRecord`: the live record
120 (unbumped) has an empty channel and an out-of-scope region. RED: `route
121 one.example.com missing`. GREEN (kept, and the plain walk agrees).
122- `TestSnapshotRemovalKeepsARehomedHost`: the same host moves to another
123 app's key without a bump. RED: `route one.example.com missing`. GREEN; the
124 same test then deletes it everywhere and the row is removed (really gone
125 still removed).
126- `TestRemovalOverTheBucketWalksOnce`: disarmed path unchanged, one
127 `List accounts/` per pass and the removal happens. GREEN before and after
128 (a guard).
129- Existing `TestSnapshotAbsenceDoesNotRetireALiveRecord`,
130 `TestSnapshotTrashMarkerIsConfirmedBeforeRemoval`,
131 `TestSnapshotAbsenceDoesNotRetireALiveSidecar` pass unchanged.
132
133## F3 (P1): stalled index GET, frozen observation
134
135- `internal/release/changewatch.go:76-81`: a Poll whose ctx has already ended
136 or that gives up waiting for the poll slot now calls `w.fail`, so the watch
137 is unknown (`Gen`/`Observed` not ok, `Usable` false) until the next good
138 poll. ETag and entries are kept, so recovery is a 304.
139- `internal/provision/recordview.go:201` `RecordView.poll` bounds every index
140 poll with `pollTimeout` (`release.ChangeIndexPollTimeout`, set in
141 `NewRecordView` at `:62`, a field so provision tests can shorten it),
142 derived from the caller's ctx. Refresh's failed poll walks without polling
143 again (`rebuildLocked(ctx, false)`, `:126`, `:130`).
144- Agent: **both** protections. The watch is unknown after any failed poll
145 (so the canary gate finds every group due), and `fastTick` installs the
146 canary pointer gate only when its own index observation succeeded
147 (`internal/host/agent.go:2749`, `d.canaryGate != nil && indexOK`), which
148 also covers another caller's poll succeeding in between.
149
150Tests:
151- `TestChangeWatchPollLockWaitHonoursContext` updated to the new rule (the
152 give-up makes the watch unknown; after the stuck poll answers, the
153 generation is back). RED: `a give-up on the lock left the watch usable: its
154 observation is frozen`. New `TestChangeWatchCancelledPollIsUnknownThenA304`.
155 RED: `a cancelled Poll left the watch usable`. Both GREEN.
156- `TestViewStalledIndexIsBounded` (provision): index GET hangs while the
157 bucket answers; Refresh and Rebuild each return, walk, publish a Complete
158 snapshot with the new record, poll the index exactly once each, the watch
159 is unknown; after the stall, a refresh recovers and the next quiet refresh
160 is one conditional GET. RED: `Refresh over a stalled index did not return:
161 its poll is unbounded`. GREEN.
162- `TestStalledIndexGetKeepsReconcileWalking` (host): armed agent, a stalled
163 index GET; `reconcileSerialized` returns within the bound and mints the new
164 record (it walked), the watch is unknown, a gate marked before the stall is
165 due again, and the next good poll recovers the watch. RED against HEAD's
166 RecordView: `a reconcile pass hung behind a stalled index GET`. GREEN.
167- `TestCanaryPointerReadWhenTheTicksIndexPollFails` (host): another caller's
168 index GET hangs holding the slot; the tick gives up and must read the
169 canary pointer; after the stall, the quiet tick is gated again. RED against
170 HEAD's ChangeWatch: `a tick whose index poll gave up skipped the pointer
171 read (1 reads, want 2)`. GREEN.
172- Edge tests re-checked unchanged: `TestHomeMapStampsRefreshWhileTheWatchIsStuck`,
173 `TestCertMirrorPollsPastAStuckWatch` and the rest of
174 `-run 'Cert|HomeMap|Mirror|Mesh|Health'` pass; they assert the poll and the
175 heartbeat finish, which still holds.
176
177## F5 (P2): telemetry unchanged while disarmed
178
179- `internal/host/telemetry_pass.go:87`: `telemetrySwitchesWith` over a nil
180 watch (`r == nil || r.w == nil`) runs `telemetrySwitchesResolve` (`:136`),
181 main's `telemetrySwitches` body verbatim (one `telemetry.Resolve` per
182 account/app). Memo and cross-pass values apply only when armed.
183- `TestTelemetryAccountReadOncePerPass` now runs over a polled watch (armed).
184- New `TestTelemetryDisarmedResolvesEachAppOnItsOwn`: three apps of one
185 account, the first account read fails once; disarmed, the account file is
186 read three times and two apps stay on. RED: `three apps read the account's
187 telemetry.json 1 time(s) disarmed, want 3`. GREEN.
188
189## F6 (P2): one canary channel suppressing another's retry
190
191- `internal/host/agent.go:2201` `canaryRead{head, gen, at}` stored per
192 (account, app, channel) in `canaryHeads`; the read callback records the
193 generation `Due` returned and `now`. `:2823 canarySettled(k, routes, gen,
194 now)` requires the group's own `gen` to equal the one `Due` returned and
195 its own read to be younger than `ReadGate.MaxAge()` (new,
196 `internal/release/changewatch.go:204`), then every route live on its head.
197- New `TestCanaryGroupFreshnessIsPerChannel`: canary/dev and canary/beta of
198 one app; dev's promote is held out by a restart hold while beta reads and
199 marks the app scope; when dev's hold ends it reads again and adopts v2.
200 RED: `dev's pointer read 1 time(s) after its hold ended, want 2: beta's
201 read settled dev's group`. GREEN.
202
203## F7 (P3): cert local-copy retry
204
205- `internal/edge/certsync.go`: `syncLocal` returns whether any entry failed;
206 `publish` stores it in `localPending` (`:932`); the armed poll's unchanged
207 branch retries `syncLocal` against `m.snapshot()` when pending (`:763-764`).
208 No bucket read.
209- New `TestCertMirrorRetriesAFailedLocalCopyOnAnUnchangedPoll`: local cache
210 refuses Puts on the first poll; the next unchanged poll writes it with zero
211 bucket reads; a settled unchanged poll writes nothing. RED: `an unchanged
212 poll after a failed local copy left the local cache without it:
213 acme/autocert: certificate cache miss`. GREEN.
214
215## Census changes (for the #280 request amendment)
216
217All in this branch's own unratified overlays, still `PROPOSED-s3-change-index`.
218
219`internal/provision/testdata/plan14-root-reads-s3-change-index.tsv`
220(seal in `plan14_root_reads_test.go` moves from `7d4b63ec…` to
221`1eccb15a9cc0a215c4c3addb818b6310f9dd76fc6f8aba38038254b6037dfee6`):
222- Re-pinned (class unchanged, `legacy`/`accepted-legacy`; new digests, basis
223 rewritten for what they now do): `internal/provision/reconcile.go:Reconcile`
224 (scan and retire closures; a snapshot removal is decided by a second walk
225 over the bucket; relative to main a removal can only be withheld),
226 `internal/provision/sidecar_reconcile.go:ReconcileSidecars` (same, with live
227 placement), `internal/host/agent.go:fastTick` (canary gate only on a tick
228 whose index observation succeeded), `fastDeps.canaryPointerGate` and
229 `fastDeps.canarySettled` (per-channel generation and age),
230 `internal/host/telemetry_pass.go:telemetrySwitchesWith` (hands the
231 disarmed pass to main's body).
232- Removed (branch-own rows, functions deleted):
233 `internal/provision/recordview_store.go:confirmRecordGone`,
234 `confirmSidecarGone` (both the `@legacy` declaration and the entry).
235- Added: `internal/host/movespass.go:movesDirHolds`, `legacy`/
236 `accepted-legacy`, basis root-input (J4, as
237 `movesWorker.sweepFinalJournals`): two `file:ReadDir` sites listing
238 `<DataDir>/.moves` and its first level, only to decide whether to walk.
239 `internal/host/telemetry_pass.go:telemetrySwitchesResolve`, `legacy`/
240 `accepted-legacy`, basis J1 root-route: main's `telemetrySwitches` body
241 under a new name, same six sites at the same relative offsets.
242- Reworded: the `@removed` H3 row for `telemetrySwitches` (still declared,
243 still reads nothing; it now reaches main's body through
244 `telemetrySwitchesWith` when disarmed).
245- Envelopes moved: `internal/edge`, `internal/host`, `internal/provision`,
246 `internal/release`. Header line "What changed" updated.
247
248`internal/provision/s3_change_index_census_test.go` (B8 record writes):
249re-pins `Reconcile` (`b93306e1…`) and `ReconcileSidecars` (`cc2aa4db…`),
250class `key-site` and kinds unchanged; the comment describes the closures and
251the live re-walk instead of the confirmations.
252
253Console authority and invariant census: no change needed (passes).
254
255## Covering runs
256
257All with the sandbox off, GOCACHE default, serial and niced, at `25eac78c`.
258
259- `go test ./internal/release/ -run 'ChangeWatch|ReadGate|ChangeIndex|PassFilter|Adopt'`: ok (3.5s).
260- `go test ./internal/provision/ -run 'TestView|TestSnapshot|TestRemoval|Reconcile|Sidecar|RecordView|Census|Plan14|Plan9'`
261 (includes `TestRecordViewSimulation` at its default 50 seeds, the plan14
262 root-read census and the B8 record-write census): ok (31.1s).
263- `go test ./internal/host/ -run 'Moves|Offline|ChangeIndex|Canary|ConfigGen|Telemetry|FastTick|Scoped|Backstop|Reconcile|Instances|Stalled|Gates'`: ok (145.3s).
264- `go test ./internal/edge/ -run 'Cert|HomeMap|Mirror|Mesh|Health'`: ok (9.8s).
265- `go test ./internal/console/ -run 'RunSweep|SeedSweep|Census|Authority|Invariant'`: ok (195.6s), run after every code change.
266- `nice -n 19 go test ./internal/provision/ -run TestRecordViewSimulation -seeds 3000 -count=1`: ok (286s), one sweep for this revision.
267- `go vet ./...`: clean. `gofmt -l internal cmd`: empty.
268- `GOOS=darwin GOARCH=arm64 go build ./...` and `GOOS=windows GOARCH=amd64 go build ./...`: both build.
269
270## Concerns
271
2721. F1 cost and limit, as the ruling accepts: a move started by a write
273 whose bump failed is not seen until the backstop rebuild (or any pass
274 that walks for another reason). A box that keeps local move state on
275 disk (an op journal that never reaches its stamp, a root-owned page
276 entry) walks every pass, which is main's cost, not worse. The disk check
277 counts any non-empty first-level directory under `.moves`, so unrelated
278 leftovers there would also keep the walk on; I judged over-walking the
279 safe direction.
2802. F1 offline flows: skipping the walk also skips the per-pass refresh of
281 the offline keep view, which keeps the last walked state (no moves,
282 nothing undecodable, by the idle rule). A new offline move whose request
283 bump failed cannot flip without the source box's report, and the source
284 only reports from a pass that walked, so I see no path to a row being
285 removed early; worth a reviewer's second look.
2863. F2/F4: on a snapshot pass with a candidate, log lines from the removal
287 phase ("not removing X: participant ...", "protected") can appear twice,
288 once from the snapshot facts and once from the live ones. Cosmetic.
2894. F2/F4 granularity: the live re-walk is a whole-deployment walk, once per
290 pass that has any removal candidate (the ruling's cost). Candidates that
291 persist (a row whose live walk keeps it, e.g. a protected record) make
292 every snapshot pass walk until the record is fixed; that is main's cost
293 for those passes.
2945. F3: `TestStalledIndexGetKeepsReconcileWalking` and
295 `TestCanaryPointerReadWhenTheTicksIndexPollFails` each wait out the real
296 5s `ChangeIndexPollTimeout` (host serial and parallel phases, about 10s
297 added in total); the provision test shortens the bound through the new
298 unexported `RecordView.pollTimeout` field.
2996. Census: all changes are in this branch's own PROPOSED overlays and need
300 the #280 request amended (list above). New rows `movesDirHolds` and
301 `telemetrySwitchesResolve` are legacy/accepted-legacy like their siblings
302 in this overlay; the census owner may prefer different bases.
3037. Pre-existing, not touched: the branch's earlier tests in
304 `recordview_test.go` (`TestSnapshotAbsenceDoesNotRetireALiveRecord`,
305 `TestSnapshotTrashMarkerIsConfirmedBeforeRemoval`,
306 `TestSnapshotHoldRereadReadsTheBucket`) reuse the old `setup` fixture,
307 which uses tenant-like names (`tito`, `go.tito.io`) against the "no
308 tenant names" rule. My new tests use `shop`/`example.com`; the move
309 regression reuses the moves harness's `a.go.example.com` and account
310 `bab`.
311