carlos / platform Public

db-region-forward plans: answer Astra's plan review round 2

All 14 findings (25-38) fixed; none declined beyond round 1's audit-insert
item. Every new top-level identifier in the three plans checked against
origin/main for a same-package declaration: dbStream was the only
collision (now dbReadRelay).

Admission checks the request's context before each of its four writes and
cancels once anything is published (28, 33); a skipped readiness probe
stays owed (29); the mask test masks the /etc unit itself (27); plan 3
keeps plan 1's timing lines (26); T7 runs the real CLI and console through
the backhaul hop, the edges in package edge's test binary as a helper
process (34); new witnesses for 31, 35 and 38; DBF-32 and a console 400
for SQL that JSON would rewrite (36); a chunk count and size bound before
memory or the spool keeps a chunk (37).

Spec rev 6: section 5.5 step 3 (the per-write check, the U+FFFD refusal)
and section 10 (DBF-32).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev e765d57d158a79e308289ecbe52620d8bf1a29b2 parent 0e74eb7
5 files changed, +1110 −212
  • docs/superpowers/plans/2026-10-22-db-region-forward-plan-1-box.md +121 −23
  • docs/superpowers/plans/2026-10-22-db-region-forward-plan-2-query.md +827 −153
  • docs/superpowers/plans/2026-10-22-db-region-forward-plan-3-pull.md +124 −28
  • docs/superpowers/specs/2026-10-22-db-region-forward-design.md +16 −7
  • docs/superpowers/specs/CODEX-REVIEW-BRIEF-DB-REGION-FORWARD-PLANS.md +22 −1

CI passed — run details

CI log
=== gate ===
.amadan/test-gate.sh
=== launchctx (1 s)
ok  	amadan.net/carlos/platform.git/launchctx	0.014s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
=== vet (2 s)
=== gofmt (1 s)
gofmt -l .: clean
=== rest (145 s)
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	4.685s
ok  	github.com/carlosframework/platform/cmd/moveprobe	11.584s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.705s
ok  	github.com/carlosframework/platform/internal/activator	59.300s
ok  	github.com/carlosframework/platform/internal/agentsock	0.016s
ok  	github.com/carlosframework/platform/internal/alerting	0.010s
ok  	github.com/carlosframework/platform/internal/analytics	0.022s
ok  	github.com/carlosframework/platform/internal/appconfig	2.835s
ok  	github.com/carlosframework/platform/internal/apperr	0.076s
ok  	github.com/carlosframework/platform/internal/applog	0.130s
ok  	github.com/carlosframework/platform/internal/awsinv	0.009s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.008s
ok  	github.com/carlosframework/platform/internal/boxid	0.632s
?   	github.com/carlosframework/platform/internal/boxid/boxidtest	[no test files]
?   	github.com/carlosframework/platform/internal/boxid/internal/signtest	[no test files]
ok  	github.com/carlosframework/platform/internal/boxvolumes	0.010s
ok  	github.com/carlosframework/platform/internal/caps	0.016s
ok  	github.com/carlosframework/platform/internal/certwarm	0.008s
ok  	github.com/carlosframework/platform/internal/cgroup	0.703s
ok  	github.com/carlosframework/platform/internal/changelog	0.348s
ok  	github.com/carlosframework/platform/internal/claimactivation	0.888s
ok  	github.com/carlosframework/platform/internal/clamd	0.054s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	6.308s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.016s
ok  	github.com/carlosframework/platform/internal/dbverify	0.011s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.008s
ok  	github.com/carlosframework/platform/internal/docsite	0.014s
ok  	github.com/carlosframework/platform/internal/domains	2.719s
ok  	github.com/carlosframework/platform/internal/doorkey	0.007s
ok  	github.com/carlosframework/platform/internal/economics	0.004s
ok  	github.com/carlosframework/platform/internal/edge	45.178s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.008s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.134s
ok  	github.com/carlosframework/platform/internal/gate	1.700s
ok  	github.com/carlosframework/platform/internal/genselect	0.015s
ok  	github.com/carlosframework/platform/internal/geo	0.015s
ok  	github.com/carlosframework/platform/internal/geoip	0.029s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.155s
ok  	github.com/carlosframework/platform/internal/host	139.261s
ok  	github.com/carlosframework/platform/internal/hostactors	5.764s
ok  	github.com/carlosframework/platform/internal/hostactors/awsmembership	0.005s
ok  	github.com/carlosframework/platform/internal/hostbox	3.135s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.738s
ok  	github.com/carlosframework/platform/internal/hostenv	0.010s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.007s
ok  	github.com/carlosframework/platform/internal/hostexec	5.499s
ok  	github.com/carlosframework/platform/internal/hostfence	0.051s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.294s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.016s
ok  	github.com/carlosframework/platform/internal/hostguard	1.038s
ok  	github.com/carlosframework/platform/internal/hostops	2.178s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.002s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.004s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.645s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.180s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.005s
ok  	github.com/carlosframework/platform/internal/infraview	0.002s
ok  	github.com/carlosframework/platform/internal/instancectl	5.241s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.775s
ok  	github.com/carlosframework/platform/internal/instancefs	0.012s
ok  	github.com/carlosframework/platform/internal/instancekey	0.002s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.010s
ok  	github.com/carlosframework/platform/internal/layout	1.976s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.007s
ok  	github.com/carlosframework/platform/internal/lease	10.150s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.547s
ok  	github.com/carlosframework/platform/internal/ledger	0.022s
ok  	github.com/carlosframework/platform/internal/mail	0.223s
ok  	github.com/carlosframework/platform/internal/maintenance	0.085s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.607s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.114s
ok  	github.com/carlosframework/platform/internal/metrics	0.005s
ok  	github.com/carlosframework/platform/internal/moves	21.369s
ok  	github.com/carlosframework/platform/internal/moves/archive	1.117s
ok  	github.com/carlosframework/platform/internal/objstore	0.246s
ok  	github.com/carlosframework/platform/internal/ops	0.189s
ok  	github.com/carlosframework/platform/internal/park	0.259s
ok  	github.com/carlosframework/platform/internal/plans	0.005s
ok  	github.com/carlosframework/platform/internal/privatefile	0.006s
ok  	github.com/carlosframework/platform/internal/provision	14.154s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.010s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.639s
ok  	github.com/carlosframework/platform/internal/relay	7.243s
ok  	github.com/carlosframework/platform/internal/release	24.843s
ok  	github.com/carlosframework/platform/internal/rollout	3.819s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.004s
ok  	github.com/carlosframework/platform/internal/rootaws	0.008s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.059s
ok  	github.com/carlosframework/platform/internal/rootstate	0.027s
ok  	github.com/carlosframework/platform/internal/runs	0.569s
ok  	github.com/carlosframework/platform/internal/s3compat	0.009s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.774s
ok  	github.com/carlosframework/platform/internal/seeds	0.011s
ok  	github.com/carlosframework/platform/internal/sim/engines	2.723s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	10.876s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.405s
ok  	github.com/carlosframework/platform/internal/status	0.090s
ok  	github.com/carlosframework/platform/internal/steering	0.010s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.265s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.006s
ok  	github.com/carlosframework/platform/internal/termfence	0.005s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.003s
ok  	github.com/carlosframework/platform/internal/updatewords	0.004s
ok  	github.com/carlosframework/platform/internal/usage	0.005s
ok  	github.com/carlosframework/platform/internal/version	0.003s
ok  	github.com/carlosframework/platform/internal/vet	1.644s
=== internal_console@0 (79 s)
ok  	github.com/carlosframework/platform/internal/console	77.877s
=== internal_console@1 (34 s)
ok  	github.com/carlosframework/platform/internal/console	32.440s
=== internal_console@2 (41 s)
ok  	github.com/carlosframework/platform/internal/console	39.302s
=== internal_console@3 (36 s)
ok  	github.com/carlosframework/platform/internal/console	34.955s
=== internal_console@4 (37 s)
ok  	github.com/carlosframework/platform/internal/console	35.690s
=== internal_console@5 (39 s)
ok  	github.com/carlosframework/platform/internal/console	37.142s
=== internal_console@6 (41 s)
ok  	github.com/carlosframework/platform/internal/console	39.668s
=== internal_console@7 (42 s)
ok  	github.com/carlosframework/platform/internal/console	40.710s
=== root@0 (32 s)
ok  	github.com/carlosframework/platform	30.634s
=== root@1 (44 s)
ok  	github.com/carlosframework/platform	42.491s
=== root@2 (41 s)
ok  	github.com/carlosframework/platform	39.619s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
linux/arm64 carlos binary: 50069666 bytes (ceiling 52000000)
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
  minio: cached, digest verified
  mc: cached, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.025s
