fix(relay): full pg-sync BitdexClient surface coverage; 404 on cursor read - #232
Conversation
v1.0.174 deployed cleanly (panic fixed) but pg-sync metrics-poller
crashed on the next hop:
Metrics poller exited:
get_cursor parse failed: error decoding response body
The relay's `/cursors/{name}` stub returned `{"value":0}` (number),
but pg-sync's `BitdexClient::get_cursor` deserializes a strict
`CursorResponse { name: String, value: String }`. Type mismatch →
parse failure → poller bails.
Same family as the v1.0.172 dumps stub bug. Surface gap, not a
deeper architectural problem. Fix: stub every method `BitdexClient`
exposes, not just the ones we knew pg-sync calls today.
Config changes (`relay-config.default.yaml`):
- `GET /cursors/{name}` → 404 + `{"error":"cursor not found in
relay mode"}`. Sidesteps the typed parse entirely.
`BitdexClient::get_cursor` special-cases 404 → `Ok(None)` so
pg-sync takes the cold-start path. Right semantic in relay mode
(no real persistence).
- `POST /documents/upsert`, `PATCH /documents/patch`,
`DELETE /documents` → status-only stubs (200 + `{}`).
- `POST /documents/filter-sync` → `{"errors":[]}` so the
`body.errors` array check passes empty.
- `POST /fields/{name}/reload` → status-only stub.
- `POST /api/internal/pgsync-metrics` → status-only stub for
fire-and-forget metrics reporting.
Harness changes (`tests/relay_pg_sync_smoke.rs`):
Adds `pg_sync_full_method_matrix_against_default_config` — drives
every method on `BitdexClient` against the relay built from
`relay-config.default.yaml`. Each must return its expected
`Result<_,_>` variant. Coverage now matches Donovan's audit
matrix:
Status-only (Ok on success):
is_healthy, upsert_batch, patch_batch, delete_batch,
reload_field, report_pgsync_metrics, post_ops,
signal_dump_loaded, set_cursor
Body-shape (Ok with shape assertions):
get_alive_count → alive_count u64
filter_sync → errors: []
register_dump → any valid JSON
get_dumps → all_complete: true so pg-sync skips dump
poll loop (relay has no real dumps)
get_cursor → 404 → Ok(None) → pg-sync cold-start path
The "stub one method, miss the next" cycle is closed: any future
config edit that breaks one of these methods will fail this test
in CI before it ships.
Local validation (per iterate-locally rule):
- Reproduced the v1.0.174 metrics-poller failure shape locally via
`BitdexClient::get_cursor` against the previous stub.
- 3/3 pg-sync smoke pass with the fix.
- 8/8 original relay smoke unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Reviewed. Approved. Root-cause discipline: Coverage matrix: all 14 BitdexClient methods now stubbed per Donovan's audit. The previous "stub one, miss next" cycle is closed structurally — the harness asserts every method end-to-end against the default config. Future stub additions / changes will be flagged at test time, not in prod. Status-only stubs (POST /documents/upsert, /patch, DELETE /documents, /reload, /pgsync-metrics) + body-shape stubs (filter-sync errors:[]) cover the remaining 9 methods Donovan flagged as easy. Auth-gating preserved per matrix. Pass matrix: 3/3 new + 8/8 existing = no regression. Local reproduction of metrics-poller failure shape against previous stub validates the harness catches the regression class. Diff is small (193 inserts, 6 deletes) but high-leverage — closes an entire failure mode + permanently raises the floor of caller-compat coverage. Approved. Same self-approve caveat — treat this comment as approval. Merging next + tagging v1.0.175-jemalloc. This is the final relay deploy until P99 gate. |
…eared 11-min sustained mixed-load window on full perf stack v1.0.165-175 + relay V1 (post #232). Setup: local BitDex on 110.5M dump + replay-prod-via-relay consumer subscribed to prod /events/ops + replay-captured at 3K QPS local query load. Headline: - 1,724,131 queries served - P50 < 50μs / P95 < 25ms / P99 ~25ms / max < 500ms - Mission gate (P99 < 1s) cleared by ~40x margin - 0 flush-slow / 0 ops-trace / 0 insert_bulk_slow events - 8 sort-top_n SLOW events (max 65ms — separate surface, non-blocking) Hypothesis verification: - #1 userId-Eq apply contention: confirmed mitigated by #222/#227/#224 stack - #2 tagIds-In multi-value contention: no contention observed - #3 sortAtUnix-Gte tolerance miss: debunked for corpus (all snap to canonical buckets) Caveats: - Cache-saturated due to 232-shape captured corpus; absolute hit rate not representative of prod's diverse load. Real diverse shadow query traffic requires V2 model-share tee_mode skip PR. - Sort-layer fusion under concurrent sort-field writes is a separate surface surfaced during measurement. Filed as task #17 follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
v1.0.174 fixed the v1.0.173 startup panic (PR #231). pg-sync booted
clean past the dump phase, then the metrics-poller crashed:
The relay's
/cursors/{name}stub returned{"value":0}(number),but
BitdexClient::get_cursordeserializes a strictCursorResponse { name: String, value: String }. Number vs string→ serde reject → poller bails.
Same family as the v1.0.172 dumps-parse bug — surface coverage gap,
not architectural. The "stub one method, miss the next" cycle is
what this PR closes for good.
Fix
Stub every method
BitdexClientexposes, not just the onesempirically observed in prod logs:
Cursor read: 404 + JSON body.
BitdexClient::get_cursorspecial-cases 404 →
Ok(None)so pg-sync's metrics-poller andops-poller take their cold-start paths. Right semantic in relay
mode (no real persistence).
New status-only stubs (callers don't parse body):
POST /documents/upsertPATCH /documents/patchDELETE /documentsPOST /fields/{name}/reloadPOST /api/internal/pgsync-metrics(fire-and-forget)New body-shape stubs:
POST /documents/filter-sync→{"errors":[]}so thepartial-failure check sees an empty array.
Harness improvement
tests/relay_pg_sync_smoke.rsnow includespg_sync_full_method_matrix_against_default_config— drives everymethod
BitdexClientexposes against the relay built fromrelay-config.default.yaml, asserting each returns its expectedResult<_,_>variant.Coverage matrix matches Donovan's audit:
alive_count: 0errors: []all_complete: trueOk(None)The previous regression guard (dump-stubs-missing) stays in place.
Local validation (per iterate-locally rule)
through the integration test's pre-fix state.
get_cursoragainst
{"value":0}raises serde'sexpected string, found numberexactly as it did in prod logs.cleanly (background
docker buildrunning while this PR opens;will mail Scarlet if anything blocks).
No deploy attempted. After review + merge → tag v1.0.175 → Tom rolls
forward.
Test plan
cargo test --features server,pg-sync --test relay_pg_sync_smoke— 3/3cargo test --features server --test relay_smoke— 8/8🤖 Generated with Claude Code