events,eventstore: concurrent index; migrate off membitmaps - #756
Conversation
…tmaps Rework the in-memory events index and the full-history eventstore as one unit — they are tightly coupled, since the eventstore consumes the events index types and the membitmaps removal forces the eventstore migration in lockstep. The query engine (Query + postfilter) is deliberately left out and follows in a separate stacked PR. events: - Add ConcurrentBitmaps and ConcurrentLedgerOffsets for lock-free concurrent reads during ingest; remove the old membitmaps implementation. - Add the ingest_view path: build the index directly from xdr LedgerCloseMetaView / TransactionMetaView zero-copy views. - payload / index / ledgeroffsets / bitmaps reworked accordingly. eventstore: - Migrate the cold store (format / index / reader / writer) and the hot store onto the new events index API. - reader.go interface updates. deps: roaring v2.18.0 -> v2.18.2 (upstream FastOr/runContainer16 fix), go-stellar-sdk bump (XDR View types used by ingest_view), and tamirms/streamhash promoted to a direct dependency. The eventstore.Query concurrency test (TestHotStore_QueryUnderConcurrentIngest) moves to the query-engine PR alongside query.go. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub. |
1358391 to
2739b51
Compare
8628fab to
2510810
Compare
The view-based ingest extractor (ingest_view.go / LCMToPayloadsFromRaw) and its Payload term-precompute plumbing move to the separate #764 work, so remove them here along with the now-dead TermKeys() skip-branch in IngestLedgerEvents and the V3-SorobanMeta test fixture only the extractor's test used. Payload now carries the event purely as raw XDR (ContractEventBytes) — the decoded xdr.ContractEvent field is gone. Ingest (LCMToPayloads) marshals each event into the bytes, and terms are derived straight from the raw XDR via xdr.ContractEventView (events.TermsForBytes), no full UnmarshalBinary. Remove the per-Reader useXDRViews toggle from HotStore and ColdReader; the read path always decodes via views. Payload.Unmarshal is the sole consumer decoder (struct decoder removed; former UnmarshalView renamed to Unmarshal). FetchEvents returns owned Payloads; FetchRange/All yield borrowed Payloads (ContractEventBytes aliases the iterator's step buffer — clone to retain). IngestLedgerEvents marshals each payload into one reused scratch buffer (BatchWriter.Put copies the value synchronously), and is idempotent on retry: re-ingesting an already-committed ledger is a no-op (a gap or out-of-range ledger still errors). Warmup now cross-checks the per-chunk CFs on open (verifyChunkConsistency): the index may not reference an event beyond the committed count, and the data tail must align with it (event total-1 present, nothing at id >= total) — a corrupt or tampered chunk fails to open loudly instead of serving an inconsistent cache. ConcurrentLedgerOffsets.Append is now a single positional primitive (no ledger arg, no error); the sequence, capacity, and cumulative-overflow checks live at the warmup trust boundary in warmupOffsets, where on-disk rows are untrusted. Deps: - go-stellar-sdk -> latest main (v0.5.1-0.20260604220920-ff1e140adca5) - streamhash -> github.com/stellar/streamhash (was tamirms/streamhash) - roaring/v2 unchanged at v2.18.2 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2510810 to
93f13b0
Compare
urvisavla
left a comment
There was a problem hiding this comment.
Reviewed and approving this PR. Going to let this merge so @chowbao's PR PR isn't blocked.
A few things I'd like to address in a follow up PR / issues:
-
Main concern: full snapshot clone for freeze. Snapshot has to clone because RunOptimize mutates the bitmaps and we want to allow concurrent reads to during freeze but cloning the whole
~500MB index will double the memory briefly. Streaming the snapshot (clone one bitmap at a time, RunOptimize, marshal) would be the better approach. -
Unify ConcurrentLedgerOffsets and LedgerOffsets. Don't see a particular benefit in having both. They can be one type as the current concurrent version with the backing array trick.
-
Drop sparse-list mode. Saves heap at rest but every sparse read will materialize a fresh bitmap on every read. Under sustained read load that's continuous gc pressure.
Treating sparse the same as dense (always roaring with COW) costs more memory but eliminates the per read allocs and gives better steady state performance.
feature/full-history received the official squashed #756 (concurrent index; migrate off membitmaps), while this PR's base (fh-ingest-base) already carried an equivalent variant of the same work. Both branches resolve to byte-identical trees (c388061), so the apparent conflicts are purely topological. Recording the merge with -s ours keeps the PR tree unchanged while making feature/full-history an ancestor so the PR is mergeable.
Stacked on slice 1 (the ledgers skeleton); this commit's diff is only the
events additions on top of it. Adds the EVENTS data type to the streaming
daemon:
- events column families in the per-chunk hot RocksDB (hotchunk), so one
atomic synced WriteBatch per ledger now carries ledgers + events;
- the events cold-segment writer in processChunk;
- the chunk:{c}:events catalog key + its sweeps;
- events coverage in the audit (INV-3 disk<->catalog) and in the
crash-injection/convergence suite and lifecycle E2E.
Events is a per-chunk artifact, like ledgers — no window/index subsystem
(that is tx-hash, deferred to slice 3).
Composes the events store (pkg/stores/eventstore, #740/#756) and the events
design (getevents-full-history-design.md, #635), already on
feature/full-history.
Built against RocksDB 10.9.1 (grocksdb 1.10.7); fullhistory tree green on the
non-short suite incl. the lifecycle E2E.
Stacked on slice 1 (the ledgers skeleton); this commit's diff is only the
events additions on top of it. Adds the EVENTS data type to the streaming
daemon:
- events column families in the per-chunk hot RocksDB (hotchunk), so one
atomic synced WriteBatch per ledger now carries ledgers + events;
- the events cold-segment writer in processChunk;
- the chunk:{c}:events catalog key + its sweeps;
- events coverage in the audit (INV-3 disk<->catalog) and in the
crash-injection/convergence suite and lifecycle E2E.
Events is a per-chunk artifact, like ledgers — no window/index subsystem
(that is tx-hash, deferred to slice 3).
Composes the events store (pkg/stores/eventstore, #740/#756) and the events
design (getevents-full-history-design.md, #635), already on
feature/full-history.
Built against RocksDB 10.9.1 (grocksdb 1.10.7); fullhistory tree green on the
non-short suite incl. the lifecycle E2E.
Stacked on slice 1 (the ledgers skeleton); this commit's diff is only the
events additions on top of it. Adds the EVENTS data type to the streaming
daemon:
- events column families in the per-chunk hot RocksDB (hotchunk), so one
atomic synced WriteBatch per ledger now carries ledgers + events;
- the events cold-segment writer in processChunk;
- the chunk:{c}:events catalog key + its sweeps;
- events coverage in the audit (INV-3 disk<->catalog) and in the
crash-injection/convergence suite and lifecycle E2E.
Events is a per-chunk artifact, like ledgers — no window/index subsystem
(that is tx-hash, deferred to slice 3).
Composes the events store (pkg/stores/eventstore, #740/#756) and the events
design (getevents-full-history-design.md, #635), already on
feature/full-history.
Built against RocksDB 10.9.1 (grocksdb 1.10.7); fullhistory tree green on the
non-short suite incl. the lifecycle E2E.
Stacked on slice 1 (the ledgers skeleton); this commit's diff is only the
events additions on top of it. Adds the EVENTS data type to the streaming
daemon:
- events column families in the per-chunk hot RocksDB (hotchunk), so one
atomic synced WriteBatch per ledger now carries ledgers + events;
- the events cold-segment writer in processChunk;
- the chunk:{c}:events catalog key + its sweeps;
- events coverage in the audit (INV-3 disk<->catalog) and in the
crash-injection/convergence suite and lifecycle E2E.
Events is a per-chunk artifact, like ledgers — no window/index subsystem
(that is tx-hash, deferred to slice 3).
Composes the events store (pkg/stores/eventstore, #740/#756) and the events
design (getevents-full-history-design.md, #635), already on
feature/full-history.
Built against RocksDB 10.9.1 (grocksdb 1.10.7); fullhistory tree green on the
non-short suite incl. the lifecycle E2E.
Stacked on slice 1 (the ledgers skeleton); this commit's diff is only the
events additions on top of it. Adds the EVENTS data type to the streaming
daemon:
- events column families in the per-chunk hot RocksDB (hotchunk), so one
atomic synced WriteBatch per ledger now carries ledgers + events;
- the events cold-segment writer in processChunk;
- the chunk:{c}:events catalog key + its sweeps;
- events coverage in the audit (INV-3 disk<->catalog) and in the
crash-injection/convergence suite and lifecycle E2E.
Events is a per-chunk artifact, like ledgers — no window/index subsystem
(that is tx-hash, deferred to slice 3).
Composes the events store (pkg/stores/eventstore, #740/#756) and the events
design (getevents-full-history-design.md, #635), already on
feature/full-history.
Built against RocksDB 10.9.1 (grocksdb 1.10.7); fullhistory tree green on the
non-short suite incl. the lifecycle E2E.
Stacked on slice 1 (the ledgers skeleton); this commit's diff is only the
events additions on top of it. Adds the EVENTS data type to the streaming
daemon:
- events column families in the per-chunk hot RocksDB (hotchunk), so one
atomic synced WriteBatch per ledger now carries ledgers + events;
- the events cold-segment writer in processChunk;
- the chunk:{c}:events catalog key + its sweeps;
- events coverage in the audit (INV-3 disk<->catalog) and in the
crash-injection/convergence suite and lifecycle E2E.
Events is a per-chunk artifact, like ledgers — no window/index subsystem
(that is tx-hash, deferred to slice 3).
Composes the events store (pkg/stores/eventstore, #740/#756) and the events
design (getevents-full-history-design.md, #635), already on
feature/full-history.
Built against RocksDB 10.9.1 (grocksdb 1.10.7); fullhistory tree green on the
non-short suite incl. the lifecycle E2E.
Summary
Part of the series extracting reviewable components from the
rpc-hackbranch intofeature/full-history.This reworks the in-memory events index (
internal/events) and the full-historyeventstoreas one unit — they're tightly coupled: the eventstore consumes the events-index types, and removing the oldmembitmapsimplementation forces the eventstore migration in lockstep. The query engine (Query+ postfilter) and the view-based ingest extractor are deliberately left out (see below) and follow in separate PRs.internal/eventsConcurrentBitmapsandConcurrentLedgerOffsetsfor lock-free concurrent reads during ingest; remove the oldmembitmapsimplementation.ConcurrentLedgerOffsets.Appendis a single positional primitive (no ledger argument, no error) — the structure is purely positional, so ledger N lives at slot N−startLedger. Sequence / capacity / cumulative-overflow validation lives at each caller's trust boundary: ingest validates up front, andwarmupOffsetsvalidates the untrusted on-disk rows. (The cold siblingLedgerOffsets.Appendkeeps its validating two-arg form because its caller decodes untrusted bytes.)Payloadcarries the contract event purely as raw XDR inContractEventBytes— the decodedxdr.ContractEventstruct field is gone.Marshalwrites the bytes;Unmarshalaliases them zero-copy (no XDR decode).LCMToPayloads) marshals each event intoContractEventBytes; term keys are derived straight from the raw XDR viaxdr.ContractEventView(events.TermsForBytes), without a fullUnmarshalBinary.index/ledgeroffsets/bitmapsreworked accordingly.The view-based ingest extractor (
ingest_view.go/LCMToPayloadsFromRaw, walkingxdr.LedgerCloseMetaViewto emit payloads zero-copy) is not in this PR — it is tracked in #764. ThePayloadterm-precompute plumbing that only that extractor populated is removed here too.eventstorecold_format/cold_index/cold_reader/cold_writer) and the hot store onto the new events-index API.reader.gointerface updates.useXDRViewstoggle (HotStore.WithXDRViews/ColdReaderOptions.UseXDRViews) is removed.FetchEventsreturns owned Payloads (safe to retain).FetchRange/Allyield borrowed Payloads —ContractEventBytesaliases the reader's per-step iteration buffer and is valid only until the next step, so a consumer that retains one must clone. Seeevents.Payload.Unmarshalfor the alias contract.IngestLedgerEventsmarshals each payload into one reused scratch buffer inside the RocksDB batch (BatchWriter.Putcopies the value synchronously), avoiding a per-event allocation. It is idempotent on retry — re-ingesting an already-committed ledger is a no-op, while a gap or out-of-range ledger errors.warmupcross-checks the three per-chunk CFs (verifyChunkConsistency): the index may not reference an event beyond the committed count, and the data tail must align with it (eventtotal-1present, nothing at id ≥total). A corrupt or tampered chunk fails to open loudly instead of silently serving an inconsistent in-memory cache. This is a cheap open-time tripwire on denormalized state, not load-bearing correctness — the atomic write batch guarantees these invariants for the writer, and an interior data hole (which the tripwire does not detect) is caught lazily byFetchRange's short-scan check on first read.Dependencies
RoaringBitmap/roaring/v2v2.18.0 → v2.18.2 (upstream FastOr/runContainer16 fix, replacing the previous fork).go-stellar-sdkbumped to match the latestmain(v0.5.1-0.20260604220920-ff1e140adca5).streamhashis a direct dependency ongithub.com/stellar/streamhash(moved offgithub.com/tamirms/streamhash).go mod tidypruned the upload-cold/bench-only deps, so go.mod carries only what this PR uses.Why the simple/concurrent bitmap split (rationale)
The original
memBitmapswas a singleRWMutex-protected type used for both the live query path and the single-threaded cold build. It was split into a lock-freeConcurrentBitmaps(hot) and a plain, unsynchronizedBitmapsmap (cold build). Motivation, from the implementing session and the (squashed) PR #751 commits — see060993502"split bitmaps + offsets into simple vs concurrent" and00aa03944"ConcurrentBitmaps termState + COW":memBitmaps.Getcloned the bitmap on every lookup so concurrent ingest couldn't mutate it mid-read.Queryis read-only, so those clones are pure overhead — projected at ~15–30% of a core at 1000 QPS. (This is an analytical projection, not yet a benchmarked figure — the harness's--query-concurrencysweep is what confirms it at load.)ConcurrentBitmaps' immutable COW snapshots (atomic.Pointer[termState]) give safe, zero-clone reads with no lock spanning the borrow.The roaring index, flat-file cold storage, and hot-chunk-index structure are unchanged; only the hot in-memory index went lock-free. Note
00aa03944records a ~+7% hot-ingest wall cost (clawed back from +42% via COW), so this trades a small write-side cost for the read-side clone elimination.Ingest robustness (rationale)
IngestLedgerEventswrites data + index + offsets to the per-chunk DB in one atomic, WAL-synced batch, then updates the in-memory cache (mirror + offsets). The crash/retry contract rests on three properties, made explicit in this PR:warmup. A crash anywhere is recovered by warmup replaying the atomic batch's CFs on next open.ConcurrentLedgerOffsets.Appendis positional, so the post-batch phase has no reachable error path; a crash is the only non-completion, and warmup repairs it.warmupOffsetsre-validates untrusted on-disk rows (gap / out-of-order / excess / cumulative overflow);verifyChunkConsistencycross-checks the CFs on open. Internal already-validated values are not re-checked.Reviewer notes
eventstore.Queryconcurrency test (TestHotStore_QueryUnderConcurrentIngest) is intentionally absent here — it moves to the query-engine PR alongsidequery.go.useXDRViewsread toggle were pared out of this PR (extractor → XDR view extractors: events, tx-hashes, tx-details, tx-pages from LedgerCloseMetaView #764; the read path is now unconditionally view-mode) to keep this change focused on the concurrent index + eventstore migration.Test plan
go build+go vet+go test ./cmd/stellar-rpc/internal/events/go build+go vet+go test ./cmd/stellar-rpc/internal/fullhistory/pkg/stores/eventstore/TestWarmup_Rejects*: data orphan, offsets gap, offsets overflow, missing tail, index-beyond-committed, orphan-in-empty-chunk)-race(go test -race ./cmd/stellar-rpc/internal/events/ -run ConcurrentLedgerOffsets)golangci-lint runclean on both packagesgo build ./...; thecmd/stellar-rpcbinary's native preflight/xdr2json link is unchanged and environment-dependent)🤖 Generated with Claude Code