=== RUN   TestT4StorageProbeMinio
2026/10/22 16:45:50 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E0E7D44E48FDD3, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.021s
minio contract: PASS
diff --git a/docs/superpowers/plans/2026-10-22-db-region-forward-plan-1-box.md b/docs/superpowers/plans/2026-10-22-db-region-forward-plan-1-box.md
index 4b03b38f3..64c5122f6 100644
--- a/docs/superpowers/plans/2026-10-22-db-region-forward-plan-1-box.md
+++ b/docs/superpowers/plans/2026-10-22-db-region-forward-plan-1-box.md
@@ -61,6 +61,17 @@ Astra's findings (`docs/superpowers/specs/CODEX-FINDINGS-DB-REGION-FORWARD-PLANS
None declined.
+## Plan review, round 2
+
+From `CODEX-FINDINGS-DB-REGION-FORWARD-PLANS-R2.md`:
+
+- **27** (the mask could not take): T13 swaps `/etc/systemd/system/carlos-dbread@.service` for a `/dev/null` symlink, proves `LoadState=masked`, then removes the symlink before restoring the file; cleanup removes before it writes.
+- **29** (an owed probe could be lost): `Probe` answers whether it ran, `Run` retries a skipped one every `probeRetry`, and a failed worker's connection is closed before the probe is asked for (`closeThenProbe`), so admission sees the lingering unit. Test: `TestAProbeSkippedWhileTheUnitLingersRunsOnceItIsGone`, through `Run`.
+- **26, plan 1's side**: plan 3 now keeps the two timing lines this plan writes.
+- Every new top-level identifier in the three plans was checked against `origin/main` for a declaration of the same name in the same package (functions, types, variables, constants and methods on existing types); `dbStream` (plan 2) was the only collision.
+
+None declined.
+
## Census impact
- **Plan 14 root-read census:** one overlay, `plan14-root-reads-db-region-forward-1.tsv`, `rev3=PROPOSED-db-region-forward-1`:
@@ -79,7 +90,7 @@ Plan 1 creates the whole deck from spec §10 (every row `pending`) so its gate p
|---|---|---|
| DBF-31 | docs/site/cli.md | carlos system db-worker runs on platform boxes only. systemd starts it for each forwarded database query. you never run it yourself. |
-Rows DBF-1 to DBF-16, DBF-21, DBF-22 and DBF-30 are plan 2's to review and build.
+Rows DBF-1 to DBF-16, DBF-21, DBF-22, DBF-30 and DBF-32 are plan 2's to review and build.
## File structure
@@ -195,6 +206,7 @@ RFC 3339 time.
| DBF-13 | dbforward.go | the answer from region <region> did not check out, so carlos db showed none of it. try again. | pending |
| DBF-15 | dbforward.go | <host> changed while the read was waiting. try again. | pending |
| DBF-16 | dbforward.go | <host> is moving between regions. read its database once the move has finished. | pending |
+| DBF-32 | dbforward.go | this query cannot be sent to region <region>, because it holds bytes that are not UTF-8 text or the character U+FFFD. fix the query and run it again. | pending |
## Docs
@@ -235,13 +247,13 @@ const dbForwardCopyRecordPath = "docs/superpowers/plans/2026-10-22-db-region-for
var dbForwardCopyIDPattern = regexp.MustCompile(`^DBF-[0-9]+$`)
// dbForwardCopyIDs is the record's inventory, pinned: DBF-1 to DBF-16,
-// DBF-21, DBF-22, DBF-30 and DBF-31. Plan 3 adds its pull rows here.
+// DBF-21, DBF-22, DBF-30, DBF-31 and DBF-32. Plan 3 adds its pull rows here.
var dbForwardCopyIDs = func() []string {
- ids := make([]string, 0, 20)
+ ids := make([]string, 0, 21)
for i := 1; i <= 16; i++ {
ids = append(ids, "DBF-"+strconv.Itoa(i))
}
- return append(ids, "DBF-21", "DBF-22", "DBF-30", "DBF-31")
+ return append(ids, "DBF-21", "DBF-22", "DBF-30", "DBF-31", "DBF-32")
}()
// dbForwardCopyBuilt is every id whose text the code or docs carry. The task
@@ -3499,7 +3511,7 @@ type inProcWorkers struct {
liveAtDial []int64
// failStart makes the worker end at once without a word, as a unit
// whose service fails to start after the socket accepted does.
- failStart bool
+ failStart atomic.Bool
}
func (w *inProcWorkers) dial(context.Context) (net.Conn, error) {
@@ -3509,7 +3521,7 @@ func (w *inProcWorkers) dial(context.Context) (net.Conn, error) {
w.mu.Unlock()
w.dials.Add(1)
w.live.Add(1)
- if w.failStart {
+ if w.failStart.Load() {
// The socket accepted and the service died: the plan's header is
// read by nobody useful, and the connection drops.
go func() {
@@ -4073,6 +4085,15 @@ func (a *Agent) askProbe() {
}
}
+// closeThenProbe gives the failed worker's connection slot back before
+// asking for a probe, so admission sees the worker's unit as the orphan it
+// now is and holds the probe until the unit has stopped (plan review
+// finding 29). The deferred Close that follows is a no-op (countedConn).
+func closeThenProbe(a *Agent, conn net.Conn) {
+ conn.Close()
+ a.askProbe()
+}
+
// countedConn gives a worker connection's slot back exactly once.
type countedConn struct {
net.Conn
@@ -4384,7 +4405,7 @@ func TestRetryWaitsForTheFirstWorkerToStop(t *testing.T) {
func TestAWorkerThatNeverStartsAsksForAProbe(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
- a, _ := fx.agent("uk-1", func(_ *AgentConfig, w *inProcWorkers) { w.failStart = true })
+ a, _ := fx.agent("uk-1", func(_ *AgentConfig, w *inProcWorkers) { w.failStart.Store(true) })
_, _, r := serveOne(t, a, fx, fx.request("SELECT 1", ""))
if r.Code != CodeInterrupted || len(a.probeAfter) != 1 {
t.Fatalf("result %+v, probe asked %d", r, len(a.probeAfter))
@@ -5020,7 +5041,7 @@ func (a *Agent) attempt(ctx context.Context, req Request, pub *ecdh.PublicKey, a
if err := writeJSONFrame(conn, frameHeader, planHeader{V: ProtocolVersion, Op: req.Op, Files: plan.Planned,
Target: plan.TXID, MaxRestored: a.cfg.MaxRestored}); err != nil {
- a.askProbe()
+ closeThenProbe(a, conn)
return fail(CodeInterrupted, Summary{})
}
for _, f := range plan.Planned {
@@ -5032,14 +5053,14 @@ func (a *Agent) attempt(ctx context.Context, req Request, pub *ecdh.PublicKey, a
// The worker went before ready: its unit may have failed to
// start after the socket accepted (spec §7.1; plan review
// finding 15). Probe once this connection is closed.
- a.askProbe()
+ closeThenProbe(a, conn)
return fail(CodeInterrupted, Summary{})
}
}
typ, p, err := readFrame(conn)
switch {
case err != nil:
- a.askProbe()
+ closeThenProbe(a, conn)
return fail(CodeInterrupted, Summary{})
case typ == frameEnd:
var e endMsg
@@ -5181,7 +5202,7 @@ git commit -m "dbreads: the owned read, its identity bracket, sealed chunks and
**Interfaces:**
- Consumes: `consider`, `decide`, `isDecided`, `prune`, `dial`, `adm.admitProbe` (Tasks 9, 10).
-- Produces: `(*Agent).Run(ctx context.Context, nudge <-chan struct{})`; `(*Agent).Pass(ctx context.Context)`; `(*Agent).Probe(ctx context.Context)`; `errSandbox`.
+- Produces: `(*Agent).Run(ctx context.Context, nudge <-chan struct{})`; `(*Agent).Pass(ctx context.Context)`; `(*Agent).Probe(ctx context.Context) bool` (false when admission skipped it); `probeRetry`; `errSandbox`.
- [ ] **Step 1: Write the failing tests**
@@ -5493,11 +5514,49 @@ func TestRunPassesOnANudge(t *testing.T) {
}
t.Fatal("a nudge did not serve the read")
}
+
+// Plan review finding 29, through the real loop: a read whose worker never
+// starts asks for a probe while that worker's unit is still stopping, so
+// admission skips it; the probe stays owed and runs once the unit is gone,
+// and the capability is withdrawn well inside ProbeEvery. Mutation: Run
+// calls Probe once on probeAfter and drops it when skipped (no retry):
+// Capable stays true until ProbeEvery and this test is red.
+func TestAProbeSkippedWhileTheUnitLingersRunsOnceItIsGone(t *testing.T) {
+ t.Parallel()
+ fx := newFixture(t, 1)
+ a, w := fx.agent("uk-1", func(_ *AgentConfig, w *inProcWorkers) { w.linger = 700 * time.Millisecond })
+ ctx, cancel := context.WithCancel(context.Background())
+ nudge := make(chan struct{}, 1)
+ done := make(chan struct{})
+ go func() { a.Run(ctx, nudge); close(done) }()
+ defer func() { cancel(); <-done }()
+ until := func(what string, ok func() bool) {
+ t.Helper()
+ deadline := time.Now().Add(10 * time.Second)
+ for !ok() {
+ if time.Now().After(deadline) {
+ t.Fatalf("never: %s", what)
+ }
+ time.Sleep(20 * time.Millisecond)
+ }
+ }
+ // The start probe has run and its unit is gone.
+ until("the start probe", func() bool { return w.dials.Load() >= 1 && w.live.Load() == 0 && a.Capable() })
+ w.failStart.Store(true)
+ req := fx.request("SELECT 1", "")
+ fx.submit(req)
+ nudge <- struct{}{}
+ until("the read's result", func() bool {
+ _, _, err := fx.store.Get(context.Background(), ResultKey(req.ID))
+ return err == nil
+ })
+ until("the owed probe withdrawing the capability", func() bool { return !a.Capable() })
+}
```
- [ ] **Step 2: Run them to verify they fail**
-Run: `go test ./internal/dbreads/ -run 'TestPass|TestReplay|TestTwoAgents|TestProbe|TestRun' -count=1`
+Run: `go test ./internal/dbreads/ -run 'TestPass|TestReplay|TestTwoAgents|TestProbe|TestRun|TestAProbeSkipped' -count=1`
Expected: FAIL, `Pass`, `Probe`, `Run` undefined.
- [ ] **Step 3: Add the loop to `agent.go`**
@@ -5505,11 +5564,23 @@ Expected: FAIL, `Pass`, `Probe`, `Run` undefined.
```go
var errSandbox = errors.New("dbreads: the worker can see more than its sandbox allows")
+// probeRetry is how soon a probe that admission skipped is tried again.
+var probeRetry = time.Second
+
// Run is the db-reads loop: a probe at start, a pass on every nudge (a
// serial move) and every ListEvery, a probe every ProbeEvery and whenever a
-// read's worker failed to start (spec §5.5 step 4, §7.1).
+// read's worker failed to start (spec §5.5 step 4, §7.1). A probe that
+// admission skips (the failed worker's unit is still stopping) stays owed
+// and is tried every probeRetry until it runs (plan review finding 29).
func (a *Agent) Run(ctx context.Context, nudge <-chan struct{}) {
- a.Probe(ctx)
+ var retry <-chan time.Time
+ tryProbe := func() {
+ retry = nil
+ if !a.Probe(ctx) {
+ retry = time.After(probeRetry)
+ }
+ }
+ tryProbe()
list := time.NewTicker(ListEvery)
defer list.Stop()
probe := time.NewTicker(ProbeEvery)
@@ -5524,9 +5595,11 @@ func (a *Agent) Run(ctx context.Context, nudge <-chan struct{}) {
case <-list.C:
a.Pass(ctx)
case <-probe.C:
- a.Probe(ctx)
+ tryProbe()
case <-a.probeAfter:
- a.Probe(ctx)
+ tryProbe()
+ case <-retry:
+ tryProbe()
}
}
}
@@ -5559,11 +5632,11 @@ func (a *Agent) Pass(ctx context.Context) {
// Probe is the readiness probe (spec §7.1): a hello on its own connection,
// so a busy box is never mistaken for a broken one. While admission is
-// blocked (an orphan unit, or no unit listing) it is skipped and the last
-// result stands.
-func (a *Agent) Probe(ctx context.Context) {
+// blocked (an orphan unit, or no unit listing) it is skipped, the last
+// result stands, and it answers false so Run keeps it owed.
+func (a *Agent) Probe(ctx context.Context) bool {
if !a.adm.admitProbe(ctx) {
- return
+ return false
}
defer a.adm.probeDone()
err := a.probe(ctx)
@@ -5571,6 +5644,7 @@ func (a *Agent) Probe(ctx context.Context) {
a.logf("dbreads: readiness probe: %v", err)
}
a.probeOK.Store(err == nil)
+ return true
}
func (a *Agent) probe(ctx context.Context) error {
@@ -7127,9 +7201,11 @@ func TestRootDBReadReadiness(t *testing.T) {
if err != nil {
t.Fatal(err)
}
+ // Cleanup removes first: during the mask the path is a symlink to
+ // /dev/null, and writing through it would write nothing.
t.Cleanup(func() {
+ os.Remove(dbreadServiceFile)
os.WriteFile(dbreadServiceFile, body, 0o644)
- runSystemctl("unmask", "--runtime", "carlos-dbread@.service")
runSystemctl("daemon-reload")
})
if err := os.Remove(dbreadServiceFile); err != nil {
@@ -7146,11 +7222,32 @@ func TestRootDBReadReadiness(t *testing.T) {
if !capable() {
t.Fatal("not capable once the service file is back")
}
- mustSystemctl(t, "mask", "--runtime", "carlos-dbread@.service")
+ // Masked where the unit is installed (plan review finding 27): a mask is
+ // a /dev/null symlink, and only one in /etc/systemd/system itself
+ // outranks the unit file ensureSystemUnits writes there. `systemctl
+ // mask` refuses while the file exists, and `--runtime` would land in
+ // /run/systemd/system, which /etc outranks; so swap the file for the
+ // symlink by hand, and prove the mask took before asking.
+ if err := os.Remove(dbreadServiceFile); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Symlink("/dev/null", dbreadServiceFile); err != nil {
+ t.Fatal(err)
+ }
+ mustSystemctl(t, "daemon-reload")
+ if out, _ := runSystemctl("show", "-p", "LoadState", "--value", "carlos-dbread@probe.service"); strings.TrimSpace(out) != "masked" {
+ t.Fatalf("the mask did not take: LoadState %q", out)
+ }
if capable() {
t.Error("capable with the service masked")
}
- mustSystemctl(t, "unmask", "--runtime", "carlos-dbread@.service")
+ if err := os.Remove(dbreadServiceFile); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(dbreadServiceFile, body, 0o644); err != nil {
+ t.Fatal(err)
+ }
+ mustSystemctl(t, "daemon-reload")
for _, m := range []string{"exit-1", "version-2"} {
r.marker(m, true)
if capable() {
@@ -7568,6 +7665,7 @@ Each mutation below must turn its test red. Make them in a copy of the tree, nev
| `consider` drops the Draining check after the jitter | `TestADrainingBoxDoesNotPick` |
| `consider` drops the cancel read | `TestPassSkipsACancelledRequest` |
| `attempt` drops `askProbe` after a pre-ready `sendFile` or `readFrame` failure | `TestAWorkerThatNeverStartsAsksForAProbe`; on the root runner, `TestRootDBReadReadiness`'s fail-start case |
+| `Run` drops a probe that admission skipped (no `retry`) (plan review finding 29) | `TestAProbeSkippedWhileTheUnitLingersRunsOnceItIsGone` |
| check B removed from `attempt` | `TestRecordChangeAtEveryStageWritesNoChunk` |
| `putCreateOnly` called with a non-empty `ifMatch`, or a `Delete` added after `result.json` | `TestReadNeverDeletesOrReplaces` |
| `elect` returns `false` without reading `picked` back | `TestElectionReadsBackALostAnswer` |
diff --git a/docs/superpowers/plans/2026-10-22-db-region-forward-plan-2-query.md b/docs/superpowers/plans/2026-10-22-db-region-forward-plan-2-query.md
index 3c71fa9d1..764f0e1bb 100644
--- a/docs/superpowers/plans/2026-10-22-db-region-forward-plan-2-query.md
+++ b/docs/superpowers/plans/2026-10-22-db-region-forward-plan-2-query.md
@@ -61,6 +61,23 @@ Astra's findings (`docs/superpowers/specs/CODEX-FINDINGS-DB-REGION-FORWARD-PLANS
- **11, in part: making the audit insert itself cancellation-aware.** `RecordActivityContext` takes a context, but its claim plumbing ends in `claimRawRecordActivity`'s context-free `Exec`, shared by every audited route (`internal/console/store.go:2588`, `2668`). Changing that is a change to the claim SQL layer and its census, for one local SQLite insert. The order makes it unnecessary: the row is written while the request is open (checked just before) and before any object a box can see, so a request that ends during the insert leaves the row of an admission and no read. The serial bump, the one bucket write, is request-bound.
+## Plan review, round 2
+
+From `CODEX-FINDINGS-DB-REGION-FORWARD-PLANS-R2.md`:
+
+- **25** (`dbStream` is `internal/console/apidb.go:475`'s function): the relay type is `dbReadRelay`. Every new identifier in the three plans was then checked against `origin/main`; this was the only collision.
+- **28** (admission could still publish after cancellation): the request's context is checked immediately before each of admission's four writes (audit row, `request.json`, queue entry, serial), with `cancel` once anything is published. Test: `TestDBReadEndedDuringAdmissionWritesOnlyTheCancel`, one case per step, through the `s.dbf.beforeWrite` seam. Not closed by the checks alone, and not closable without a transactional store: a request that ends between a check and its write still sees that write land; the next check then writes `cancel`, and a box that has already elected stops at its next `cancel` read (every 5 s). The audit insert's own cancellation stays declined (round 1).
+- **30** (pickup windows shorter than `issued_at`'s truncation): every accelerated window leaves the observation it needs after up to a second of truncation; production's timestamps are unchanged.
+- **31** (the completeness guard unwitnessed): `TestDBReadResultWaitsForAChunkTheListingMissed`.
+- **33** (the serial mutation never ran): "ended during the serial read" cancels inside the bump, with a store that refuses a cancelled write as S3 does.
+- **34** (T7 lacked the real CLI and console through backhaul): Task 11 runs both edges in package `edge`'s test binary as a helper process and the real CLI and console in the root package; `checkProgressAtTheCLI` watches the CLI's stderr and socket, through the local edge as well.
+- **35** (the result-line deadline check has a witness): `TestDBReadResultAfterTheDeadlineIsNotRelayed`, with a `ResponseWriter` that holds the last chunk's line.
+- **36** (SQL JSON would rewrite): the CLI refuses it with DBF-32 before sending, the console refuses U+FFFD with 400. Tests at the console, the CLI and the binary.
+- **37** (unbounded chunk count): `dbfChunkBound` caps the count and refuses a short chunk, or anything after an empty one, before it is kept.
+- **38** (the extension's mutation stayed green): `TestDBReadSetsBothReadDeadlines` watches the connection's read deadlines.
+
+None declined beyond round 1's audit-insert item.
+
## Census impact
- **Plan 14 root-read census:** one overlay, `plan14-root-reads-db-region-forward-2.tsv`, `rev3=PROPOSED-db-region-forward-2`:
@@ -96,6 +113,7 @@ Reviewed in Task 1 and built by the tasks named:
| DBF-16 | dbforward.go | <host> is moving between regions. read its database once the move has finished. | 8 |
| DBF-21 | internal/console/apidbforward.go | forwarded a database query to region <region>. the query was <n> bytes, sha256 <sha> (cli) | 6 |
| DBF-22 | internal/console/apidbforward.go | forwarded a database query to region <region>, as of <time>. the query was <n> bytes, sha256 <sha> (cli) | 6 |
+| DBF-32 | dbforward.go | this query cannot be sent to region <region>, because it holds bytes that are not UTF-8 text or the character U+FFFD. fix the query and run it again. | 8 |
| DBF-30 | docs/site/cli.md | (two paragraphs; spec §10) | 9 |
Unchanged and reused (spec §10): `dbQueryStatus`, the `dbRestoreError` sentences, SQLite's own text for `sql-error`, `stopped`, the `checkQuerySQL` refusals, the residency sentence when forwarding is off.
@@ -117,7 +135,7 @@ Unchanged and reused (spec §10): `dbQueryStatus`, the `dbRestoreError` sentence
| `infra/modules/deployment-buckets/main.tf` (modify), `tests/db_reads.tftest.hcl` (create) | The lifecycle rule (J4), T17. |
| `docs/superpowers/notes/db-reads-lifecycle-approval.md` (create) | The scoped approval plan for applying it. |
| `.amadan/ci.d/30-minio-contract` (modify) | Buckets, users and the PASS check for T1 and T16. |
-| Tests: `internal/console/apidbforward_test.go`, `internal/console/dbforward_minio_test.go`, `dbforward_test.go`, `dbforward_e2e_test.go`, `internal/edge/dbforward_backhaul_test.go` (create) | T1, T6, T7, T8, T11, T14, T15, T16, T18. |
+| Tests: `internal/console/apidbforward_test.go`, `internal/console/dbforward_minio_test.go`, `dbforward_test.go`, `dbforward_e2e_test.go`, `internal/edge/dbforward_backhaul_helper_test.go` (create) | T1, T6, T7, T8, T11, T14, T15, T16, T18. |
---
@@ -131,7 +149,7 @@ Unchanged and reused (spec §10): `dbQueryStatus`, the `dbRestoreError` sentence
- [ ] **Step 1: Run the review**
-Invoke the `copy-review` skill with rows DBF-1 to DBF-16, DBF-21, DBF-22 and DBF-30, each with its context from spec §10 (where it appears and when), neutral example values (`shop.example.com`, region `uk`, `2.0GB`, `64 MB`). Read `result.json` before relaunching the review server: a relaunch has destroyed a submitted review before.
+Invoke the `copy-review` skill with rows DBF-1 to DBF-16, DBF-21, DBF-22, DBF-30 and DBF-32, each with its context from spec §10 (where it appears and when), neutral example values (`shop.example.com`, region `uk`, `2.0GB`, `64 MB`). Read `result.json` before relaunching the review server: a relaunch has destroyed a submitted review before.
- [ ] **Step 2: Apply Paul's edits verbatim**
@@ -376,7 +394,7 @@ git commit -m "db forward: the reads route's lines and the list door's additive
**Interfaces:**
- Consumes: `release.RootSigner`, `newDBStreamLimits` (`apidb.go`), `dbreads.OpenPerConsole`, `dbreads.OpenPerPerson`.
-- Produces: `console.NewHTTPServer(h http.Handler) *http.Server`; `Config.DBForward bool`; `(*Server).dbForwardSigner() (release.Signer, bool)`; `Server.dbReads *dbStreamLimits`; `Server.dbf dbForwardTimes` with fields `poll, wait, stall, preHeader, pickup, grace, bound, call time.Duration`.
+- Produces: `console.NewHTTPServer(h http.Handler) *http.Server`; `Config.DBForward bool`; `(*Server).dbForwardSigner() (release.Signer, bool)`; `Server.dbReads *dbStreamLimits`; `Server.dbf dbForwardTimes` with fields `poll, wait, stall, preHeader, pickup, grace, bound, call time.Duration` and the test seam `beforeWrite func(step string)`.
- [ ] **Step 1: Write the failing tests**
@@ -533,6 +551,11 @@ type dbForwardTimes struct {
grace time.Duration // after the owner's deadline
bound time.Duration // the request's own bound after issued_at
call time.Duration // one store call inside the stream (plan review finding 12)
+
+ // beforeWrite, nil in production, runs just before admission checks the
+ // request's context ahead of each write ("audit", "request", "queue",
+ // "serial"): a test ends the request there (plan review finding 28).
+ beforeWrite func(step string)
}
func defaultDBForwardTimes() dbForwardTimes {
@@ -932,7 +955,7 @@ Waits for Task 1 (DBF-9, DBF-10, DBF-21, DBF-22 approved).
**Interfaces:**
- Consumes: `dbForwardSigner`, `forwardable`, `dbForwardEvidence`, `s.dbReads`, `s.dbf` (Tasks 3 to 5); `resolveDBSource`, `dbClassRefusal`, `dbResidencyRefusal`, `apiAccountApp`, `principalFrom`, `s.cli.db.allow`, `hashToken`, `bearer`, `readJSONLimit`, `jsonError`, `s.store.RecordActivityContext`, `release.BumpSerial`, `release.SerialKey`, `dbAuditAction` (existing console code); `dbreads.Sign`, `MintID`, `RequestKey`, `QueueKey`, `ParseReplyKey`, `ReadBody`; `dbquery.CheckSQL`; `hostclaim.Incarnation`.
-- Produces: `(*Server).handleAPIDBRead(w, r)`; `(*Server).admitDBRead(w, r) (dbReadAdmitted, string, bool)`; `type dbReadAdmitted struct{ id, region string; issued time.Time }`; `dbForwardAuditDetail(region, sqlText string, at time.Time) string`; `(*Server).cancelDBRead(id)`; `(*Server).announceDBRead(ctx)`; `dbfTooMany`, `dbfMoving`, `dbfAudit`, `dbfAuditAt`; the stream half (`streamDBRead`) is Task 7's; test helpers `(*dbfFixture).readURL(host)`, `queryBody(t, sql)`, `(*dbfFixture).openRead(t, host, bearer, body) *http.Response`, `readLines(t, body io.Reader) []dbreads.Line`, `refusal(t, res)`, `dbfPutHook`, `dbReadAudited(t, st)`.
+- Produces: `(*Server).handleAPIDBRead(w, r)`; `(*Server).admitDBRead(w, r) (dbReadAdmitted, string, bool)`; `type dbReadAdmitted struct{ id, region string; issued time.Time }`; `dbForwardAuditDetail(region, sqlText string, at time.Time) string`; `(*Server).cancelDBRead(id)`; `(*Server).announceDBRead(ctx)`; `dbfTooMany`, `dbfMoving`, `dbfAudit`, `dbfAuditAt`; the stream half (`streamDBRead`) is Task 7's; test helpers `(*dbfFixture).readURL(host)`, `queryBody(t, sql)`, `(*dbfFixture).openRead(t, host, bearer, body) *http.Response`, `readLines(t, body io.Reader) []dbreads.Line`, `refusal(t, res)`, `dbfPutHook`, `dbReadAudited(t, st)`, `(*dbfFixture).dbfServe(t, ctx, sql)`, `dbfBucket(t, s)`, `dbfCtxStore`; the seam `s.dbf.beforeWrite`.
- [ ] **Step 1: Write the failing tests**
@@ -1022,6 +1045,24 @@ func TestDBReadRouteRefusesBeforeAdmitting(t *testing.T) {
t.Errorf("%s: %d %s", name, status, body)
}
}
+ // SQL that JSON decoding would have rewritten (plan review finding 36):
+ // a byte that is not UTF-8, and a lone surrogate escape.
+ for name, sqlJSON := range map[string]string{
+ "a byte that is not UTF-8": "\"SELECT '\xff'\"",
+ "a lone surrogate": `"SELECT '\ud800'"`,
+ } {
+ raw := `{"op":"query","sql":` + sqlJSON + `,"reply_key":"` + good.ReplyKey + `"}`
+ req, _ := http.NewRequest(http.MethodPost, fx.readURL(dbfHost), strings.NewReader(raw))
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", "Bearer "+fx.member)
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if status, body := refusal(t, res); status != http.StatusBadRequest {
+ t.Errorf("%s: %d %s", name, status, body)
+ }
+ }
if keys, _ := fx.rel.List(context.Background(), dbreads.Prefix); len(keys) != 0 {
t.Fatalf("a refused read wrote %v", keys)
}
@@ -1118,13 +1159,74 @@ func dbReadAudited(t *testing.T, st *Store) bool {
return false
}
-// Plan review finding 11 (J1, spec §9.3 rev 6): the audit row is written
-// before any box can see the read, and a request that ends once the queue
-// entry is down writes the cancel and nothing else: no serial bump.
-// Mutations: move the audit after the creates (red: no row when request.json
-// lands); call s.bumpSerial instead of announceDBRead, or drop the ctx check
-// after the queue create (red: the serial moves); drop the cancel on that
-// path (red: no cancel object).
+// dbfServe drives the handler in process with ctx as the request's own
+// context, so a test can end the request at an exact step.
+func (fx *dbfFixture) dbfServe(t *testing.T, ctx context.Context, sqlText string) *httptest.ResponseRecorder {
+ t.Helper()
+ body, _ := queryBody(t, sqlText)
+ raw, _ := json.Marshal(body)
+ req := httptest.NewRequest(http.MethodPost, fx.readURL(dbfHost), bytes.NewReader(raw)).WithContext(ctx)
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", "Bearer "+fx.member)
+ rec := httptest.NewRecorder()
+ fx.srv.ServeHTTP(rec, req)
+ return rec
+}
+
+// dbfBucket is what a read left under db-reads/: request objects, queue
+// entries and cancels, by count.
+func dbfBucket(t *testing.T, s release.Store) (requests, queued, cancels int) {
+ t.Helper()
+ keys, _ := s.List(context.Background(), dbreads.Prefix)
+ for _, k := range keys {
+ switch {
+ case strings.HasPrefix(k, dbreads.Prefix+"queue/"):
+ queued++
+ case strings.HasSuffix(k, "/request.json"):
+ requests++
+ case strings.HasSuffix(k, "/cancel"):
+ cancels++
+ }
+ }
+ return
+}
+
+// dbfCtxStore refuses a write whose context has ended, as the S3 store
+// does and DirStore does not, and runs onGet once on a read of key.
+type dbfCtxStore struct {
+ release.Store
+ key string
+ onGet func()
+ once sync.Once
+}
+
+func (c *dbfCtxStore) Get(ctx context.Context, key string) ([]byte, string, error) {
+ if key == c.key {
+ c.once.Do(c.onGet)
+ }
+ return c.Store.Get(ctx, key)
+}
+
+func (c *dbfCtxStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) {
+ if err := ctx.Err(); err != nil {
+ return "", err
+ }
+ return c.Store.Put(ctx, key, body, ifMatch)
+}
+
+// Plan review findings 11, 28 and 33 (J1, spec §5.5 step 3 and §9.3 rev 6):
+// the request's context is checked immediately before every write of
+// admission. Ended before the audit row: nothing at all. Before
+// request.json: the row only. Before the queue entry: the row, request.json
+// and the cancel. Before the serial: the row, both objects and the cancel.
+// Never a serial bump, never a 200. And the bump itself runs on the
+// request's context, so a request that ends while it reads the serial does
+// not write it.
+// Mutations, each red in its own case: drop any one of the four ended()
+// checks; drop the cancel on a published step; write the audit row after
+// the creates ("the audit row is down before the request"); call
+// s.bumpSerial instead of announceDBRead ("ended during the serial read":
+// the detached write lands).
func TestDBReadEndedDuringAdmissionWritesOnlyTheCancel(t *testing.T) {
t.Run("the audit row is down before the request", func(t *testing.T) {
fx := newDBFFixture(t, true)
@@ -1141,39 +1243,50 @@ func TestDBReadEndedDuringAdmissionWritesOnlyTheCancel(t *testing.T) {
t.Fatal("request.json was written before the audit row")
}
})
- t.Run("ended after the queue entry", func(t *testing.T) {
+ for _, c := range []struct {
+ step string
+ audited bool
+ requests, queued, cancels int
+ }{
+ {"audit", false, 0, 0, 0},
+ {"request", true, 0, 0, 0},
+ {"queue", true, 1, 0, 1},
+ {"serial", true, 1, 1, 1},
+ } {
+ t.Run("ended before "+c.step, func(t *testing.T) {
+ fx := newDBFFixture(t, true)
+ _, serialBefore, _ := fx.rel.Get(context.Background(), release.SerialKey)
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ fx.srv.dbf.beforeWrite = func(step string) {
+ if step == c.step {
+ cancel()
+ }
+ }
+ rec := fx.dbfServe(t, ctx, "SELECT 1")
+ // The ended path writes no response (nobody is there to read
+ // it), and a recorder never written reports 200: read the body.
+ if strings.Contains(rec.Body.String(), "admitted") {
+ t.Fatalf("an ended request was admitted: %s", rec.Body)
+ }
+ if _, serialAfter, _ := fx.rel.Get(context.Background(), release.SerialKey); serialAfter != serialBefore {
+ t.Fatal("the serial was bumped after the request ended")
+ }
+ requests, queued, cancels := dbfBucket(t, fx.rel)
+ if got := dbReadAudited(t, fx.st); got != c.audited || requests != c.requests || queued != c.queued || cancels != c.cancels {
+ t.Fatalf("audited %v, requests %d, queue entries %d, cancels %d", got, requests, queued, cancels)
+ }
+ })
+ }
+ t.Run("ended during the serial read", func(t *testing.T) {
fx := newDBFFixture(t, true)
_, serialBefore, _ := fx.rel.Get(context.Background(), release.SerialKey)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- fx.srv.cfg.Releases = &dbfPutHook{Store: fx.rel, suffix: "queue", hook: cancel}
- body, _ := queryBody(t, "SELECT 2")
- raw, _ := json.Marshal(body)
- req := httptest.NewRequest(http.MethodPost, fx.readURL(dbfHost), bytes.NewReader(raw)).WithContext(ctx)
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("Authorization", "Bearer "+fx.member)
- rec := httptest.NewRecorder()
- fx.srv.ServeHTTP(rec, req)
- // The ended path writes no response (nobody is there to read it), and
- // a recorder that was never written reports 200: read the body.
- if strings.Contains(rec.Body.String(), "admitted") {
- t.Fatalf("an ended request was admitted: %s", rec.Body)
- }
+ fx.srv.cfg.Releases = &dbfCtxStore{Store: fx.rel, key: release.SerialKey, onGet: cancel}
+ fx.dbfServe(t, ctx, "SELECT 1")
if _, serialAfter, _ := fx.rel.Get(context.Background(), release.SerialKey); serialAfter != serialBefore {
- t.Fatal("the serial was bumped after the request ended")
- }
- keys, _ := fx.rel.List(context.Background(), dbreads.Prefix)
- var queued, cancelled int
- for _, k := range keys {
- switch {
- case strings.HasPrefix(k, dbreads.Prefix+"queue/"):
- queued++
- case strings.HasSuffix(k, "/cancel"):
- cancelled++
- }
- }
- if queued != 1 || cancelled != 1 || !dbReadAudited(t, fx.st) {
- t.Fatalf("queue entries %d, cancels %d, audited %v in %v", queued, cancelled, dbReadAudited(t, fx.st), keys)
+ t.Fatal("the serial was written after the request ended during the bump")
}
})
}
@@ -1205,7 +1318,7 @@ func TestDBReadStalledBodyIsCutAtThePreHeaderBound(t *testing.T) {
}
```
-Add `"bufio"`, `"bytes"`, `"crypto/ecdh"`, `"io"`, `"net"`, `"net/http"`, `"net/http/httptest"` and the `dbquery` import.
+Add `"bufio"`, `"bytes"`, `"crypto/ecdh"`, `"io"`, `"net"`, `"net/http"`, `"net/http/httptest"`, `"sync"` and the `dbquery` import.
The admission bounds test (DBF-9, T15) is Task 7's: it needs the stream to hold its requests open (plan review finding 7).
@@ -1370,6 +1483,16 @@ func (s *Server) admitDBRead(w http.ResponseWriter, r *http.Request) (dbReadAdmi
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return none, "", false
}
+ // encoding/json turns bytes that are not UTF-8, and a lone surrogate
+ // escape, into U+FFFD without a word, so the SQL decoded here may not be
+ // the SQL sent. The box must run exactly the bytes typed or be told no
+ // (plan 1's Review Focus 1), so any U+FFFD is refused; a query that
+ // meant one literally is refused too, and says so the same way (plan
+ // review finding 36). The CLI refuses first, with DBF-32.
+ if strings.ContainsRune(body.SQL, utf8.RuneError) {
+ http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
+ return none, "", false
+ }
if err := dbquery.CheckSQL(body.SQL); err != nil || body.SQL == "" {
msg := http.StatusText(http.StatusBadRequest)
if err != nil {
@@ -1435,40 +1558,65 @@ func (s *Server) admitDBRead(w http.ResponseWriter, r *http.Request) (dbReadAdmi
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return none, "", false
}
- // The audit row comes first, while the request is open and before any box
- // can see the read: a read a box runs always has its row, and a request
- // that ends here leaves the row of an admission and nothing else (spec
- // §9.3 rev 6; plan review finding 11). A row the store cannot write
- // refuses the read rather than run it unaudited.
- if err := ctx.Err(); err != nil {
+ // J1 at every write (plan review finding 28): the request's context is
+ // checked immediately before each one, because the stores do not stop a
+ // write for it (DirStore.Put ignores its context,
+ // internal/release/store.go:414). The audit row comes first, while the
+ // request is open and before any box can see the read, so a read a box
+ // runs always has its row (spec §9.3 rev 6). A request that ends before
+ // request.json is written leaves at most that row; one that ends after
+ // leaves request.json, perhaps the queue entry, and the cancel, never a
+ // serial bump. A row the store cannot write refuses the read rather than
+ // run it unaudited.
+ ended := func(step string, published bool) bool {
+ if s.dbf.beforeWrite != nil {
+ s.dbf.beforeWrite(step)
+ }
+ if ctx.Err() == nil {
+ return false
+ }
+ if published {
+ s.cancelDBRead(id)
+ }
+ return true
+ }
+ if ended("audit", false) {
return none, "", false
}
if err := s.store.RecordActivityContext(ctx, acct.ID, app, actor, Activity{Action: dbAuditAction,
TargetType: "instance", Target: host, Detail: dbForwardAuditDetail(rec.Region, body.SQL, at)}, time.Now()); err != nil {
return fail("audit", err)
}
+ if ended("request", false) {
+ return none, "", false
+ }
if _, err := s.cfg.Releases.Put(ctx, dbreads.RequestKey(id), wire, ""); err != nil {
+ // An ambiguous create may have landed: cancel, as for the queue.
+ s.cancelDBRead(id)
return fail("write the request", err)
}
+ if ended("queue", true) {
+ return none, "", false
+ }
if _, err := s.cfg.Releases.Put(ctx, dbreads.QueueKey(rec.Region, issued, id), nil, ""); err != nil {
// The queue entry may have landed on an ambiguous answer: cancel, so
// a box that lists it skips it (J1's one write).
s.cancelDBRead(id)
return fail("write the queue entry", err)
}
- // J1: once the request has ended, nothing about the read is written but
- // the cancel. The serial bump runs on the request's context, not
- // bumpSerial's detached one.
- if err := ctx.Err(); err != nil {
- s.cancelDBRead(id)
+ if ended("serial", true) {
return none, "", false
}
+ // The bump runs on the request's context, not bumpSerial's detached one,
+ // so a request that ends while it runs does not announce (finding 33).
s.announceDBRead(ctx)
admitted = true
return dbReadAdmitted{id: id, region: rec.Region, issued: issued}, actor, true
}
```
+Add `"strings"` and `"unicode/utf8"` to `apidbforward.go`'s imports if they are not there yet.
+
Until Task 7, add a stub so this task builds, and delete it in Task 7:
```go
@@ -1515,7 +1663,7 @@ git commit -m "console: the reads route's gate and admission (DBF-9, DBF-10, DBF
**Interfaces:**
- Consumes: `dbReadAdmitted`, `s.dbf` (Tasks 3, 6); `dbreads.Line`, `Picked`, `Result`, `PickedKey`, `ResultKey`, `OutPrefix`, `ParseChunkKey`, `ChunkKey`, `CancelKey`, the wait and ended words; `dbreadstest.Start`, `Box.Agent` (plan 1).
- Consumes also: `(*Server).cancelDBRead` (Task 6), `s.dbf.call` (Task 3).
-- Produces: `(*Server).streamDBRead(w, r, rc, a)`; `type dbLineWriter` (with `until`), `(*dbLineWriter).tick(ctx, phase, poll, every) error`; `errDBClientGone`; `dbfEndSlack`; `type dbStream`; `dbfCall[T](st, ctx, phase, op) (T, error)`; `(*Server).readDBResult(ctx, id) *dbreads.Result`; test helpers `runBox(t, box)`, `(*dbfFixture).startBox(t, dir) *dbreadstest.Box`, `dbfOpen`, `(*dbfFixture).openHeld`, `(*dbfFixture).admitWithin`, `refusedTooMany`, `slowStore`, `scriptedBox`, `dbReadHookStore`, `strictStore`.
+- Produces: `(*Server).streamDBRead(w, r, rc, a)`; `type dbLineWriter` (with `until`), `(*dbLineWriter).tick(ctx, phase, poll, every) error`; `errDBClientGone`; `dbfEndSlack`; `type dbReadRelay`; `dbfCall[T](st, ctx, phase, op) (T, error)`; `(*Server).readDBResult(ctx, id) *dbreads.Result`; test helpers `runBox(t, box)`, `(*dbfFixture).startBox(t, dir) *dbreadstest.Box`, `dbfOpen`, `(*dbfFixture).openHeld`, `(*dbfFixture).admitWithin`, `refusedTooMany`, `slowStore`, `scriptedBox`, `dbReadHookStore`, `dbfHideStore`, `dbfHeldWriter`, `dbfDeadlineListener`, `dbfDeadlineConn`, `strictStore`.
- [ ] **Step 1: Write the failing tests**
@@ -1597,7 +1745,10 @@ func TestDBReadStreamsAQueryEndToEnd(t *testing.T) {
// the pickup window, a cancel, and the slot free again.
func TestDBReadEndsNotPickedAndCancels(t *testing.T) {
fx := newDBFFixture(t, true)
- fx.srv.dbf.poll, fx.srv.dbf.wait, fx.srv.dbf.pickup = 10*time.Millisecond, 50*time.Millisecond, 400*time.Millisecond
+ // issued_at is truncated to the second (production's semantics), so the
+ // pickup window seen from admission is pickup minus up to a second: 1.4 s
+ // leaves at least 400 ms, eight wait periods (plan review finding 30).
+ fx.srv.dbf.poll, fx.srv.dbf.wait, fx.srv.dbf.pickup = 10*time.Millisecond, 50*time.Millisecond, 1400*time.Millisecond
body, _ := queryBody(t, "SELECT 1")
res := fx.openRead(t, dbfHost, fx.member, body)
lines := readLines(t, res.Body)
@@ -1621,7 +1772,9 @@ func TestDBReadEndsNotPickedAndCancels(t *testing.T) {
// is flushed as it is written.
func TestDBReadLinesAreFlushedAsWritten(t *testing.T) {
fx := newDBFFixture(t, true)
- fx.srv.dbf.poll, fx.srv.dbf.wait, fx.srv.dbf.pickup = 10*time.Millisecond, 100*time.Millisecond, time.Second
+ // At least a second of pickup is left after issued_at's truncation: room
+ // for the five lines below at 100 ms each (plan review finding 30).
+ fx.srv.dbf.poll, fx.srv.dbf.wait, fx.srv.dbf.pickup = 10*time.Millisecond, 100*time.Millisecond, 2*time.Second
body, _ := queryBody(t, "SELECT 1")
start := time.Now()
res := fx.openRead(t, dbfHost, fx.member, body)
@@ -2068,6 +2221,208 @@ func TestDBReadResultWaitsForItsLastChunk(t *testing.T) {
}
}
+// dbfHideStore leaves chunk seq out of the first hide listings of a read's
+// chunks: a listing that lags the box's writes.
+type dbfHideStore struct {
+ release.Store
+ seq int64
+ mu sync.Mutex
+ hide int
+}
+
+func (h *dbfHideStore) List(ctx context.Context, prefix string) ([]string, error) {
+ keys, err := h.Store.List(ctx, prefix)
+ if !strings.HasSuffix(prefix, "/out/") {
+ return keys, err
+ }
+ h.mu.Lock()
+ defer h.mu.Unlock()
+ if h.hide == 0 {
+ return keys, err
+ }
+ h.hide--
+ var out []string
+ for _, k := range keys {
+ if seq, ok := dbreads.ParseChunkKey(k, prefix); !ok || seq != h.seq {
+ out = append(out, k)
+ }
+ }
+ return out, err
+}
+
+// T18's completeness guard on its own (plan review finding 31): result.json
+// says last_seq 2 from the first round, and the first three listings miss
+// chunk 2. The result line waits until chunk 2 is relayed. Mutation: write
+// the result line without waiting for next > res.LastSeq (red: the result
+// comes after one chunk).
+func TestDBReadResultWaitsForAChunkTheListingMissed(t *testing.T) {
+ fx := newDBFFixture(t, true)
+ fx.srv.dbf.poll = 10 * time.Millisecond
+ fx.srv.cfg.Releases = &dbfHideStore{Store: fx.rel, seq: 2, hide: 3}
+ body, key := queryBody(t, "SELECT 1")
+ res := fx.openRead(t, dbfHost, fx.member, body)
+ defer res.Body.Close()
+ br := bufio.NewReader(res.Body)
+ first, _ := br.ReadBytes('\n')
+ var adm dbreads.Line
+ json.Unmarshal(first, &adm)
+ box := scriptFor(t, fx.rel, adm.Admitted)
+ box.chunk(1, false, "a\n")
+ box.chunk(2, true, "b\n")
+ box.result(2, "a\nb\n")
+ box.picked(time.Now().Add(time.Minute))
+ lines := append([]dbreads.Line{adm}, readLines(t, br)...)
+ chunks := 0
+ for _, l := range lines {
+ if l.Chunk != 0 {
+ chunks++
+ }
+ if l.Result != nil && chunks != 2 {
+ t.Fatalf("the result line came after %d chunks", chunks)
+ }
+ }
+ if rows, _ := openAnswer(t, key, lines); string(rows) != "a\nb\n" {
+ t.Fatalf("rows %q", rows)
+ }
+}
+
+// dbfHeldWriter is a ResponseWriter that holds the write of a line
+// containing holdOn until until: a client that takes one line slowly. A
+// stand-in for a network client, in a unit test.
+type dbfHeldWriter struct {
+ header http.Header
+ holdOn string
+ until time.Time
+ mu sync.Mutex
+ buf bytes.Buffer
+}
+
+func (w *dbfHeldWriter) Header() http.Header { return w.header }
+func (w *dbfHeldWriter) WriteHeader(int) {}
+func (w *dbfHeldWriter) Flush() {}
+
+func (w *dbfHeldWriter) Write(p []byte) (int, error) {
+ if bytes.Contains(p, []byte(w.holdOn)) {
+ time.Sleep(time.Until(w.until))
+ }
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ return w.buf.Write(p)
+}
+
+// Plan review finding 35: the owner's deadline holds for the result line
+// too. Every chunk and result.json are in the bucket, but the client takes
+// the last chunk's line until past the deadline: the read ends no-result,
+// and the result is not relayed. Mutation: drop the ownCtx check before the
+// result line (red: the result is relayed).
+func TestDBReadResultAfterTheDeadlineIsNotRelayed(t *testing.T) {
+ fx := newDBFFixture(t, true)
+ fx.srv.dbf.poll, fx.srv.dbf.grace = 10*time.Millisecond, 0
+ key, err := dbreads.NewReplyKey()
+ if err != nil {
+ t.Fatal(err)
+ }
+ id, err := dbreads.MintID()
+ if err != nil {
+ t.Fatal(err)
+ }
+ box := &scriptedBox{t: t, s: fx.rel, pub: key.PublicKey(), id: id}
+ deadline := time.Now().Add(800 * time.Millisecond)
+ box.chunk(1, true, "a\n")
+ box.result(1, "a\n")
+ box.picked(deadline)
+ w := &dbfHeldWriter{header: http.Header{}, holdOn: `"chunk":1,`, until: deadline.Add(200 * time.Millisecond)}
+ r := httptest.NewRequest(http.MethodPost, "/", nil)
+ fx.srv.streamDBRead(w, r, http.NewResponseController(w), dbReadAdmitted{id: id, region: "uk", issued: time.Now().UTC().Truncate(time.Second)})
+ w.mu.Lock()
+ lines := readLines(t, bytes.NewReader(w.buf.Bytes()))
+ w.mu.Unlock()
+ chunks := 0
+ for _, l := range lines {
+ if l.Result != nil {
+ t.Fatalf("a result relayed past the owner's deadline: %+v", l.Result)
+ }
+ if l.Chunk != 0 {
+ chunks++
+ }
+ }
+ if last := lines[len(lines)-1]; chunks != 1 || last.Ended != dbreads.EndedNoResult {
+ t.Fatalf("%d chunks, last line %+v", chunks, last)
+ }
+}
+
+// dbfDeadlineListener records, for every read deadline the server sets on
+// its connections, how far ahead it was set.
+type dbfDeadlineListener struct {
+ net.Listener
+ mu sync.Mutex
+ ahead []time.Duration
+}
+
+type dbfDeadlineConn struct {
+ net.Conn
+ l *dbfDeadlineListener
+}
+
+func (l *dbfDeadlineListener) Accept() (net.Conn, error) {
+ c, err := l.Listener.Accept()
+ if err != nil {
+ return nil, err
+ }
+ return dbfDeadlineConn{Conn: c, l: l}, nil
+}
+
+func (c dbfDeadlineConn) SetReadDeadline(t time.Time) error {
+ if !t.IsZero() {
+ c.l.mu.Lock()
+ c.l.ahead = append(c.l.ahead, time.Until(t))
+ c.l.mu.Unlock()
+ }
+ return c.Conn.SetReadDeadline(t)
+}
+
+// Plan review findings 13 and 38: admission reads under s.dbf.preHeader,
+// and once admitted the read deadline moves out to the read's bound plus a
+// minute. Observed on the connection itself: once a body has been read to
+// its end, net/http's background read clears the read deadline on its own,
+// so a stream that outlives ReadTimeout does not show the extension.
+// Mutations: drop the extension (red: no deadline past 22 minutes); drop the
+// preHeader deadline (red: none between 15 and 20 s; the server's own are
+// 10 s and 5 minutes).
+func TestDBReadSetsBothReadDeadlines(t *testing.T) {
+ fx := newDBFFixture(t, true)
+ fx.srv.dbf.poll, fx.srv.dbf.pickup = 10*time.Millisecond, 1500*time.Millisecond
+ raw, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ t.Fatal(err)
+ }
+ ln := &dbfDeadlineListener{Listener: raw}
+ hs := NewHTTPServer(fx.srv)
+ go hs.Serve(ln)
+ t.Cleanup(func() { hs.Close() })
+ body, _ := queryBody(t, "SELECT 1")
+ rawBody, _ := json.Marshal(body)
+ req, _ := http.NewRequest(http.MethodPost, "http://"+raw.Addr().String()+strings.TrimPrefix(fx.readURL(dbfHost), fx.ts.URL), bytes.NewReader(rawBody))
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", "Bearer "+fx.member)
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ io.Copy(io.Discard, res.Body)
+ res.Body.Close()
+ ln.mu.Lock()
+ defer ln.mu.Unlock()
+ var pre, extended bool
+ for _, d := range ln.ahead {
+ pre = pre || (d > 15*time.Second && d <= 20*time.Second)
+ extended = extended || d > 22*time.Minute
+ }
+ if !pre || !extended {
+ t.Fatalf("read deadlines set this far ahead: %v", ln.ahead)
+ }
+}
+
// Review Focus 3: a picked deadline far in the future does not hold the
// request past its own bound.
func TestDBReadRequestBoundClampsAForgedDeadline(t *testing.T) {
@@ -2170,11 +2525,11 @@ func TestDBReadLateWritesChangeNothing(t *testing.T) {
}
```
-Add imports `"crypto/sha256"`, `"encoding/base64"`, `"encoding/hex"`, `"slices"`, `"sync"`, `lsfile "github.com/benbjohnson/litestream/file"`, `dbdoortest`, `dbreadstest`.
+Add imports `"crypto/sha256"`, `"encoding/base64"`, `"encoding/hex"`, `"slices"`, `lsfile "github.com/benbjohnson/litestream/file"`, `dbdoortest`, `dbreadstest`.
- [ ] **Step 2: Run them to verify they fail**
-Run: `go test ./internal/console/ -run 'TestDBReadStreams|TestDBReadEnds|TestDBReadLines|TestDBReadAdmissionBounds|TestDBReadStalledStore|TestDBReadSlowRelay|TestDBReadResultWaits|TestDBReadRequestBound|TestDBReadIgnores|TestDBReadLateWrites' -count=1`
+Run: `go test ./internal/console/ -run 'TestDBReadStreams|TestDBReadEnds|TestDBReadLines|TestDBReadAdmissionBounds|TestDBReadStalledStore|TestDBReadSlowRelay|TestDBReadResultWaits|TestDBReadResultAfterTheDeadline|TestDBReadSetsBothReadDeadlines|TestDBReadRequestBound|TestDBReadIgnores|TestDBReadLateWrites' -count=1`
Expected: FAIL: the stub writes only `admitted`.
- [ ] **Step 3: Write the stream**
@@ -2239,8 +2594,9 @@ func (l *dbLineWriter) tick(ctx context.Context, phase string, poll, every time.
return nil
}
-// dbStream is one admitted read's relay, with what its last log line counts.
-type dbStream struct {
+// dbReadRelay is one admitted read's relay, with what its last log line
+// counts. (dbStream is apidb.go's streaming function: plan review finding 25.)
+type dbReadRelay struct {
s *Server
lw *dbLineWriter
chunks int
@@ -2252,7 +2608,7 @@ type dbStream struct {
// the request past its phase nor silences the stream (plan review finding
// 12). A store that ignores its context is left behind: its goroutine ends
// when the call does, into a buffered channel nobody reads.
-func dbfCall[T any](st *dbStream, ctx context.Context, phase string, op func(context.Context) (T, error)) (T, error) {
+func dbfCall[T any](st *dbReadRelay, ctx context.Context, phase string, op func(context.Context) (T, error)) (T, error) {
var zero T
cctx, cancel := context.WithTimeout(ctx, st.s.dbf.call)
defer cancel()
@@ -2312,7 +2668,7 @@ func (s *Server) streamDBRead(w http.ResponseWriter, r *http.Request, rc *http.R
w.Header().Set("Content-Type", "application/x-ndjson")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusOK)
- st := &dbStream{s: s, lw: &dbLineWriter{w: w, rc: rc, stall: s.dbf.stall, until: bound.Add(dbfEndSlack)}}
+ st := &dbReadRelay{s: s, lw: &dbLineWriter{w: w, rc: rc, stall: s.dbf.stall, until: bound.Add(dbfEndSlack)}}
start, why := time.Now(), "client gone"
defer func() {
log.Printf("console: db read %s: relayed %d chunks (%d bytes) in %s, ended %s",
@@ -2458,11 +2814,11 @@ Waits for Task 1 (the CLI rows approved).
- Create: `dbforward.go`
- Create: `dbforward_test.go`
- Modify: `db.go:187-195` (`dbRestore` hands a forward marker back), `db.go:277-290` (`dbPull`), `db.go:374-382` (`dbQuery`)
-- Modify: `approved_db_forward_copy_test.go` (DBF-3 to DBF-8, DBF-11 to DBF-13, DBF-15, DBF-16)
+- Modify: `approved_db_forward_copy_test.go` (DBF-3 to DBF-8, DBF-11 to DBF-13, DBF-15, DBF-16, DBF-32)
**Interfaces:**
- Consumes: `dbdoor.DoorError.Forward`, `dbdoor.Forward` (Task 2); `dbreads.NewReplyKey`, `EncodeReplyKey`, `ReadBody`, `Line`, `OpenChunk`, `OpenSummary`, `MaxLineBytes`, `CLIBound`, `StreamRows`, the state and code constants; `consoleClient.doStream`, `apiPath`, `apiErrorFromBody`, `dbQueryStatus`, `dbRestoreError`, `humanBytes`, `checkQuerySQL`, `stdout` (existing CLI code).
-- Produces: `errDBForward` (an error type carrying `dbdoor.Forward`); `dbQueryForward(ctx context.Context, t dbTarget, sqlText string, fwd dbdoor.Forward) error`; `readDBForward(ctx context.Context, body io.Reader, key *ecdh.PrivateKey, host, region string, status io.Writer) (dbForwardAnswer, error)`; `openDBForward(id string, key *ecdh.PrivateKey, sealed [][]byte, res dbreads.Result, host, region string) (dbForwardAnswer, error)`; `type dbForwardAnswer struct{ rows []byte; summary dbreads.Summary; capped bool }`; the `dbf*` CLI constants.
+- Produces: `errDBForward` (an error type carrying `dbdoor.Forward`); `dbQueryForward(ctx context.Context, t dbTarget, sqlText string, fwd dbdoor.Forward) error`; `readDBForward(ctx context.Context, body io.Reader, key *ecdh.PrivateKey, host, region string, status io.Writer) (dbForwardAnswer, error)`; `type dbfChunkBound`, `newDBFChunkBound(maxBytes, chunkBytes int64) *dbfChunkBound`, `(*dbfChunkBound).admit(sealed []byte) bool`; `openDBForward(id string, key *ecdh.PrivateKey, sealed [][]byte, res dbreads.Result, host, region string) (dbForwardAnswer, error)`; `type dbForwardAnswer struct{ rows []byte; summary dbreads.Summary; capped bool }`; the `dbf*` CLI constants.
- [ ] **Step 1: Write the failing tests**
@@ -2479,8 +2835,11 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
+ "net/http"
+ "net/http/httptest"
"os"
"strings"
+ "sync/atomic"
"testing"
"time"
@@ -2677,6 +3036,66 @@ func TestForwardedQueryCtrlCPrintsNothing(t *testing.T) {
}
}
+// Plan review finding 37: what the CLI holds before a result is bounded by
+// count as well as bytes, and a malformed chunk is refused before it is
+// kept. Each stream is otherwise one the CLI would accept, so each case is
+// red under its own mutation of dbfChunkBound.admit: drop the size floor
+// ("a chunk shorter than its seal" then ends DBF-12, the stream having no
+// result, instead of DBF-13 at once); drop the empty-chunk rule or the
+// count bound (the stream then opens and prints).
+func TestForwardedQueryBoundsChunkCountAndSize(t *testing.T) {
+ t.Parallel()
+ key, _ := dbreads.NewReplyKey()
+ unchecked := fmt.Sprintf(dbfUnchecked, "uk")
+ many := newDBFChunkBound(dbreads.MaxOutputBytes, dbreads.ChunkBytes).maxChunks + 1
+ for name, write := range map[string]func(s *stream){
+ "a chunk shorter than its seal": func(s *stream) {
+ s.line(dbreads.Line{Chunk: 1, Data: []byte("short")})
+ },
+ "a chunk after an empty one": func(s *stream) {
+ s.chunk(1, false, nil)
+ s.chunk(2, true, []byte("n\n"))
+ s.result(dbreads.StateDone, "", 2, summaryOf("n\n"))
+ },
+ "more chunks than a read can make": func(s *stream) {
+ for seq := int64(1); seq <= many; seq++ {
+ s.chunk(seq, seq == many, []byte("x"))
+ }
+ s.result(dbreads.StateDone, "", many, summaryOf(strings.Repeat("x", int(many))))
+ },
+ } {
+ s := newStream(t, key)
+ write(s)
+ var status bytes.Buffer
+ if _, err := readDBForward(context.Background(), &s.buf, key, "shop.example.com", "uk", &status); err == nil || err.Error() != unchecked {
+ t.Errorf("%s: %v", name, err)
+ }
+ }
+}
+
+// Plan review finding 36: SQL that JSON would rewrite is refused before
+// anything is sent. Mutation: drop the check (red: the request goes out).
+func TestForwardedQueryRefusesSQLThatIsNotUTF8(t *testing.T) {
+ t.Parallel()
+ var sent atomic.Bool
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ sent.Store(true)
+ http.Error(w, "no", http.StatusTeapot)
+ }))
+ defer ts.Close()
+ target := dbTarget{sess: &apiSession{c: newConsoleClient(ts.URL, "tok"), console: ts.URL}, acct: apiAcct{Seg: "acme"},
+ app: "shop", host: "shop.example.com"}
+ for _, q := range []string{"SELECT '\xff'", "SELECT '\uFFFD'"} {
+ err := dbQueryForward(context.Background(), target, q, dbdoor.Forward{Region: "uk", Ops: []string{dbreads.OpQuery}})
+ if err == nil || err.Error() != fmt.Sprintf(dbfNotUTF8, "uk") {
+ t.Errorf("%q: %v", q, err)
+ }
+ }
+ if sent.Load() {
+ t.Fatal("a query JSON would rewrite was sent")
+ }
+}
+
// cutReader ends like a connection the context closed.
type cutReader struct{ r *bytes.Buffer }
@@ -2742,6 +3161,7 @@ const (
dbfUnchecked = "the answer from region %s did not check out, so carlos db showed none of it. try again." // DBF-13
dbfChanged = "%s changed while the read was waiting. try again." // DBF-15
dbfMovingCLI = "%s is moving between regions. read its database once the move has finished." // DBF-16
+ dbfNotUTF8 = "this query cannot be sent to region %s, because it holds bytes that are not UTF-8 text or the character U+FFFD. fix the query and run it again." // DBF-32
)
// errDBForward is the db door's forward marker, handed back by dbRestore so
@@ -2760,6 +3180,13 @@ type dbForwardAnswer struct {
// dbQueryForward is spec §5.5 steps 2 and 8 for a query.
func dbQueryForward(ctx context.Context, t dbTarget, sqlText string, fwd dbdoor.Forward) error {
+ // json.Marshal would send U+FFFD for bytes that are not UTF-8, and the
+ // box would run SQL nobody typed; the console refuses U+FFFD for the
+ // same reason (plan review finding 36). First, so no other check can
+ // read the bytes differently.
+ if !utf8.ValidString(sqlText) || strings.ContainsRune(sqlText, utf8.RuneError) {
+ return fmt.Errorf(dbfNotUTF8, fwd.Region)
+ }
if err := checkQuerySQL(sqlText); err != nil {
return err
}
@@ -2804,6 +3231,35 @@ func dbQueryForward(ctx context.Context, t dbTarget, sqlText string, fwd dbdoor.
return nil
}
+// dbfChunkBound is what a reader keeps before a result arrives (plan review
+// finding 37). Every chunk is at least a sealed empty one; only the last
+// may be empty, so nothing may follow an empty chunk; there are at most as
+// many chunks as a read can make (full ones for the payload, plus one per
+// flush interval over the owner's deadline, plus the last); and their
+// plaintext, ciphertext less each seal, stays within the payload cap.
+type dbfChunkBound struct {
+ maxBytes, maxChunks int64
+ n, plain int64
+ sawEmpty bool
+}
+
+func newDBFChunkBound(maxBytes, chunkBytes int64) *dbfChunkBound {
+ return &dbfChunkBound{maxBytes: maxBytes,
+ maxChunks: (maxBytes+chunkBytes-1)/chunkBytes + int64(dbreads.OwnerDeadline/dbreads.ChunkEvery) + 1}
+}
+
+// admit says whether a chunk may be kept; false is DBF-13.
+func (b *dbfChunkBound) admit(sealed []byte) bool {
+ size := int64(len(sealed)) - int64(dbreads.SealOverhead)
+ if size < 0 || b.sawEmpty || b.n >= b.maxChunks || b.plain+size > b.maxBytes {
+ return false
+ }
+ b.n++
+ b.plain += size
+ b.sawEmpty = size == 0
+ return true
+}
+
// readDBForward reads the reads route's lines (spec §5.5 steps 6 to 8). It
// says DBF-11 once admitted, buffers every chunk in sequence, and opens and
// checks the answer only when the result arrives.
@@ -2813,7 +3269,7 @@ func readDBForward(ctx context.Context, body io.Reader, key *ecdh.PrivateKey, ho
sc.Buffer(make([]byte, 64<<10), dbreads.MaxLineBytes)
var id, ended string
var sealed [][]byte
- var total int64
+ bound := newDBFChunkBound(dbreads.MaxOutputBytes, dbreads.ChunkBytes)
for sc.Scan() {
var l dbreads.Line
if err := json.Unmarshal(sc.Bytes(), &l); err != nil {
@@ -2827,8 +3283,7 @@ func readDBForward(ctx context.Context, body io.Reader, key *ecdh.PrivateKey, ho
id = l.Admitted
fmt.Fprintln(status, fmt.Sprintf(dbfReading, host, region))
case l.Chunk != 0:
- total += int64(len(l.Data))
- if id == "" || l.Chunk != int64(len(sealed))+1 || total > dbreads.MaxOutputBytes+int64(len(sealed)+1)*int64(dbreads.SealOverhead) {
+ if id == "" || l.Chunk != int64(len(sealed))+1 || !bound.admit(l.Data) {
return dbForwardAnswer{}, unchecked
}
sealed = append(sealed, l.Data)
@@ -2953,7 +3408,7 @@ and in `dbPull`:
}
```
-Add `"slices"` and the `dbreads` import to `db.go`. Add the CLI ids (DBF-3 to DBF-8, DBF-11, DBF-12, DBF-13, DBF-15, DBF-16) to `dbForwardCopyBuilt`.
+Add `"slices"` and the `dbreads` import to `db.go`. Add the CLI ids (DBF-3 to DBF-8, DBF-11, DBF-12, DBF-13, DBF-15, DBF-16, DBF-32) to `dbForwardCopyBuilt`, and `"unicode/utf8"` to `dbforward.go`'s imports (and `"strings"` if it is not there).
- [ ] **Step 4: Run the tests and the gate**
@@ -3015,7 +3470,7 @@ git commit -m "docs: carlos db reads another region's instances (DBF-30)"
**Interfaces:**
- Consumes: `buildCarlos`, `useTempCredentials`, `clearDeploymentEnv`, `carlosTestEnv`, `e2eDeviceLogin`, `loadCredentials`, `credential`, `replicaS3` (`db_e2e_test.go`), `dbdoortest.Sync`, `console.Open`, `console.NewServer`, `console.NewHTTPServer`, `console.OpsAccountID`, `console.RoleOwner`, `edge.New`, `registry.Open`, `registry.Route`, `dbreadstest.Start`, `dbreads.*`, `dbdoor.RestoreLocal`, `dbdoor.NewClient`, `dbdoor.Base`, `lease.ReadReplicaClient`.
-- Produces: the harness `dbfE2E` and `newDBFE2E(t *testing.T, o dbfE2EOptions) *dbfE2E`; `(*dbfE2E).run(t, args...) (stdout, stderr string, err error)`; `(*dbfE2E).betweenTheSyncs(t) string`; `(*dbfE2E).doer(host string) dbdoor.Doer`; `(*dbfE2E).scriptBox(t, script func(*e2eScript)) (stop func())`; `e2eScript` with `picked(deadline)`, `chunk(seq, last, plain)`, `result(last, rows)`; `throughTheEdge(readTimeout time.Duration, edgeURL *string)`; `slowRestore(d time.Duration) dbreads.WorkerOptions`; and the tests below.
+- Produces: the harness `dbfE2E` and `newDBFE2E(t *testing.T, o dbfE2EOptions) *dbfE2E`; `(*dbfE2E).run(t, args...) (stdout, stderr string, err error)`; `(*dbfE2E).betweenTheSyncs(t) string`; `(*dbfE2E).doer(host string) dbdoor.Doer`; `(*dbfE2E).scriptBox(t, script func(*e2eScript)) (stop func())`; `e2eScript` with `picked(deadline)`, `chunk(seq, last, plain)`, `result(last, rows)`; `throughTheEdge(readTimeout time.Duration, edgeURL *string, tap **dbfTap)`; `dbfTap`, `newDBFTap(t, target string, connect bool) *dbfTap`, `(*dbfTap).longestGap(start, end time.Time) time.Duration`; `checkProgressAtTheCLI(t, e, tap, atLeast time.Duration)`; `slowRestore(d time.Duration) dbreads.WorkerOptions`; and the tests below.
- [ ] **Step 1: Write the harness**
@@ -3052,6 +3507,7 @@ import (
"os/exec"
"path/filepath"
"strings"
+ "sync"
"testing"
"time"
@@ -3355,7 +3811,7 @@ func (e *dbfE2E) scriptBox(t *testing.T, script func(*e2eScript)) func() {
// 127.0.0.1, its ReadTimeout set to readTimeout, behind a real edge route
// for console.test. The CLI reaches it by that name through HTTP_PROXY,
// which stands in for DNS.
-func throughTheEdge(readTimeout time.Duration, edgeURL *string) func(t *testing.T, h http.Handler) (string, []string) {
+func throughTheEdge(readTimeout time.Duration, edgeURL *string, tap **dbfTap) func(t *testing.T, h http.Handler) (string, []string) {
return func(t *testing.T, h http.Handler) (string, []string) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
@@ -3382,7 +3838,146 @@ func throughTheEdge(readTimeout time.Duration, edgeURL *string) func(t *testing.
es := httptest.NewServer(ed)
t.Cleanup(es.Close)
*edgeURL = es.URL
- return "http://console.test", []string{"HTTP_PROXY=" + es.URL, "NO_PROXY="}
+ // The CLI's proxy is a tap in front of the edge, so the test sees
+ // what reaches the CLI's own socket (plan review finding 34).
+ tp := newDBFTap(t, es.Listener.Addr().String(), false)
+ *tap = tp
+ return "http://console.test", []string{"HTTP_PROXY=http://" + tp.addr, "NO_PROXY="}
+ }
+}
+
+// dbfTap stands between the CLI and the first edge, forwarding bytes both
+// ways and noting when each read from the edge's side was passed to the
+// CLI's socket: what the CLI process received, and when. With connect it is
+// an HTTPS proxy (CONNECT), which is how the CLI reaches a TLS edge by name
+// here; without, a plain forwarder for HTTP_PROXY.
+type dbfTap struct {
+ addr string
+ mu sync.Mutex
+ down []time.Time
+}
+
+func newDBFTap(t *testing.T, target string, connect bool) *dbfTap {
+ t.Helper()
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { ln.Close() })
+ tp := &dbfTap{addr: ln.Addr().String()}
+ go func() {
+ for {
+ c, err := ln.Accept()
+ if err != nil {
+ return
+ }
+ go tp.serve(c, target, connect)
+ }
+ }()
+ return tp
+}
+
+func (tp *dbfTap) serve(c net.Conn, target string, connect bool) {
+ defer c.Close()
+ var in io.Reader = c
+ if connect {
+ br := bufio.NewReader(c)
+ req, err := http.ReadRequest(br)
+ if err != nil || req.Method != http.MethodConnect {
+ return
+ }
+ if _, err := io.WriteString(c, "HTTP/1.1 200 Connection established\r\n\r\n"); err != nil {
+ return
+ }
+ in = br
+ }
+ up, err := net.Dial("tcp", target)
+ if err != nil {
+ return
+ }
+ defer up.Close()
+ go func() {
+ io.Copy(up, in)
+ up.(*net.TCPConn).CloseWrite()
+ }()
+ buf := make([]byte, 32<<10)
+ for {
+ n, err := up.Read(buf)
+ if n > 0 {
+ tp.mu.Lock()
+ tp.down = append(tp.down, time.Now())
+ tp.mu.Unlock()
+ if _, werr := c.Write(buf[:n]); werr != nil {
+ return
+ }
+ }
+ if err != nil {
+ return
+ }
+ }
+}
+
+// longestGap is the longest silence at the CLI's socket between start and
+// end, both of which count as arrivals.
+func (tp *dbfTap) longestGap(start, end time.Time) time.Duration {
+ tp.mu.Lock()
+ defer tp.mu.Unlock()
+ prev, longest := start, time.Duration(0)
+ for _, at := range append(append([]time.Time(nil), tp.down...), end) {
+ if at.Before(start) || at.After(end) {
+ continue
+ }
+ if d := at.Sub(prev); d > longest {
+ longest = d
+ }
+ prev = at
+ }
+ return longest
+}
+
+// checkProgressAtTheCLI is T7's witness at the CLI process itself (plan
+// review finding 34): it runs the real CLI and needs DBF-11 on its stderr
+// within a second of the process starting (stricter than the spec's "of the
+// request": the process starts before it sends anything), no silence over
+// 16 s (15 s and a poll) at its socket, the read lasting at least atLeast, and
+// its rows. atLeast is the quiet restore's length.
+func checkProgressAtTheCLI(t *testing.T, e *dbfE2E, tap *dbfTap, atLeast time.Duration) {
+ t.Helper()
+ cmd := exec.Command(e.bin, "db", "query", "--app", "shop", "--instance", dbfE2EFar, "--sql", "SELECT count(*) AS n FROM orders")
+ cmd.Env = carlosTestEnv(t, append([]string{"CARLOS_CREDENTIALS=" + e.creds}, e.env...)...)
+ cmd.Dir = t.TempDir()
+ var out bytes.Buffer
+ cmd.Stdout = &out
+ pipe, err := cmd.StderrPipe()
+ if err != nil {
+ t.Fatal(err)
+ }
+ start := time.Now()
+ if err := cmd.Start(); err != nil {
+ t.Fatal(err)
+ }
+ var stderr []string
+ var reading time.Time
+ sc := bufio.NewScanner(pipe)
+ for sc.Scan() {
+ if reading.IsZero() && strings.Contains(sc.Text(), fmt.Sprintf(dbfReading, dbfE2EFar, "uk")) {
+ reading = time.Now()
+ }
+ stderr = append(stderr, sc.Text())
+ }
+ err = cmd.Wait()
+ end := time.Now()
+ if err != nil || out.String() != "n\n3\n" {
+ t.Fatalf("%q %v\n%s", out.String(), err, strings.Join(stderr, "\n"))
+ }
+ if reading.IsZero() || reading.Sub(start) > time.Second {
+ t.Fatalf("DBF-11 after %s: %q", reading.Sub(start), stderr)
+ }
+ if gap := tap.longestGap(start, end); gap > 16*time.Second {
+ t.Fatalf("%s of silence at the CLI's socket", gap)
+ }
+ if took := end.Sub(start); took < atLeast {
+ t.Fatalf("the read took %s, under %s: it did not hold the request", took, atLeast)
}
}
@@ -3441,7 +4036,8 @@ func TestDBForwardParityThroughTheCLI(t *testing.T) {
// gets its rows through the same path.
func TestDBForwardLongRequestThroughTheEdge(t *testing.T) {
var edgeURL string
- e := newDBFE2E(t, dbfE2EOptions{forward: true, serve: throughTheEdge(2*time.Second, &edgeURL), worker: slowRestore(20 * time.Second)})
+ var tap *dbfTap
+ e := newDBFE2E(t, dbfE2EOptions{forward: true, serve: throughTheEdge(2*time.Second, &edgeURL, &tap), worker: slowRestore(20 * time.Second)})
key, _ := dbreads.NewReplyKey()
body, _ := json.Marshal(dbreads.ReadBody{Op: dbreads.OpQuery, SQL: "SELECT count(*) AS n FROM orders", ReplyKey: dbreads.EncodeReplyKey(key.PublicKey())})
req, _ := http.NewRequest(http.MethodPost, edgeURL+"/api/cli/apps/"+release.OpsAccount+"/shop/db/"+dbfE2EFar+"/reads", bytes.NewReader(body))
@@ -3477,10 +4073,9 @@ func TestDBForwardLongRequestThroughTheEdge(t *testing.T) {
if !sawResult || time.Since(start) < 20*time.Second {
t.Fatalf("result %v after %s", sawResult, time.Since(start))
}
- out, stderr, err := e.run(t, "db", "query", "--app", "shop", "--instance", dbfE2EFar, "--sql", "SELECT count(*) AS n FROM orders")
- if err != nil || out != "n\n3\n" {
- t.Fatalf("%q %v\n%s", out, err, stderr)
- }
+ // The same through the real CLI process, observed at its own stderr and
+ // socket (plan review finding 34).
+ checkProgressAtTheCLI(t, e, tap, 20*time.Second)
}
// T7, unscaled: a read that holds the request for 21 minutes through the
@@ -3494,7 +4089,8 @@ func TestDBForwardTwentyOneMinuteRead(t *testing.T) {
t.Skip("set CARLOS_DBF_LONG=1 to run the unscaled 21-minute read, once, before handoff")
}
var edgeURL string
- e := newDBFE2E(t, dbfE2EOptions{forward: true, noBox: true, serve: throughTheEdge(5*time.Minute, &edgeURL)})
+ var tap *dbfTap
+ e := newDBFE2E(t, dbfE2EOptions{forward: true, noBox: true, serve: throughTheEdge(5*time.Minute, &edgeURL, &tap)})
stop := e.scriptBox(t, func(b *e2eScript) {
time.Sleep(110 * time.Second)
b.picked(time.Now().Add(20 * time.Minute))
@@ -3569,13 +4165,28 @@ func TestDBForwardPrintsNothingForABadAnswer(t *testing.T) {
})
}
}
+
+// Plan review finding 36, through the binary: a query holding a byte that
+// is not UTF-8 is refused with DBF-32 before anything is sent, so no altered
+// query reaches the bucket. (An argv may carry any bytes on Linux, where
+// this runs.)
+func TestDBForwardRefusesSQLThatIsNotUTF8(t *testing.T) {
+ e := newDBFE2E(t, dbfE2EOptions{forward: true, noBox: true})
+ out, stderr, err := e.run(t, "db", "query", "--app", "shop", "--instance", dbfE2EFar, "--sql", "SELECT 'caf\xe9'")
+ if err == nil || out != "" || !strings.Contains(stderr, fmt.Sprintf(dbfNotUTF8, "uk")) {
+ t.Fatalf("%q %q %v", out, stderr, err)
+ }
+ if keys, _ := e.store.List(context.Background(), dbreads.Prefix); len(keys) != 0 {
+ t.Fatalf("a query JSON would rewrite reached the bucket: %v", keys)
+ }
+}
```
- [ ] **Step 3: Run them**
Run (sandbox off; they build the CLI binary):
```bash
-go test . -run 'TestDBForwardParityThroughTheCLI|TestDBForwardMixedVersions|TestDBForwardPrintsNothingForABadAnswer|TestDBForwardLongRequestThroughTheEdge' -count=1 -v
+go test . -run 'TestDBForwardParityThroughTheCLI|TestDBForwardMixedVersions|TestDBForwardPrintsNothingForABadAnswer|TestDBForwardRefusesSQLThatIsNotUTF8|TestDBForwardLongRequestThroughTheEdge' -count=1 -v
```
Expected: PASS; `TestDBForwardTwentyOneMinuteRead` skips. If the edge answers the CLI with 404, the request reached it with a host other than `console.test`: print `r.Host` in a wrapper around `ed` to see what Go's proxy client sent.
@@ -3588,63 +4199,54 @@ git commit -m "db forward: T6, T7, T8 and T11 through the real CLI binary and co
---
-### Task 11: T7's backhaul half
+### Task 11: T7's backhaul half, through the real CLI and console
**Files:**
-- Create: `internal/edge/dbforward_backhaul_test.go`
+- Create: `internal/edge/dbforward_backhaul_helper_test.go`
+- Modify: `dbforward_e2e_test.go`
**Interfaces:**
-- Consumes (package `edge` test helpers in `backhaul_e2e_test.go`): `newE2ECA`, `(*e2eCA).publish`, `newBackhaulEdge`, `refreshHomeMap`, `MintMeshKey`, `putJSON`, `e2eQuietLogs`, `backhaulPort`, `backhaulRootCAs`, `backhaulHeaderTimeout`.
+- Consumes (package `edge` test helpers in `backhaul_e2e_test.go`): `newE2ECA`, `(*e2eCA).publish`, the `e2eCA.root` field, `newBackhaulEdge`, `(*backhaulEdge).tlsAddr`, `.mirror`, `.homes`, `refreshHomeMap`, `MintMeshKey`, `putJSON`, `e2eQuietLogs`, the seams `backhaulPort` and `backhaulRootCAs`. From Task 10: `newDBFE2E`, `slowRestore`, `dbfTap`, `newDBFTap`, `checkProgressAtTheCLI`.
+- Produces: `TestDBForwardBackhaulPairHelper` (a helper process, skipped unless `DBF_BACKHAUL_HELPER=1`); `throughBackhaul(readTimeout time.Duration, tap **dbfTap)`; `TestDBForwardThroughTheBackhaulHop`.
+
+Spec T7 needs the real CLI and the real console through the backhaul hop (plan review finding 34). The console cannot be imported by package `edge`'s tests (the console imports the edge, a cycle), and the backhaul harness is package `edge` test code that nothing else can import. So the acceptance test lives in the root package with the real CLI and console, and the two edges run in package `edge`'s own test binary as a helper process, built by the root test with `go test -c`.
-The console cannot be imported here (the console imports the edge, so a package `edge` test importing it is a cycle). The upstream is a stand-in that streams the forwarded read's shape and timing: headers and `admitted` at once, then a line every 5 s for longer than the backhaul hop's 30 s header bound. The real console's flushing is T7a's (Task 10) and Task 7's.
+- [ ] **Step 1: Write the helper**
-- [ ] **Step 1: Write the test**
+Create `internal/edge/dbforward_backhaul_helper_test.go`:
```go
package edge
import (
- "bufio"
"context"
+ "encoding/pem"
"fmt"
+ "io"
"net"
- "net/http"
- "net/http/httptest"
- "strings"
+ "os"
+ "path/filepath"
"testing"
- "time"
"github.com/carlosframework/platform/internal/release"
)
const dbfBackhaulHost = "console.backhaul.test"
-// dbfStream is a stand-in for the console's reads route: not acceptance. It
-// writes and flushes admitted with the headers, then a wait line every
-// 5 s, and a result after total.
-func dbfStream(total time.Duration) *httptest.Server {
- return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "application/x-ndjson")
- w.Header().Set("Cache-Control", "no-store")
- w.WriteHeader(http.StatusOK)
- rc := http.NewResponseController(w)
- fmt.Fprintln(w, `{"admitted":"dbr_aaaaaaaaaaaaaaaaaaaaaaaaaa"}`)
- rc.Flush()
- end := time.Now().Add(total)
- for time.Now().Before(end) {
- time.Sleep(5 * time.Second)
- fmt.Fprintln(w, `{"wait":"reading"}`)
- rc.Flush()
- }
- fmt.Fprintln(w, `{"result":{"state":"done","code":"","box":"uk-1","last_seq":1,"sealed":""}}`)
- }))
-}
-
-// T7, the backhaul half (codex 19): a forwarded read that reaches the
-// console through another region's edge survives the hop's 30 s header
-// bound, because the headers leave at once, and every line arrives as it is
-// written.
-func TestDBForwardStreamSurvivesTheBackhaulHop(t *testing.T) {
+// TestDBForwardBackhaulPairHelper is not a test of its own: the root
+// package's TestDBForwardThroughTheBackhaulHop builds this package's test
+// binary and runs this as a helper process. It stands up two real edges
+// over one bucket, as TestBackhaulE2EAcrossTwoEdges does: annie, home of
+// console.backhaul.test, routed to DBF_BACKHAUL_UPSTREAM (the real console
+// the parent serves), and betty, which serves no host and so backhauls every
+// request for it to annie. It writes the test CA to DBF_BACKHAUL_DIR/ca.pem,
+// prints betty's TLS address on a line starting "DBF-BACKHAUL ", and serves
+// until its stdin closes.
+func TestDBForwardBackhaulPairHelper(t *testing.T) {
+ if os.Getenv("DBF_BACKHAUL_HELPER") != "1" {
+ t.Skip("a helper process for the root package's TestDBForwardThroughTheBackhaulHop")
+ }
+ upstream, dir := os.Getenv("DBF_BACKHAUL_UPSTREAM"), os.Getenv("DBF_BACKHAUL_DIR")
e2eQuietLogs(t)
bucket := t.TempDir()
seed := release.NewDirStore(bucket)
@@ -3653,16 +4255,20 @@ func TestDBForwardStreamSurvivesTheBackhaulHop(t *testing.T) {
}
ca := newE2ECA(t)
ca.publish(t, seed, dbfBackhaulHost)
- upstream := dbfStream(backhaulHeaderTimeout + 10*time.Second)
- t.Cleanup(upstream.Close)
- home := newBackhaulEdge(t, bucket, "annie", ca, nil, map[string]string{dbfBackhaulHost: upstream.Listener.Addr().String()})
+ home := newBackhaulEdge(t, bucket, "annie", ca, nil, map[string]string{dbfBackhaulHost: upstream})
sender := newBackhaulEdge(t, bucket, "betty", ca, nil, nil)
- eip, port, _ := net.SplitHostPort(home.tlsAddr)
+ eip, port, err := net.SplitHostPort(home.tlsAddr)
+ if err != nil {
+ t.Fatal(err)
+ }
oldPort, oldCAs := backhaulPort, backhaulRootCAs
backhaulPort, backhaulRootCAs = port, ca.pool
t.Cleanup(func() { backhaulPort, backhaulRootCAs = oldPort, oldCAs })
putJSON(t, seed, "control/edges/annie.json", fmt.Sprintf(`{"v":1,"box":"annie","eip":%q,"hosts":{%q:{"role":"home"}}}`, eip, dbfBackhaulHost))
- bEIP, _, _ := net.SplitHostPort(sender.tlsAddr)
+ bEIP, _, err := net.SplitHostPort(sender.tlsAddr)
+ if err != nil {
+ t.Fatal(err)
+ }
putJSON(t, seed, "control/edges/betty.json", fmt.Sprintf(`{"v":1,"box":"betty","eip":%q,"hosts":{}}`, bEIP))
for _, be := range []*backhaulEdge{home, sender} {
if err := be.mirror.Refresh(context.Background()); err != nil {
@@ -3670,48 +4276,113 @@ func TestDBForwardStreamSurvivesTheBackhaulHop(t *testing.T) {
}
refreshHomeMap(t, be.homes)
}
- client := sender.client
- client.Timeout = 0 // the stream outlives the harness's 30 s client bound
- start := time.Now()
- res, err := client.Post("https://"+dbfBackhaulHost+"/api/cli/apps/bab/shop/db/shop.example.com/reads", "application/json", strings.NewReader(`{}`))
- if err != nil {
+ caPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.root.Raw})
+ if err := os.WriteFile(filepath.Join(dir, "ca.pem"), caPEM, 0o600); err != nil {
t.Fatal(err)
}
- defer res.Body.Close()
- if time.Since(start) > 2*time.Second || res.StatusCode != http.StatusOK {
- t.Fatalf("headers after %s: %d", time.Since(start), res.StatusCode)
- }
- br := bufio.NewReader(res.Body)
- prev, lines := time.Now(), 0
- for {
- line, err := br.ReadString('\n')
+ fmt.Println("DBF-BACKHAUL " + sender.tlsAddr)
+ io.Copy(io.Discard, os.Stdin)
+}
+```
+
+Run (sandbox off): `go test ./internal/edge/ -run 'TestDBForwardBackhaulPairHelper' -count=1`
+Expected: `ok` with the test skipped.
+
+- [ ] **Step 2: Write the acceptance test**
+
+Append to `dbforward_e2e_test.go`:
+
+```go
+// throughBackhaul serves the console's handler with console.NewHTTPServer
+// (its ReadTimeout set to readTimeout) on 127.0.0.1, behind two real edges
+// that package edge's test binary runs as a helper process: the CLI reaches
+// betty, which backhauls to annie, the console's home. The CLI finds betty
+// through a CONNECT proxy (a dbfTap, standing in for DNS) and trusts the
+// edges' test CA through SSL_CERT_FILE.
+func throughBackhaul(readTimeout time.Duration, tap **dbfTap) func(t *testing.T, h http.Handler) (string, []string) {
+ return func(t *testing.T, h http.Handler) (string, []string) {
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
- break
+ t.Fatal(err)
+ }
+ hs := console.NewHTTPServer(h)
+ hs.ReadTimeout = readTimeout
+ go hs.Serve(ln)
+ t.Cleanup(func() { hs.Close() })
+ dir := t.TempDir()
+ bin := filepath.Join(dir, "edge.test")
+ if out, err := exec.Command("go", "test", "-c", "-o", bin, "./internal/edge").CombinedOutput(); err != nil {
+ t.Fatalf("build the edge test binary: %v\n%s", err, out)
}
- if gap := time.Since(prev); gap > 7*time.Second {
- t.Fatalf("line %d arrived %s after the last: held by the hop", lines, gap)
+ cmd := exec.Command(bin, "-test.run", "^TestDBForwardBackhaulPairHelper$", "-test.v")
+ cmd.Env = append(os.Environ(), "DBF_BACKHAUL_HELPER=1", "DBF_BACKHAUL_UPSTREAM="+ln.Addr().String(), "DBF_BACKHAUL_DIR="+dir)
+ stdin, err := cmd.StdinPipe()
+ if err != nil {
+ t.Fatal(err)
}
- prev, lines = time.Now(), lines+1
- if strings.Contains(line, `"result"`) {
- break
+ pr, pw := io.Pipe()
+ cmd.Stdout = pw
+ var helperErr bytes.Buffer
+ cmd.Stderr = &helperErr
+ if err := cmd.Start(); err != nil {
+ t.Fatal(err)
}
- }
- if elapsed := time.Since(start); elapsed < backhaulHeaderTimeout || lines < 8 {
- t.Fatalf("%d lines over %s", lines, elapsed)
- }
+ t.Cleanup(func() {
+ stdin.Close()
+ cmd.Wait()
+ pw.Close()
+ if t.Failed() {
+ t.Logf("backhaul helper stderr:\n%s", helperErr.String())
+ }
+ })
+ lines := bufio.NewScanner(pr)
+ sender := ""
+ for lines.Scan() {
+ if addr, ok := strings.CutPrefix(lines.Text(), "DBF-BACKHAUL "); ok {
+ sender = addr
+ break
+ }
+ }
+ if sender == "" {
+ t.Fatalf("the backhaul helper never came up:\n%s", helperErr.String())
+ }
+ go func() {
+ for lines.Scan() {
+ }
+ }()
+ tp := newDBFTap(t, sender, true)
+ *tap = tp
+ return "https://console.backhaul.test", []string{"HTTPS_PROXY=http://" + tp.addr, "NO_PROXY=", "SSL_CERT_FILE=" + filepath.Join(dir, "ca.pem")}
+ }
+}
+
+// T7, the backhaul half, end to end (plan review finding 34): the real CLI
+// binary reaches the real console through two real edges, the second hop a
+// backhaul with its 30 s header bound, during a 40 s quiet restore. At the
+// CLI's own stderr and socket: DBF-11 within a second, no silence over 16 s,
+// and its rows after more than the hop's 30 s. The console's real
+// http.Server runs with ReadTimeout 2 s, as in
+// TestDBForwardLongRequestThroughTheEdge.
+// Mutations: drop the stream's flush (red: the wait lines sit in the
+// console's buffer and the CLI's socket is silent); stream nothing before
+// the box's first chunk (red: no headers inside the hop's 30 s).
+func TestDBForwardThroughTheBackhaulHop(t *testing.T) {
+ var tap *dbfTap
+ e := newDBFE2E(t, dbfE2EOptions{forward: true, serve: throughBackhaul(2*time.Second, &tap), worker: slowRestore(40 * time.Second)})
+ checkProgressAtTheCLI(t, e, tap, 40*time.Second)
}
```
-- [ ] **Step 2: Run it**
+- [ ] **Step 3: Run it**
-Run (sandbox off): `go test ./internal/edge/ -run 'TestDBForwardStreamSurvivesTheBackhaulHop' -count=1 -v`
-Expected: PASS after about 40 s. If the sender answers 404 instead of backhauling, the home map did not see annie's edges file: compare with `newBackhaulScene`'s order (mesh key, certificates, edges, `converge`) and follow it.
+Run (sandbox off; it builds the CLI and the edge test binary): `go test . -run 'TestDBForwardThroughTheBackhaulHop' -count=1 -v`
+Expected: PASS after about 45 s. If the helper never comes up, its stderr is in the log. If the CLI gets 404 from betty instead of a backhaul, betty's home map did not see annie's edges file: compare with `newBackhaulScene`'s order (mesh key, certificates, edges files, then each edge's mirror and home map) and follow it. If the CLI fails its TLS handshake, check that `SSL_CERT_FILE` reached it (`carlosTestEnv` appends the harness's extras last).
-- [ ] **Step 3: Commit**
+- [ ] **Step 4: Commit**
```bash
-git add internal/edge/dbforward_backhaul_test.go
-git commit -m "edge: a forwarded read's stream survives the backhaul hop (T7)"
+git add internal/edge/dbforward_backhaul_helper_test.go dbforward_e2e_test.go
+git commit -m "db forward: T7 through the backhaul hop with the real CLI and console"
```
---
@@ -4315,13 +4986,18 @@ In a copy of the tree (`cp -a . "$TMPDIR/dbf2-mut"`), make each mutation, run it
| in `dbreads` `attempt`, open the replica at a prefix from anything but `rec.Host` (for example the first host the agent ever checked) | `TestDBForwardMinio` (the second tenant's rows), in the MinIO step; `TestDBReadsParityWithTheInRegionDoor` |
| `openDBForward` skips the byte count and SHA-256 comparison | `TestForwardedQueryRefusesAnAnswerThatDoesNotCheckOut`, `TestDBForwardPrintsNothingForABadAnswer` |
| `admitDBRead` releases the counts before returning, or `handleAPIDBRead` drops its `defer` | `TestDBReadAdmissionBounds` |
-| `handleAPIDBRead` drops `SetReadDeadline` | `TestDBForwardLongRequestThroughTheEdge` |
-| `dbLineWriter.write` drops `Flush` | `TestDBReadLinesAreFlushedAsWritten`, `TestDBForwardLongRequestThroughTheEdge` |
-| `streamDBRead` writes the result line without waiting for `next > res.LastSeq` | `TestDBReadResultWaitsForItsLastChunk` (the chunk-count check) |
+| `handleAPIDBRead` drops the post-admission `SetReadDeadline` (plan review finding 38) | `TestDBReadSetsBothReadDeadlines` (no deadline past 22 minutes); a finished body lets net/http clear the read deadline itself, so `TestDBForwardLongRequestThroughTheEdge` is not its witness |
+| `dbLineWriter.write` drops `Flush` | `TestDBReadLinesAreFlushedAsWritten`, `TestDBForwardLongRequestThroughTheEdge`, `TestDBForwardThroughTheBackhaulHop` |
+| `streamDBRead` writes the result line without waiting for `next > res.LastSeq` (plan review finding 31) | `TestDBReadResultWaitsForAChunkTheListingMissed` |
| `streamDBRead` lists chunks before reading `result.json`, the wait left in place (plan review finding 20) | `TestDBReadResultWaitsForItsLastChunk` (the operation-order check: a `list` not preceded by its round's `result`) |
| `admitDBRead` writes the audit row after the creates (plan review finding 11) | `TestDBReadEndedDuringAdmissionWritesOnlyTheCancel` ("the audit row is down before the request") |
-| `admitDBRead` calls `s.bumpSerial` instead of `announceDBRead`, or drops the `ctx.Err()` check after the queue create | `TestDBReadEndedDuringAdmissionWritesOnlyTheCancel` ("ended after the queue entry": the serial moves) |
-| `admitDBRead` drops `cancelDBRead` on the ended path | `TestDBReadEndedDuringAdmissionWritesOnlyTheCancel` (no cancel) |
+| `admitDBRead` drops any one of its four `ended()` checks (plan review finding 28) | `TestDBReadEndedDuringAdmissionWritesOnlyTheCancel`, the "ended before <step>" case for that step |
+| `admitDBRead` drops `cancelDBRead` on a published step | `TestDBReadEndedDuringAdmissionWritesOnlyTheCancel` ("ended before queue", "ended before serial": no cancel) |
+| `admitDBRead` calls `s.bumpSerial` instead of `announceDBRead` (plan review finding 33) | `TestDBReadEndedDuringAdmissionWritesOnlyTheCancel` ("ended during the serial read": the detached write lands) |
+| `admitDBRead` drops the U+FFFD refusal (plan review finding 36) | `TestDBReadRouteRefusesBeforeAdmitting` (the two raw bodies) |
+| `dbQueryForward` drops the UTF-8 check | `TestForwardedQueryRefusesSQLThatIsNotUTF8`, `TestDBForwardRefusesSQLThatIsNotUTF8` |
+| `dbfChunkBound.admit` drops its size floor, its empty-chunk rule or its count bound (plan review finding 37) | `TestForwardedQueryBoundsChunkCountAndSize`, the matching case |
+| `streamDBRead` drops the `ownCtx` check before the result line (plan review finding 35) | `TestDBReadResultAfterTheDeadlineIsNotRelayed` |
| `handleAPIDBRead` drops the `preHeader` read deadline (plan review finding 13) | `TestDBReadStalledBodyIsCutAtThePreHeaderBound` |
| `streamDBRead` calls the store on `r.Context()` without `dbfCall`, or drops either phase's deadline context (plan review finding 12) | `TestDBReadStalledStoreKeepsTheStreamAndItsBound` |
| `streamDBRead` relays without the owner's deadline (chunk Gets on `r.Context()`, no `ownCtx` check after them) | `TestDBReadSlowRelayEndsAtTheBound` |
@@ -4331,8 +5007,6 @@ In a copy of the tree (`cp -a . "$TMPDIR/dbf2-mut"`), make each mutation, run it
Record the table with every row red in the branch description.
-One check is not in the table on purpose: the `ownCtx.Err()` check just before the result line. `dbfCall` already cuts every store call once the owner's deadline passes, so that check guards only a line write that outlasts the deadline, which no in-process client can force. It stays as defence in depth, and the branch description says it has no witness of its own.
-
- [ ] **Step 2: Merge main and run the census**
```bash
diff --git a/docs/superpowers/plans/2026-10-22-db-region-forward-plan-3-pull.md b/docs/superpowers/plans/2026-10-22-db-region-forward-plan-3-pull.md
index 253a9221d..12be46399 100644
--- a/docs/superpowers/plans/2026-10-22-db-region-forward-plan-3-pull.md
+++ b/docs/superpowers/plans/2026-10-22-db-region-forward-plan-3-pull.md
@@ -54,6 +54,17 @@ Astra's findings (`docs/superpowers/specs/CODEX-FINDINGS-DB-REGION-FORWARD-PLANS
None declined.
+## Plan review, round 2
+
+From `CODEX-FINDINGS-DB-REGION-FORWARD-PLANS-R2.md`:
+
+- **26** (Task 5 dropped plan 1's timing lines and left `attemptStart` unused): the replacement keeps both lines, the first saying `held through` for a pull. `TestPullShipsThePlannedFiles` asserts them.
+- **32** (the hash test failed before the hash check): the scripted answer is far's real planned files with a valid manifest; only the digest is wrong.
+- **37** (the spool's growth): `readDBForward` takes plan 2's `dbfChunkBound` and checks it before the sink keeps anything. Test: `TestForwardedPullSpoolsNoMalformedOrExcessChunk`.
+- The Ctrl-C test now holds after `lines[:3]` (admitted, picked, chunk 1); `lines[:2]` would never have spooled a chunk.
+
+None declined.
+
## Census impact
- **Plan 14 root-read census:** one overlay, `plan14-root-reads-db-region-forward-3.tsv`, `rev3=PROPOSED-db-region-forward-3`: the worker's pull units (receive with LTX verification, send back) at J4 as the db-read worker; the agent's changed read units re-frozen at plan 1's dispositions with the pull appended; the console's changed reads-route units re-frozen at plan 2's; new CLI units for the pull (the spool directory's writes and reads, the file replica it writes, the restore) at J4 as the carlos CLI; envelopes `.`, `internal/dbreads`, `internal/console`, `internal/host` (for the root-runner dispatch, tests only, so only if the census names it). The seal moves.
@@ -110,7 +121,7 @@ wc -c "$TMPDIR/dbf-measure.out"; rm "$TMPDIR/dbf-measure.out"
That is about 64 MB of rows (the output cap; the command ends with DBF-6, which is expected). Run it three times. Each run leaves two lines that plans 1 and 2 write for this measurement (plan review finding 21):
-- the box's, in `journalctl -u carlos-edge` on the uk box (read through the operator's usual access): `dbreads: <id>: output <B> bytes in <N> confirmed chunks in <T_box>`, the time from `run` to the last chunk's confirmed create, which excludes planning and restore (the line before it, `planned ... and restored to ... in ...`, is the restore's);
+- the box's, in `journalctl -u carlos-edge` on the uk box (read through the operator's usual access): `dbreads: <id>: output <B> bytes in <N> confirmed chunks in <T_box>`, the time from `run` to the last chunk's confirmed create, which excludes planning and restore (the line before it, `planned ... and restored to ... in ...`, is the restore's; a pull's says `held through`);
- the console's, in `journalctl -u console`: `console: db read <id>: relayed <N> chunks (<B'> bytes) in <T_console>, ended <why>`, the whole stream from the 200 to its last line.
The box's rate is B / T_box. The console's is B' / (T_console − T_plan), where T_plan is the `planned ... in <T_plan>` line's duration; that leaves the pickup inside the console's time, so it understates the console's rate, which only makes the cap smaller. R is the slower of the two, in MB/s. Record all three runs, with both lines of each.
@@ -153,18 +164,18 @@ Add a "Pull (plan 3)" table to the record with DBF-17, DBF-18, DBF-19, DBF-20, D
```go
var dbForwardCopyIDs = func() []string {
- ids := make([]string, 0, 26)
+ ids := make([]string, 0, 27)
for i := 1; i <= 24; i++ {
if i == 21 || i == 22 {
continue // appended below, in the record's order
}
ids = append(ids, "DBF-"+strconv.Itoa(i))
}
- return append(ids, "DBF-21", "DBF-22", "DBF-30", "DBF-31")
+ return append(ids, "DBF-21", "DBF-22", "DBF-30", "DBF-31", "DBF-32")
}()
```
-The list is DBF-1 to DBF-20, DBF-23, DBF-24, then DBF-21, DBF-22, DBF-30 and DBF-31; `checkMovesInventory` compares the sets. Run `go test . -run 'TestDBForwardCopy' -count=1`. Expected: PASS.
+The list is DBF-1 to DBF-20, DBF-23, DBF-24, then DBF-21, DBF-22, DBF-30, DBF-31 and DBF-32; `checkMovesInventory` compares the sets. Run `go test . -run 'TestDBForwardCopy' -count=1`. Expected: PASS.
- [ ] **Step 2: Review**
@@ -507,7 +518,7 @@ git commit -m "dbreads: the worker's hold mode for a pull: verify, hold, send ba
- [ ] **Step 1: Write the failing tests**
-Append to `internal/dbreads/pull_test.go`:
+Append to `internal/dbreads/pull_test.go`, and add `"fmt"`, `"strings"` and `"sync"` to its imports:
```go
func (fx *fixture) pullRequest(at string) Request {
@@ -544,7 +555,15 @@ func (fx *fixture) answerPull(id string) ([]byte, Summary, Result) {
func TestPullShipsThePlannedFiles(t *testing.T) {
t.Parallel()
fx := newFixture(t, 4)
- a, _ := fx.agent("uk-1")
+ var logMu sync.Mutex
+ var logs []string
+ a, _ := fx.agent("uk-1", func(c *AgentConfig, _ *inProcWorkers) {
+ c.Logf = func(format string, args ...any) {
+ logMu.Lock()
+ defer logMu.Unlock()
+ logs = append(logs, fmt.Sprintf(format, args...))
+ }
+ })
req := fx.pullRequest("")
fx.submit(req)
a.Pass(context.Background())
@@ -566,6 +585,13 @@ func TestPullShipsThePlannedFiles(t *testing.T) {
t.Fatalf("chunk %d is %d bytes", seq, len(sealed))
}
}
+ // Task 1's measurement reads these two lines (plan review finding 26).
+ logMu.Lock()
+ all := strings.Join(logs, "\n")
+ logMu.Unlock()
+ if !strings.Contains(all, "and held through") || !strings.Contains(all, "confirmed chunks in") {
+ t.Fatalf("the pull's timing lines are missing:\n%s", all)
+ }
}
func TestPullTooLargeAndNoChunkBeforeCheckB(t *testing.T) {
@@ -710,7 +736,7 @@ In `read.go`'s `attempt`, replace plan 1's restored-size check and its `a.adm.gr
}
```
-and replace everything from plan 1's `stopPlanBound()` after check B through its `s := Summary{...}` line with this, which chooses the stream, the chunk size, the bound and the summary:
+and replace everything from plan 1's `stopPlanBound()` after check B through its `s := Summary{...}` line with this, which chooses the stream, the chunk size, the bound and the summary, and keeps plan 1's two timing lines and its `attemptStart` and `runStart` uses (plan review finding 26):
```go
stopPlanBound() // the run is bounded by the worker and the owner's deadline
@@ -718,12 +744,22 @@ and replace everything from plan 1's `stopPlanBound()` after check B through its
if pull {
stream, size, max = StreamLTX, PullChunkBytes, plan.LTXBytes
}
+ // Plan 1's phase timings, kept for both ops (plan review finding 26;
+ // Task 1 measures from them): a pull's first phase plans and has the
+ // worker verify and hold the files, a query's plans and restores.
+ held := "restored to"
+ if pull {
+ held = "held through"
+ }
+ a.logf("dbreads: %s: planned %d files (%d bytes) and %s %d in %s", req.ID, plan.Files, plan.LTXBytes, held, plan.TXID, time.Since(attemptStart).Round(time.Millisecond))
+ runStart := time.Now()
if err := writeJSONFrame(conn, frameRun, runMsg{SQL: req.SQL}); err != nil {
return fail(CodeInterrupted, Summary{})
}
cw := newChunkWriter(ctx, a.cfg.Store, req.ID, pub, stream, a.cfg.ChunkEvery, max, size)
endCode, sqlErr, rerr := relay(conn, cw)
last, n, sum, werr := cw.close()
+ a.logf("dbreads: %s: output %d bytes in %d confirmed chunks in %s", req.ID, n, last, time.Since(runStart).Round(time.Millisecond))
s := Summary{TXID: plan.TXID, At: plan.At, Bytes: n, SHA256: sum, Error: sqlErr}
if pull {
s.Files = plan.Planned
@@ -889,7 +925,7 @@ Waits for Task 2 (DBF-17, DBF-18, DBF-24 approved).
**Interfaces:**
- Consumes: plan 2's `readDBForward`, `openDBForward`, `errDBForward`, `newStream`, `fwdID`; `newConsoleClient`, `apiSession`, `apiAcct`, `dbTarget` (main); `dbdoor.ForwardBody`; `ltx.HeaderSize`, `ltx.TrailerSize`, `litestream.SnapshotLevel`; `dbdoor.RestoreLocal`, `dbdoor.Restored`, `lsfile.NewReplicaClient`, `litestream.LTXFilePath`; `copyOut`, `dbPullHolds`, `dbRestoreError`, `humanBytes`.
-- Produces: `type dbChunkSink interface { add([]byte) error; count() int; chunk(i int) ([]byte, error) }`; `memSink`, `fileSink{dir string}`; `readDBForward(ctx, body io.Reader, key *ecdh.PrivateKey, host, region, reading string, status io.Writer, sink dbChunkSink, max int64) (string, *dbreads.Result, error)` (the id and the result; `openDBForward` is called by the query path); `openDBPull(ctx, id string, key *ecdh.PrivateKey, sink dbChunkSink, res dbreads.Result, host, region, dir string) (*dbdoor.Restored, error)`; `dbPullForward(ctx context.Context, t dbTarget, fwd dbdoor.Forward) (*dbdoor.Restored, error)`; `checkPullManifest(s dbreads.Summary) error`; `var dbPullSpoolWrite = os.WriteFile`; `dbfPullTooLarge`, `dbfFetching`, `dbfPullNoRoom`; test helpers `pullStream`, `pullStreamN`, `readPull`, `readPullIn`, `standInConsole`, `leftBehind`.
+- Produces: `type dbChunkSink interface { add([]byte) error; count() int; chunk(i int) ([]byte, error) }`; `memSink`, `fileSink{dir string}`; `readDBForward(ctx, body io.Reader, key *ecdh.PrivateKey, host, region, reading string, status io.Writer, sink dbChunkSink, bound *dbfChunkBound) (string, *dbreads.Result, error)` (the id and the result; `openDBForward` is called by the query path); `openDBPull(ctx, id string, key *ecdh.PrivateKey, sink dbChunkSink, res dbreads.Result, host, region, dir string) (*dbdoor.Restored, error)`; `dbPullForward(ctx context.Context, t dbTarget, fwd dbdoor.Forward) (*dbdoor.Restored, error)`; `checkPullManifest(s dbreads.Summary) error`; `var dbPullSpoolWrite = os.WriteFile`; `dbfPullTooLarge`, `dbfFetching`, `dbfPullNoRoom`; test helpers `pullStream`, `pullStreamN`, `readPull`, `readPullIn`, `standInConsole`, `leftBehind`.
- [ ] **Step 1: Write the failing tests**
@@ -944,7 +980,7 @@ func readPull(t *testing.T, st *stream, key *ecdh.PrivateKey) (*dbdoor.Restored,
func readPullIn(t *testing.T, st *stream, key *ecdh.PrivateKey, dir string) (*dbdoor.Restored, error) {
t.Helper()
sink := &fileSink{dir: filepath.Join(dir, "sealed")}
- id, res, err := readDBForward(context.Background(), &st.buf, key, "shop.example.com", "uk", dbfFetching, io.Discard, sink, dbreads.MaxPullBytes)
+ id, res, err := readDBForward(context.Background(), &st.buf, key, "shop.example.com", "uk", dbfFetching, io.Discard, sink, newDBFChunkBound(dbreads.MaxPullBytes, dbreads.PullChunkBytes))
if err != nil {
return nil, err
}
@@ -1016,6 +1052,41 @@ func TestForwardedPullRefusesFilesThatDoNotAddUp(t *testing.T) {
}
}
+// Plan review finding 37, at the spool: a chunk shorter than a sealed
+// empty one creates no spool file, and a stream of tiny chunks stops at the
+// bound's count, so the spool never holds more files than a read can make.
+// (TestForwardedQueryBoundsChunkCountAndSize pins the count's derivation; a
+// bound of 10 keeps this test small.) Mutation: add to the sink before
+// admit (red: a file for the short chunk, eleven files for the tiny ones).
+func TestForwardedPullSpoolsNoMalformedOrExcessChunk(t *testing.T) {
+ t.Parallel()
+ key, _ := dbreads.NewReplyKey()
+ for name, write := range map[string]func(s *stream) int{
+ "a chunk shorter than its seal": func(s *stream) int {
+ s.line(dbreads.Line{Chunk: 1, Data: []byte("short")})
+ return 0
+ },
+ "tiny chunks past the count": func(s *stream) int {
+ for seq := int64(1); seq <= 11; seq++ {
+ sealed, _ := dbreads.SealChunk(key.PublicKey(), fwdID, seq, dbreads.StreamLTX, false, []byte("x"))
+ s.line(dbreads.Line{Chunk: seq, Data: sealed})
+ }
+ return 10
+ },
+ } {
+ s := newStream(t, key)
+ want := write(s)
+ dir := filepath.Join(t.TempDir(), "sealed")
+ sink := &fileSink{dir: dir}
+ _, _, err := readDBForward(context.Background(), &s.buf, key, "shop.example.com", "uk", dbfFetching, io.Discard, sink,
+ &dbfChunkBound{maxBytes: dbreads.MaxPullBytes, maxChunks: 10})
+ files, _ := os.ReadDir(dir)
+ if err == nil || err.Error() != fmt.Sprintf(dbfUnchecked, "uk") || len(files) != want {
+ t.Errorf("%s: %v, %d spool files, want %d", name, err, len(files), want)
+ }
+ }
+}
+
// standInConsole answers the list door with the forward marker offering a
// pull, and the reads route with body (then holds the request open until
// the client goes when hold is set). It is a stand-in for the console, so
@@ -1106,10 +1177,11 @@ func TestForwardedPullCtrlCLeavesNothing(t *testing.T) {
dbdoortest.Replicate(t, dir, 3)
key, _ := dbreads.NewReplyKey()
st := pullStreamN(t, key, dir, 4, nil)
- // Admitted and the first chunk only, then the stand-in holds the request.
+ // Admitted, picked and the first chunk only (newStream writes admitted
+ // and picked first), then the stand-in holds the request.
lines := bytes.SplitAfter(st.buf.Bytes(), []byte("\n"))
out := filepath.Join(t.TempDir(), "copy.db")
- err := dbPull(ctx, standInConsole(t, bytes.Join(lines[:2], nil), true), out)
+ err := dbPull(ctx, standInConsole(t, bytes.Join(lines[:3], nil), true), out)
if err == nil || err.Error() != "stopped" {
t.Fatalf("%v", err)
}
@@ -1124,7 +1196,7 @@ func readFwd(t *testing.T, s *stream, key *ecdh.PrivateKey) (dbForwardAnswer, st
t.Helper()
var status bytes.Buffer
sink := &memSink{}
- id, res, err := readDBForward(context.Background(), &s.buf, key, "shop.example.com", "uk", dbfReading, &status, sink, dbreads.MaxOutputBytes)
+ id, res, err := readDBForward(context.Background(), &s.buf, key, "shop.example.com", "uk", dbfReading, &status, sink, newDBFChunkBound(dbreads.MaxOutputBytes, dbreads.ChunkBytes))
if err != nil {
return dbForwardAnswer{}, status.String(), err
}
@@ -1133,7 +1205,7 @@ func readFwd(t *testing.T, s *stream, key *ecdh.PrivateKey) (dbForwardAnswer, st
}
```
-and `TestForwardedQueryCtrlCPrintsNothing` calls `readDBForward` with `dbfReading, &status, &memSink{}, dbreads.MaxOutputBytes` and checks only its error. Add imports `"database/sql"`, `"errors"`, `"io"`, `"net/http"`, `"net/http/httptest"`, `"path/filepath"`, `lsfile`, `ltx`, `dbdoortest`. Check before writing `standInConsole` that `newStream` (plan 2) writes the `admitted` line first, so `lines[:2]` is admitted and chunk 1; and that `dbRestore` turns the marker into `errDBForward` from the list door's first GET, whatever its path (plan 2 Task 8).
+and `TestForwardedQueryCtrlCPrintsNothing` and `TestForwardedQueryBoundsChunkCountAndSize` call `readDBForward` with `dbfReading, &status, &memSink{}, newDBFChunkBound(dbreads.MaxOutputBytes, dbreads.ChunkBytes)` and check only its error. Add imports `"database/sql"`, `"errors"`, `"io"`, `"path/filepath"`, `lsfile`, `ltx`, `dbdoortest` (plan 2 already imports `net/http`, `net/http/httptest` and `sync/atomic` there). `newStream` (plan 2) writes `admitted` then `picked`, so `lines[:3]` in the Ctrl-C test is those and chunk 1; `dbRestore` turns the marker into `errDBForward` from the list door's first GET, whatever its path (plan 2 Task 8), which is why `standInConsole` answers the marker for every path but `/reads`.
- [ ] **Step 2: Run them to verify they fail**
@@ -1192,15 +1264,14 @@ func (f *fileSink) chunk(i int) ([]byte, error) {
}
```
-`readDBForward` takes the waiting sentence, the sink and the byte bound, and returns the id and the result instead of opening it:
+`readDBForward` takes the waiting sentence, the sink and plan 2's chunk bound (`dbfChunkBound`, plan review finding 37), and returns the id and the result instead of opening it:
```go
-func readDBForward(ctx context.Context, body io.Reader, key *ecdh.PrivateKey, host, region, reading string, status io.Writer, sink dbChunkSink, max int64) (string, *dbreads.Result, error) {
+func readDBForward(ctx context.Context, body io.Reader, key *ecdh.PrivateKey, host, region, reading string, status io.Writer, sink dbChunkSink, bound *dbfChunkBound) (string, *dbreads.Result, error) {
unchecked := fmt.Errorf(dbfUnchecked, region)
sc := bufio.NewScanner(body)
sc.Buffer(make([]byte, 64<<10), dbreads.MaxLineBytes)
var id, ended string
- var total int64
for sc.Scan() {
var l dbreads.Line
if err := json.Unmarshal(sc.Bytes(), &l); err != nil {
@@ -1214,8 +1285,9 @@ func readDBForward(ctx context.Context, body io.Reader, key *ecdh.PrivateKey, ho
id = l.Admitted
fmt.Fprintln(status, fmt.Sprintf(reading, host, region))
case l.Chunk != 0:
- total += int64(len(l.Data))
- if id == "" || l.Chunk != int64(sink.count())+1 || total > max+int64(sink.count()+1)*int64(dbreads.SealOverhead) {
+ // The bound is checked before the sink keeps anything, so a
+ // malformed or excess chunk never reaches memory or the spool.
+ if id == "" || l.Chunk != int64(sink.count())+1 || !bound.admit(l.Data) {
return "", nil, unchecked
}
if err := sink.add(l.Data); err != nil {
@@ -1240,7 +1312,7 @@ func readDBForward(ctx context.Context, body io.Reader, key *ecdh.PrivateKey, ho
}
```
-`openDBForward` reads its chunks from the sink: its signature becomes `openDBForward(id string, key *ecdh.PrivateKey, sink dbChunkSink, res dbreads.Result, host, region string)`, with `len(sealed)` replaced by `sink.count()` and the loop reading `sink.chunk(i)` (an error from it is `unchecked`). `dbQueryForward` calls `readDBForward(ctx, res.Body, key, t.host, fwd.Region, dbfReading, os.Stderr, &memSink{}, dbreads.MaxOutputBytes)` and then `openDBForward`.
+`openDBForward` reads its chunks from the sink: its signature becomes `openDBForward(id string, key *ecdh.PrivateKey, sink dbChunkSink, res dbreads.Result, host, region string)`, with `len(sealed)` replaced by `sink.count()` and the loop reading `sink.chunk(i)` (an error from it is `unchecked`). `dbQueryForward` calls `readDBForward(ctx, res.Body, key, t.host, fwd.Region, dbfReading, os.Stderr, &memSink{}, newDBFChunkBound(dbreads.MaxOutputBytes, dbreads.ChunkBytes))` and then `openDBForward`.
The pull's opener and verb:
@@ -1415,7 +1487,7 @@ func dbPullForward(ctx context.Context, t dbTarget, fwd dbdoor.Forward) (*dbdoor
}
}()
sink := &fileSink{dir: filepath.Join(dir, "sealed")}
- id, result, err := readDBForward(ctx, res.Body, key, t.host, fwd.Region, dbfFetching, os.Stderr, sink, dbreads.MaxPullBytes)
+ id, result, err := readDBForward(ctx, res.Body, key, t.host, fwd.Region, dbfFetching, os.Stderr, sink, newDBFChunkBound(dbreads.MaxPullBytes, dbreads.PullChunkBytes))
if err != nil {
return nil, err
}
@@ -1557,17 +1629,39 @@ func TestDBForwardPullParityThroughTheCLI(t *testing.T) {
}
}
-// T11 for a pull: a hash mismatch saves nothing at --out.
+// T11 for a pull: a hash mismatch saves nothing at --out. Everything else
+// is a pull the CLI would accept: far's real planned files, whole and
+// valid, a manifest that passes checkPullManifest, and a summary whose only
+// fault is its SHA-256 (plan review finding 32). Mutation: openDBPull skips
+// the hash comparison (red: the files restore and a copy is saved).
func TestDBForwardPullSavesNothingForABadAnswer(t *testing.T) {
e := newDBFE2E(t, dbfE2EOptions{forward: true, noBox: true})
stop := e.scriptBox(t, func(b *e2eScript) {
+ ctx := context.Background()
+ rd := lsfile.NewReplicaClient(filepath.Join(e.root, "instances", dbfE2EFar))
+ plan, err := dbdoor.PlanFrom(ctx, rd, time.Time{})
+ if err != nil {
+ t.Errorf("plan far's replica: %v", err)
+ return
+ }
+ var all bytes.Buffer
+ for _, f := range plan.Planned {
+ rc, err := rd.OpenLTXFile(ctx, f.Level, ltx.TXID(f.MinTXID), ltx.TXID(f.MaxTXID), 0, 0)
+ if err != nil {
+ t.Errorf("open %+v: %v", f, err)
+ return
+ }
+ io.Copy(&all, rc)
+ rc.Close()
+ }
b.picked(time.Now().Add(time.Minute))
- sealed, _ := dbreads.SealChunk(b.pub, b.id, 1, dbreads.StreamLTX, true, []byte("not the files"))
- b.store.Put(context.Background(), dbreads.ChunkKey(b.id, 1), sealed, "")
- summary, _ := dbreads.SealSummary(b.pub, b.id, dbreads.Summary{TXID: 1, Bytes: 13, SHA256: "00",
- Files: []dbdoor.LTXFile{{Level: 0, MinTXID: 1, MaxTXID: 1, Size: 13}}})
+ sealed, _ := dbreads.SealChunk(b.pub, b.id, 1, dbreads.StreamLTX, true, all.Bytes())
+ b.store.Put(ctx, dbreads.ChunkKey(b.id, 1), sealed, "")
+ wrong := sha256.Sum256([]byte("not these bytes"))
+ summary, _ := dbreads.SealSummary(b.pub, b.id, dbreads.Summary{TXID: plan.TXID, At: plan.At, Files: plan.Planned,
+ Bytes: int64(all.Len()), SHA256: hex.EncodeToString(wrong[:])})
r, _ := json.Marshal(dbreads.Result{State: dbreads.StateDone, LastSeq: 1, Box: "uk-1", Sealed: summary})
- b.store.Put(context.Background(), dbreads.ResultKey(b.id), r, "")
+ b.store.Put(ctx, dbreads.ResultKey(b.id), r, "")
})
defer stop()
out := filepath.Join(t.TempDir(), "far.db")
@@ -1706,7 +1800,7 @@ with `"crypto/sha256"` and `"encoding/hex"` added to that file's imports.
- [ ] **Step 2: Run them**
-Add `"os/exec"` to the file's imports if plan 2's harness does not already import it.
+Add `lsfile "github.com/benbjohnson/litestream/file"` and `"github.com/superfly/ltx"` to the file's imports (plan 2's harness already imports `os/exec`, `crypto/sha256`, `encoding/hex` and `dbdoor`). The far instance's replica is the litestream file layout under `e.root/instances/far.example.com`, which `betweenTheSyncs` already reads.
Run (sandbox off): `go test . -run 'TestDBForwardPull' -count=1 -v`
Expected: PASS. The MinIO subtest runs in CI's MinIO step.
@@ -1740,7 +1834,9 @@ In a tree copy under `$TMPDIR`, as plans 1 and 2 did; every row must turn red:
|---|---|
| `holdAndSend` skips `verifyLTX` | `TestWorkerHoldRefusesACorruptFile` |
| the agent's pull sends `run` before check B | `TestPullTooLargeAndNoChunkBeforeCheckB` (the recorded frames hold a `run`) |
-| `openDBPull` skips the hash comparison | `TestDBForwardPullSavesNothingForABadAnswer` |
+| `openDBPull` skips the hash comparison | `TestDBForwardPullSavesNothingForABadAnswer` (real files and a valid manifest; only the digest is wrong: plan review finding 32) |
+| `readDBForward` adds to the sink before `bound.admit` (plan review finding 37) | `TestForwardedPullSpoolsNoMalformedOrExcessChunk` |
+| the agent's pull drops its two timing lines (plan review finding 26) | `TestPullShipsThePlannedFiles` |
| `checkPullManifest` drops the sum | `TestForwardedPullRefusesFilesThatDoNotAddUp` ("sizes that do not sum": a file is created before the failure) |
| `checkPullManifest` drops the per-file size bounds | `TestForwardedPullRefusesFilesThatDoNotAddUp` ("sizes -1 and N+1": a panic, or a file created) |
| `checkPullManifest` drops the duplicate or the level check | `TestForwardedPullRefusesFilesThatDoNotAddUp` ("a file twice", "level 12") |
diff --git a/docs/superpowers/specs/2026-10-22-db-region-forward-design.md b/docs/superpowers/specs/2026-10-22-db-region-forward-design.md
index 0aaf635e8..23500f4fe 100644
--- a/docs/superpowers/specs/2026-10-22-db-region-forward-design.md
+++ b/docs/superpowers/specs/2026-10-22-db-region-forward-design.md
@@ -6,7 +6,10 @@ the plans had to decide, each marked **(plan review N)**: what a result
names when a chunk cannot be confirmed (§5.4, 9), a box's `cancel` read
before it elects (§5.5 step 4, §5.8, 11), a retry's reservation (§5.7, 10),
and the order of the audit row, the creates and the serial bump, with
-nothing but `cancel` once the request has ended (§5.5 step 3, §9.3, 11).
+nothing but `cancel` once the request has ended, checked before each write
+(§5.5 step 3, §9.3, 11 and 28). Round 2 of the plan review added DBF-32
+(§10) for SQL that JSON would rewrite, and the console's refusal of it
+(§5.5 step 3, 36).
Rev 5 answers Astra's round 4
(`CODEX-FINDINGS-DB-REGION-FORWARD-R4.md`), which found J1 to J5 fixed, with
four direct fixes and no new mechanism: flushed progress lines and the
@@ -327,13 +330,18 @@ The envelope is unchanged from rev 3:
byte-identical 404, the db budget, configuration, the record, the class
rail, pooled, fleet), refuses a moving record, checks `checkQuerySQL`
again, checks capability (§7.1), and takes its admission counts (J5).
- Any refusal so far is an ordinary status and JSON error. Then it mints
- the id, writes the audit row (§9.3), creates the request and the queue
- entry, bumps the serial, and starts a `200` response of
+ Any refusal so far is an ordinary status and JSON error, and so is SQL
+ holding U+FFFD, which is what JSON decoding makes of bytes that are not
+ UTF-8 (the CLI refuses such SQL first, with DBF-32) **(plan review 36)**.
+ Then it mints the id, writes the audit row (§9.3), creates the request
+ and the queue entry, bumps the serial, and starts a `200` response of
newline-delimited JSON. From here every outcome is a line in that body.
- If the request has ended by the time the queue entry is down, the
- console writes `cancel` and nothing else: no serial bump, no `200`
- **(plan review 11)**.
+ The console checks that the request is still open immediately before
+ each of those four writes, and the serial bump runs on the request's own
+ context. A request that ends before `request.json` leaves at most the
+ audit row; one that ends after it leaves `request.json`, perhaps the
+ queue entry, and `cancel`, and never a serial bump or a `200`
+ **(plan review 11, 28, 33)**.
4. Each capable box lists its region's queue for this hour and the last
when the serial moves, and every 30 s if it does not. For an in-window
entry it has not decided, it reads and verifies the request, skips it if
@@ -903,6 +911,7 @@ it is written into code.
| DBF-21 | audit detail, query | `forwarded a database query to region %s. the query was %d bytes, sha256 %s (cli)` |
| DBF-22 | audit detail, query with `--at` | `forwarded a database query to region %s, as of %s. the query was %d bytes, sha256 %s (cli)` |
| DBF-30 | `docs/site/cli.md`, `carlos db`, replacing the paragraph that starts "If the app has more than one instance" | see below |
+| DBF-32 | CLI, a query that is not UTF-8 text or holds U+FFFD, refused before it is sent: JSON would rewrite it, and the box must run the bytes typed or none **(plan review 36)** | `this query cannot be sent to region %s, because it holds bytes that are not UTF-8 text or the character U+FFFD. fix the query and run it again.` |
| DBF-31 | `docs/site/cli.md`, `carlos system`, the worker | `carlos system db-worker runs on platform boxes only. systemd starts it for each forwarded database query. you never run it yourself.` |
DBF-13 is true as written only because the CLI buffers the whole answer and
diff --git a/docs/superpowers/specs/CODEX-REVIEW-BRIEF-DB-REGION-FORWARD-PLANS.md b/docs/superpowers/specs/CODEX-REVIEW-BRIEF-DB-REGION-FORWARD-PLANS.md
index fe1006065..985c78caa 100644
--- a/docs/superpowers/specs/CODEX-REVIEW-BRIEF-DB-REGION-FORWARD-PLANS.md
+++ b/docs/superpowers/specs/CODEX-REVIEW-BRIEF-DB-REGION-FORWARD-PLANS.md
@@ -1,4 +1,4 @@
-# Review brief, round 2: the implementation plans for `carlos db` region forward
+# Review brief: the implementation plans for `carlos db` region forward (rounds 1 and 2 answered)
You are an independent second reader of three implementation plans for the
CARLOS platform (this repository). Round 1
@@ -69,6 +69,27 @@ then the plans.
**Fixed:** 23 in full, 1 in part (11). **Declined:** the part of 11 below.
+## Round 2 dispositions
+
+Round 2 (`CODEX-FINDINGS-DB-REGION-FORWARD-PLANS-R2.md`) found 14 more (3 P0, 1 P1, 10 P2). There is no plan round 3: the build starts from the tip that answers them, with per-task reviews and a whole-branch review after. Every new top-level identifier in the three plans was checked against `origin/main` for a same-package declaration; `dbStream` was the only collision.
+
+| # | Sev | Plan, task | Disposition |
+|---|---|---|---|
+| 25 | P0 | 2: 7 | Fixed. The relay type is `dbReadRelay`. |
+| 26 | P0 | 3: 5 | Fixed. The replacement keeps plan 1's two timing lines (`held through` for a pull); `TestPullShipsThePlannedFiles` asserts them. |
+| 27 | P0 | 1: 15 | Fixed. The `/etc` unit is swapped for a `/dev/null` symlink, `LoadState=masked` is asserted, and the symlink is removed before the file is restored. |
+| 28 | P1 | 2: 6 | Fixed as far as checks can: the request's context is checked immediately before each of the four writes, `cancel` once anything is published; one test case per step. A write already past its check still lands; the next check cancels, and boxes read `cancel` before electing and every 5 s while running. Spec §5.5 step 3. |
+| 29 | P2 | 1: 11, 12 | Fixed. A skipped probe stays owed (`probeRetry`); the failed connection closes before the probe is asked for. Tested through `Run` with a lingering unit. |
+| 30 | P2 | 2: 6, 7 | Fixed. Accelerated windows allow for `issued_at`'s truncation. |
+| 31 | P2 | 2: 7, 14 | Fixed. `TestDBReadResultWaitsForAChunkTheListingMissed`. |
+| 32 | P2 | 3: 9, 10 | Fixed. Real files, a valid manifest, a wrong digest. |
+| 33 | P2 | 2: 6, 14 | Fixed. "ended during the serial read", with a store that refuses a cancelled write. |
+| 34 | P2 | 2: 10, 11 | Fixed. The edges run in package `edge`'s test binary as a helper process; the real CLI and console in the root package; progress observed at the CLI's stderr and socket, through the local edge too. |
+| 35 | P2 | 2: 7, 14 | Fixed. `TestDBReadResultAfterTheDeadlineIsNotRelayed`; the "no witness" note is gone. |
+| 36 | P2 | 2: 6, 8 | Fixed. CLI refusal with a new row, DBF-32 (pending copy review), and a console 400 for U+FFFD; console, CLI and binary tests. Spec §5.5 step 3 and §10. |
+| 37 | P2 | 2: 8; 3: 7 | Fixed. `dbfChunkBound`: a count cap, a size floor, nothing after an empty chunk, checked before memory or the spool keeps a chunk. |
+| 38 | P2 | 2: 10, 14 | Fixed. `TestDBReadSetsBothReadDeadlines` watches the connection's read deadlines. |
+
## Declined plan findings
- **11, in part: a cancellation-aware audit insert.** `RecordActivityContext`