Skip to content

Stream bulk COPYs on sibling connections and coalesce persist backlogs - #707

Closed
aditya1702 wants to merge 4 commits into
persist/3-pipelinefrom
persist/4-seams
Closed

Stream bulk COPYs on sibling connections and coalesce persist backlogs#707
aditya1702 wants to merge 4 commits into
persist/3-pipelinefrom
persist/4-seams

Conversation

@aditya1702

@aditya1702 aditya1702 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Start with: the doc comment on persistLedgerData in internal/services/ingest_live.go. Its commit-ordering rules are the contract for everything here.

1. Bulk COPYs stream on sibling connections

The three COPY families ran sequentially in one transaction on one connection, so the insert phase cost their sum and one Postgres backend did all the index maintenance.

They now stream concurrently on three sibling connections, each in its own transaction, while a coordinating transaction stages everything else. Table sets are disjoint with no FKs between them.

Commits are the visibility point. Siblings commit first, the coordinating transaction — whose cursor decides which ledgers exist — strictly last.

  • Fail before the first commit → everything rolls back, retryable as before.
  • Fail after → wraps ErrPartialPersist, fatal. COPY has no ON CONFLICT, so a retry would collide on PKs.

The only crash state this produces is orphaned rows above the cursor, which DeleteRowsAboveLedger clears at startup.

2. Backlogs coalesce into batched commits

When process outruns persist, up to --live-persist-max-batch-size consecutive ledgers fold into one commit set. At normal cadence every batch is size 1 and nothing changes.

3 and 4

Siblings commit with synchronous_commit = off (durability unchanged — the coordinator's synchronous commit covers their WAL). And batches only cut on unseen classification inputs, not any observation — the old rule cut every batch to size 1 and disabled batching entirely.

No performance numbers yet — review on design and correctness.


4 of 6 splitting #684. schema → fixes → pipeline → seams → write families → hardening

…with coordinated late commits

The live persist ran its three COPY families — transactions(+accounts),
operations(+accounts), state_changes — sequentially inside one
transaction on one connection, so the insert phase cost their sum and a
single Postgres backend did all the index maintenance. They now stream
concurrently on three sibling connections, each in its own transaction,
while the coordinating transaction stages everything else (assets,
contracts, classification, protocol state, token changes, cursor). The
table sets are disjoint with no FKs among them, and every goroutine only
reads the quiescent buffer.

Commits are the visibility point and are held until all four
transactions have done their work: siblings commit first (sub-ms each),
the coordinating transaction — whose cursor is the authority on which
ledgers exist — strictly last. A failure before the first commit rolls
everything back and stays retryable exactly as before; a failure after
it wraps the new ErrPartialPersist sentinel, which
isPermanentPersistError classifies as fatal, because COPY has no ON
CONFLICT and re-running the ledger would collide on primary keys.

The only crash state this ordering can produce is orphaned bulk rows for
the single ledger past the committed cursor, so startup runs
IngestStoreModel.DeleteRowsAboveLedger before resuming: one transaction
of TOID-bounded deletes (rows of ledgers > cursor are exactly rows with
TOID >= toid.New(cursor+1,0,0)) across the five bulk tables, kept
chunk-local by each table's chunk skipping. Backfill keeps the
single-transaction insertIntoDB path unchanged.
…ommits

When the process stage finishes ledgers faster than persist drains them,
the persist stage now folds up to --live-persist-max-batch-size (default
5) consecutive ledgers into one commit set: each sibling COPY streams the
whole batch on its connection, the coordinating transaction stages the
batch's ledgers in order, and the commit barrier fires once — amortizing
COPY setup, index-page churn, and fsyncs across the backlog. While the
pipeline keeps pace every batch has size 1 and behavior is unchanged.

A ledger with classification inputs always opens its own batch: its
plan's pool reads see exactly what the previous batch committed, which
preserves the deployed-contract-sees-prior-wasm invariant. The cursor
stays the authority — one guarded update lands on the batch's last
ledger, and a pre-commit failure rolls back and retries the whole batch.
wallet_ingestion_persist_batch_size observes coalescing; per-ledger
duration histograms record each ledger's amortized share so panels stay
comparable across batch sizes.
…on WAL flush

SET LOCAL synchronous_commit = off on each sibling session removes up to
three serialized fsync waits per persist. Durability is unchanged: the
coordinating transaction commits synchronously and strictly last, and its
flush covers all earlier WAL including the sibling commit records, so a
durable cursor implies durable siblings; rows a crash could lose are
exactly the unacknowledged ones startup reconciliation deletes.
The classification-safety cut isolated any ledger carrying protocol
wasm/contract observations, but a plan's pool reads are only unsound for
inputs no committed batch has classified yet — and re-observations of
known contracts are the overwhelmingly common case (synthetic loadtest
traffic re-observes the same token contracts every ledger, which cut
every batch to size one and disabled batching entirely; goroutine dumps
showed the process stage starved of buffers held by the always-cut
pending queue). The persist goroutine now keeps seen-sets of wasm hashes
and contract IDs folded in after each successful commit; only a ledger
introducing unseen inputs opens its own batch. A rolled-back batch marks
nothing, and a restart starts empty — conservative until re-warmed.
@aditya1702

Copy link
Copy Markdown
Contributor Author

Reopened as #711 — the branch was renamed to persist/4-parallel-writes and GitHub closes a PR when its source branch is renamed. Same commits, same code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant