fix(api): retain flushed new-chain queue rows instead of deleting them - #1018
Open
rickyrombo wants to merge 1 commit into
Open
fix(api): retain flushed new-chain queue rows instead of deleting them#1018rickyrombo wants to merge 1 commit into
rickyrombo wants to merge 1 commit into
Conversation
The flusher deleted each row after forwarding it. That is unrecoverable, and the genesis migration chain is regenerated before it ships -- the validator key is baked into every block header, so it has to be one the bootstrap node holds, which means rebuilding the chain. Anything already forwarded and deleted would survive only on the chain being discarded. Mark rows instead. A forwarded row gets flushed_at set and stays put, so the queue is a durable log: repoint the flusher at the rebuilt chain, set NewChainFlushFromBlock to the new backfill's end height, and re-drive. Replay is idempotent -- the same signed transactions are rejected as duplicates by a chain that already has them and accepted by a rebuilt one. The two non-forwarding paths are marked rather than deleted for the same reason, and record why in skip_reason: 'backfilled' for rows the genesis backfill covers, 'corrupt' for payloads that fail to unmarshal (previously deleted outright, taking the evidence with them). trimBackfillRows now only touches pending rows, so re-running it with a higher flush-from block after a regeneration does not disturb the record of what was already sent. Reads are unaffected in the steady state: the flusher only ever selects pending rows, and a partial index on (id) WHERE flushed_at IS NULL keeps retained rows off the hot path as the table grows. Retention is now an explicit operator decision rather than a side effect of flushing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The flusher deletes each row after forwarding it (
new_chain_flusher.go—DELETE FROM new_chain_queue WHERE id = $1). That is unrecoverable, and it collides with a fact about the migration: the genesis chain gets regenerated before it ships.The CometBFT validator key is hashed into every block header (
ValidatorsHash, plus the proposer address and chain ID), so the validator set cannot be swapped by editing genesis — the chain has to be rebuilt with a key the bootstrap node actually holds. Any write already forwarded and deleted would then survive only on the chain being discarded.Queueing is already enabled in production, so this wants to land before flushing is turned on.
What
Mark rows instead of deleting them. A forwarded row gets
flushed_atset and stays put, making the queue a durable log — repoint the flusher at the rebuilt chain, setNewChainFlushFromBlockto the new backfill's end height, and re-drive. Replay is idempotent: the same signed transactions are rejected as duplicates by a chain that already has them, and accepted by a rebuilt one.The two non-forwarding paths are marked for the same reason and record why in
skip_reason:DELETEflushed_at = now()DELETEflushed_at = now(),skip_reason = 'backfilled'tx_dataDELETEflushed_at = now(),skip_reason = 'corrupt'Corrupt rows previously took the evidence with them; they are now inspectable.
trimBackfillRowsonly touches pending rows, so re-running it with a higher flush-from block after a regeneration marks the newly-covered rows without disturbing the record of what was already sent.Ordering and performance
Unchanged in the steady state. The flusher still processes strictly in
idorder, one at a time, and still refuses to advance past a failed row — a failure leavesflushed_atNULL, so the next fetch returns that same row first.Reads select only pending rows, and a partial index (
ON new_chain_queue (id) WHERE flushed_at IS NULL) keeps retained rows out of the index entirely, so the hot path does not degrade as the table grows.Tradeoff
The table no longer self-truncates. Retention becomes an explicit operator decision rather than a side effect of flushing — deliberate here, since the whole point is not to lose rows, but worth a purge step once the migration is finished and rollback is off the table.
Migration
0238_new_chain_queue_cursor.sql— additive only: two nullable columns and one partial index. Existing rows default toflushed_at IS NULL, i.e. pending, which is correct given flushing has never run.Testing
go test ./api/ -run 'NewChainFlusher|EnqueueForNewChain'— 5 passed.Existing tests updated for retention semantics (
queueDepthsplit intopendingDepth/totalDepth, plus askipReasonshelper), and each now asserts that rows survive rather than vanish.New
TestNewChainFlusherRedriveAfterRegenerationcovers the capability this PR exists for: drain the queue against one chain, clearflushed_atas an operator would when repointing at the rebuilt chain, and confirm the same transactions forward again. Under delete-on-success there would be nothing left to re-drive.🤖 Generated with Claude Code