What
How — raw bytes through the view path (changed from the earlier plan)
- The earlier plan was "
StreamLedgerRange with v1's IngestFees as the store.StreamLedgerFn callback". It is dead. The ELI5 of why — do not resurrect it by routing the replay through store.LedgerReader, even though that looks like the blessed seam:
- New mechanism — the same code path as live ingestion's fee product:
# pseudocode; real types are query.ReadView / xdr.LedgerCloseMetaView
view = registry.NewReadView() # frozen range + handle set; defer view.Release()
latest = lastCommittedLedger # the hot loop resumes at latest + 1
start = max(latest - window + 1, view.OldestLedger())
if start > latest:
return # fresh start / short history: no-op, not an error
for entry in view.ScanLedgers(start, latest):
lcm = LedgerCloseMetaView(entry.Bytes)
parts = ingest.ExtractLedgerTxParts(lcm) # the one walk
fees = ingest.FeesFromTxParts(parts) # fees only; events never computed
# seq + close time read off the view header; append to BOTH windows,
# each trimming to its own retention
windows.AppendLedgerFees(seq, closeTime, fees)
entry.Bytes is borrowed (it aliases the reader's scratch buffer, overwritten on the next step) — fine here: fees are folded within the loop body and only plain uint64 slices are retained.
- The replay range spanning a chunk boundary is the common case, not an edge case (any restart within the first
window ledgers of a chunk). ScanLedgers does the split and per-chunk clip; ≤1000 ledgers means at most 2 chunks, so routing failures surface before any ledger is emitted.
Ordering — the double-count hazard
Scope notes (carried over, still true)
- Clamp to what exists: on a fresh start
OldestLedger() can exceed latest by one, so a short history must be a no-op, not an error.
- No config-time window-vs-retention guard needed (v1 has one): windows are capped at 1,000 ledgers (
limits.MaxFeeStatsRetentionWindow) and v2's smallest retention is one chunk = 10,000 ledgers, so the guard can never fire.
Dependency change
Sizing
- ~80–120 lines in 1 file, ~6 tests (unchanged estimate).
What
max(classic window, soroban window)ledgers (≤1000) to refill them before live ingestion resumes.ExtractLedgerTxParts→FeesFromTxParts, nothing else computed. Exactly the third composition the SDK reshape was built for.How — raw bytes through the view path (changed from the earlier plan)
StreamLedgerRangewith v1'sIngestFeesas thestore.StreamLedgerFncallback". It is dead. The ELI5 of why — do not resurrect it by routing the replay throughstore.LedgerReader, even though that looks like the blessed seam:xdr.LedgerCloseMetabecause handlers work on parsed data (store.StreamLedgerFnisfunc(xdr.LedgerCloseMeta) error). The replay is not a handler — it's ingestion refilling its own in-memory cache. Wrong seam, wrong audience.feewindow.IngestFeeslives underinternal/rpcv1/, and v2 code may not importrpcv1/(the depguard rules from Reorganize stellar-rpc into shared/rpcv1/rpcv2 trees and build both binaries #876). Reusing v1's ingestion here was never actually possible after the reorg.ReadViewdirectly means the replay can be built as soon as v2 ingestion: one walk per ledger — migrate backfill + streaming to parts/products, and feed the getFeeStats windows #881 lands, without waiting for theLedgerReaderadapter.entry.Bytesis borrowed (it aliases the reader's scratch buffer, overwritten on the next step) — fine here: fees are folded within the loop body and only plainuint64slices are retained.windowledgers of a chunk).ScanLedgersdoes the split and per-chunk clip; ≤1000 ledgers means at most 2 chunks, so routing failures surface before any ledger is emitted.Ordering — the double-count hazard
[start, lastCommitted]; the hot loop starts feeding the same windows atlastCommitted + 1.HotService(see v2 ingestion: one walk per ledger — migrate backfill + streaming to parts/products, and feed the getFeeStats windows #881 work item 3).Scope notes (carried over, still true)
OldestLedger()can exceedlatestby one, so a short history must be a no-op, not an error.limits.MaxFeeStatsRetentionWindow) and v2's smallest retention is one chunk = 10,000 ledgers, so the guard can never fire.Dependency change
ReadViewdirectly over raw bytes — it never touchesstore.LedgerReader. (The store seam exists for handlers; the replay is ingestion.)FeesFromTxPartswired), Implement concurrent query serving: ingestion changes and the query router #865/Query routing: serving foundation (write + read side) #894 (query router — done).Sizing