fix(ctrl): read ledger-stitched queries from a single snapshot#1445
Conversation
Every query that returns data stitched from a LedgerInfo read plus per-entity attribute reads now opens one Pebble snapshot (NewReadHandle) up front and routes the existence/name read and the payload reads through it — the same pattern as the GetLedger/ListLedgers fix (#583). Converted: GetTransactionFrom, GetAccount, GetLedgerStats, AnalyzeAccounts, AggregateVolumes, InspectIndex, ListLogs (fixes an inverted handle), ListPreparedQueries (also NewDirectReadHandle->NewReadHandle), GetNumscript, ListNumscripts, and computeTransactionReceipt's live path. Defense-in-depth: unlike GetLedger (whose response packs two coupled mutable values) none of these combine two snapshots into an observable inconsistency today — the second read only supplied the immutable ledger name — so no new e2e test is added (none could be made red); the existing get_ledger_snapshot_test.go remains the coverage. GetMetadataSchemaStatus (single read), GetIndexStatus and ListIndexes (intentional cross-store / streaming reads) are left as-is. Refs EN-1408
The previous commit opened the receipt's snapshot before impl.ctrl.GetTransaction, whose routed/linearizable read (ReadIndex barrier, possible leader forwarding) can land at a later committed state. A transaction whose creation log commits in that gap would be returned by GetTransaction but missed by the earlier receipt snapshot, yielding an empty receipt for an existing transaction. Open the snapshot after the read instead (and only when receipt signing is enabled). Caught by NumaryBot review on #589.
The previous fix opened the receipt snapshot after the transaction read, but that only helps locally-served reads. When GetTransaction is routed to the leader (Consistency: leader, or a linearizable read falling back to the leader), the transaction is read from the leader's fresh state while the receipt is still re-derived from a local snapshot that may lag — yielding an empty receipt for an existing transaction. The leader already signs the receipt and returns it; we were discarding it. Controller.GetTransaction now also returns that upstream receipt (empty for a locally-served read, where the gRPC layer signs from the post-read snapshot). The adapter reuses a non-empty forwarded receipt and only re-derives locally otherwise. Regenerated the Controller/Backend mocks for the new signature. Caught by NumaryBot/codex review on #589 (finding fa4181052084ad35).
fwdReceipt != "" conflated two cases: a forwarded read of a receipt-less transaction (notably a reversal, whose creation log is a RevertedTransaction so the leader returns an empty receipt) fell into local re-derivation on a possibly-stale follower — wasteful in the common case, and an error if that follower's snapshot lacks the ledger. Controller.GetTransaction now returns *string: nil for a locally-served read (the gRPC layer signs from the post-read snapshot), non-nil and authoritative (possibly empty, e.g. a reversal) when forwarded to a signing node — used as-is, never re-derived. Internal plumbing only; the wire receipt field stays a string. Regenerated the Controller/Backend mocks. Addresses NumaryBot finding 865ba8aa on #589.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
✅ Approve — automated reviewThe PR consistently routes ledger-stitched queries through a single snapshot/read handle, eliminating snapshot inconsistency across the affected read paths. The one previously confirmed major issue — forwarded receipts being silently dropped when the local receipt signer is disabled — was identified in earlier automated review, resolved in commit 5c9f5c5, and verified with an end-to-end test before the current review cycle. All independent reviewers found no regressions or new issues in the updated code. There are no remaining blocker or major findings; the PR is ready to merge. No findings. |
NumaryBot
left a comment
There was a problem hiding this comment.
NumaryBot posted 1 new inline finding.
Summary: #1445 (comment)
The receiptSigner != nil guard wrapped the forwarded-receipt reuse, so a node with signing disabled that routed GetTransaction to a signing leader dropped the leader's authoritative receipt — making receipt presence depend on which node the client contacted under heterogeneous config. Relaying an already-signed token needs no local signer; gate only local derivation on it. Addresses NumaryBot finding 59afc7b354ba2988 on #1445.
NumaryBot
left a comment
There was a problem hiding this comment.
NumaryBot posted 1 new inline finding.
Summary: #1445 (comment)
Codecov Report❌ Patch coverage is ❌ Your patch status has failed because the patch coverage (44.28%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## release/v3.0 #1445 +/- ##
================================================
- Coverage 73.66% 71.82% -1.84%
================================================
Files 388 388
Lines 39747 39917 +170
================================================
- Hits 29278 28671 -607
- Misses 7951 8000 +49
- Partials 2518 3246 +728
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
gfyrag
left a comment
There was a problem hiding this comment.
LGTM. Les fixes de receipt sont réels (Bug A: receipt forwardé jeté; Bug D: gate signer trop englobante) et le tri-state *string encode proprement l'autorité vs l'absence. La sweep single-snapshot est du hardening défendable — pas de bug observable aujourd'hui mais by-construction safe pour toute future lecture de champ mutable de LedgerInfo.
Deux remarques non bloquantes:
-
Coverage gap sur les receipts. Aucun test rouge ne couvre les bugs corrigés côté serveur:
- Follower en retard forward au leader et surface le receipt (Bug A).
- Node sans signer relaye un receipt de leader signeur (Bug D).
- Reversal forwardé garde
receipt=""non-nil (Bug C, tri-state).
Seul
client_bucket_test.govalide le round-trip côté client.codecov/patcha échoué. Ces bugs ont été trouvés par NumaryBot, pas par test — un futur rework du routing pourrait les réintroduire silencieusement. Un test d'intégration deux-nodes (follower lag +Consistency: leader) rendrait la zone stable. -
Sweep sans garde-fou. 10 handlers modifiés pour un pattern qu'il faudrait maintenant tenir. Serait utile de documenter dans
docs/technical/architecture/la règle « toute lecture qui stitchLedgerInfoavec un attribut ouvre UN handle », voire un lint qui interditGetLedgerByName(ctx, ctrl.store, …)en dehors d'exceptions listées (GetMetadataSchemaStatus,GetIndexStatus,ListIndexes).
Configures only the leader with a receipt-signing key; a Consistency: leader GetTransaction issued against a follower with no signer must relay the leader-signed receipt rather than drop it. Red against the pre-fix signer-gated relay, green after. Adds a WithNodeInstruments helper for per-node cluster configuration.
Records the guardrail behind the read-path sweep: any controller read that stitches a LedgerInfo lookup with attribute reads opens ONE ReadHandle and routes everything through it, including GetLedgerByName. Covers the canonical pattern, the torn-read rationale, the transaction-receipt rules, and the deliberate exceptions (GetMetadataSchemaStatus, GetIndexStatus, ListIndexes).
|
Bug D now has a red-verified e2e test ( The other two can't get a meaningful red test: Bug A needs deterministic follower lag, but the test gateway isn't in the Raft path (nodes talk on real ports), and Bug C isn't observable — a forwarded reversal returns |
NumaryBot
left a comment
There was a problem hiding this comment.
NumaryBot review complete: no remaining inline findings.
Resolved 1 stale NumaryBot review thread (1 fixed, 0 outdated).
Summary: #1445 (comment)
…ingle-snapshot-reads # Conflicts: # docs/technical/architecture/README.md # docs/technical/architecture/subsystems/consensus/read-snapshot-consistency.md
Migrated from formancehq/ledger-v3-poc#589 (rebased onto
release/v3.0).Single-snapshot reads (EN-1408 follow-up)
Sweeps the remaining read paths that stitch a
LedgerInforead together with per-entity attribute reads so they all read from one Pebble snapshot — the canonical pattern from the GetLedger/ListLedgers fix.Converted (one
NewReadHandle()up front, all reads routed through it):GetTransactionFrom,GetAccount,GetLedgerStats,AnalyzeAccounts,AggregateVolumes,InspectIndex,ListLogs(also fixes an inverted handle),ListPreparedQueries(alsoNewDirectReadHandle→NewReadHandle),GetNumscript,ListNumscripts.This is defense-in-depth: none of these combined two snapshots into an observable inconsistency today (the second read only supplied the immutable ledger name), so there's no new red e2e test — the existing
get_ledger_snapshot_test.goremains the coverage. Pinning makes a future change that embedsLedgerInfocontent safe by construction. Left intentionally unchanged:GetMetadataSchemaStatus(single read),GetIndexStatus/ListIndexes(intentional cross-store / streaming reads).Transaction receipt correctness on routed reads
The receipt for
GetTransactionis computed from the transaction's creation log. Three fixes (driven by review):ctrl.GetTransaction's routed/linearizable read could pin a state predating the transaction's creation log → empty receipt for an existing tx.BucketGrpcClientwas discardingresp.Receipt; now it's surfaced.Controller.GetTransactionreturns*string:nilfor a locally-served read (the gRPC layer signs from the post-read snapshot), non-nil and authoritative (possibly empty, e.g. a reversal) when forwarded — used as-is, never re-derived. Internal plumbing only; the wirereceiptfield stays astring.Mocks regenerated for the interface change.