persist blocks and FullCommitQCs in data layer via WAL#3126
persist blocks and FullCommitQCs in data layer via WAL#3126wen-coding wants to merge 3 commits intomainfrom
Conversation
|
The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #3126 +/- ##
==========================================
+ Coverage 58.55% 58.60% +0.05%
==========================================
Files 2099 2101 +2
Lines 173847 174176 +329
==========================================
+ Hits 101803 102084 +281
- Misses 62873 62894 +21
- Partials 9171 9198 +27
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
73df12b to
2613700
Compare
sei-tendermint/internal/autobahn/consensus/persist/globalblocks.go
Outdated
Show resolved
Hide resolved
sei-tendermint/internal/autobahn/consensus/persist/globalblocks.go
Outdated
Show resolved
Hide resolved
sei-tendermint/internal/autobahn/consensus/persist/globalcommitqcs.go
Outdated
Show resolved
Hide resolved
sei-tendermint/internal/autobahn/consensus/persist/fullcommitqcs.go
Outdated
Show resolved
Hide resolved
sei-tendermint/internal/autobahn/consensus/persist/globalcommitqcs.go
Outdated
Show resolved
Hide resolved
sei-tendermint/internal/autobahn/consensus/persist/globalcommitqcs.go
Outdated
Show resolved
Hide resolved
e24ad7d to
9c79a0c
Compare
| return nil | ||
| } | ||
| if keepIdx >= w.nextIdx { | ||
| return w.TruncateAll() |
There was a problem hiding this comment.
I meant, isn't w.wal.TruncateBefore(w.nextIdx) equivalent to w.wal.TruncateAll()?
There was a problem hiding this comment.
The main difference is that when we are removing all entries, we don't need to verify every removed entry any more. We verified in TruncateBefore because we were afraid somehow we messed up with entry boundary and thought we removed one entry but actually screwed up the whole file (that said, how we can recover from this case is a big problem.)
If you think we can still verify, then we can do TruncateBefore.
There was a problem hiding this comment.
I find this distinction rather artificial in terms of requirements, but that's not a strong opinion.
There was a problem hiding this comment.
That's fair, changed
sei-tendermint/internal/autobahn/consensus/persist/fullcommitqcs.go
Outdated
Show resolved
Hide resolved
sei-tendermint/internal/autobahn/consensus/persist/fullcommitqcs.go
Outdated
Show resolved
Hide resolved
sei-tendermint/internal/autobahn/consensus/persist/fullcommitqcs.go
Outdated
Show resolved
Hide resolved
sei-tendermint/internal/autobahn/consensus/persist/fullcommitqcs_test.go
Outdated
Show resolved
Hide resolved
sei-tendermint/internal/autobahn/consensus/persist/globalblocks.go
Outdated
Show resolved
Hide resolved
sei-tendermint/internal/autobahn/consensus/persist/globalblocks_test.go
Outdated
Show resolved
Hide resolved
| } | ||
| // Persist QCs and blocks in parallel. | ||
| if err := scope.Parallel(func(ps scope.ParallelScope) error { | ||
| ps.Spawn(func() error { |
There was a problem hiding this comment.
note that it may happen that blocks will be persisted without their corresponding qcs. You should resolve it when loading.
There was a problem hiding this comment.
we now skip blocks without QC
There was a problem hiding this comment.
afaict NewGlobalBlockPersister still does s.next = loaded[len(loaded)-1].Number + 1 even if the last block does not have the corresponding QC and reconciliation only fixes the prefix mismatch
There was a problem hiding this comment.
ok, got it, I see that the extra blocks are ignored on load, and then the writes of those blocks are ignored afterwards. I suppose that's fine, while again it is an asymmetry that truncation consistency is maintained by WAL, while suffix consistency is not.
ec822a7 to
6801499
Compare
dd2768f to
e65b969
Compare
| } | ||
|
|
||
| // globalBlockState is the mutable state protected by GlobalBlockPersister's mutex. | ||
| type globalBlockState struct { |
There was a problem hiding this comment.
globalBlockState does not have committee, despite it is needed in the same sense as in case of fullcommitqcsState. Is this asymmetry intentional?
There was a problem hiding this comment.
I originally added it, then figured we can save a few bytes by passing firstBlock in directly and not saving committee.
But I agree symmetry is a good reason, adding it back.
|
|
||
| // zeroCommittee is a committee with FirstBlock()==0, used by persister tests | ||
| // that persist blocks starting at global block number 0. | ||
| var zeroCommittee = func() *types.Committee { |
There was a problem hiding this comment.
are these tests testing any properties that are specific to chain starting at 0? If not, can we make the tests use a randomized starting point instead?
There was a problem hiding this comment.
That's a good point, I don't believe we depend on any tests starting at 0, changed.
- Add GlobalBlockPersister and FullCommitQCPersister backed by indexedWAL - Group into DataWAL struct; NewState takes DataWAL for crash recovery - NewState verifies loaded data via insertQC/insertBlock (signatures + hashes) - DataWAL.reconcile() fixes WAL cursor inconsistencies at construction - Add TruncateWhile to indexedWAL; TruncateBefore handles walIdx >= nextIdx - Async persistence via runPersist with parallel QC/block writes - PushQC/PushBlock are pure in-memory; errors only from verification - nextBlockToPersist cursor gates PushAppHash for AppVote safety - Pruning keeps at least one entry to prevent empty WALs on restart Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
e65b969 to
052702c
Compare
| } | ||
|
|
||
| // insertQC verifies and inserts a FullCommitQC into the inner state. | ||
| // On the first QC, it sets inner.first and related cursors (handles |
There was a problem hiding this comment.
cursors are already set in constructor, no?
There was a problem hiding this comment.
ok, now I see what you mean. In case inner is empty, it will assume the new starting point instead of assuming committee.FirstBlock(). nvm then
There was a problem hiding this comment.
nit: imo an explicit prune() call for shifting starting point would be more readable. Up to you
| // insertBlock verifies a block and inserts it into the inner state. | ||
| // Requires a QC to already be present for block n. | ||
| func (i *inner) insertBlock(committee *types.Committee, n types.GlobalBlockNumber, block *types.Block) error { | ||
| if err := block.Verify(committee); err != nil { |
There was a problem hiding this comment.
this verification (relatively expensive) can be moved after the early exit checks.
| if err := block.Verify(committee); err != nil { | ||
| return fmt.Errorf("block.Verify(): %w", err) | ||
| } | ||
| if n < i.first || n >= i.nextQC { |
There was a problem hiding this comment.
there is a weird corner case with this implementation: in case of, let's say archival node (or a node with a very long retention), pruning does not remove the QC ranges of blocks atomically. Therefore we will end up with the first QC missing a prefix of blocks on restart. After restart nextBlock will be set to first - data.State will think that there are some very old blocks missing and will try to fetch them. That is not a desirable behavior, since old blocks are stored for the convenience of peers - our node should not be blocked on availability of old blocks that it already has executed.
|
|
||
| // insertBlock verifies a block and inserts it into the inner state. | ||
| // Requires a QC to already be present for block n. | ||
| func (i *inner) insertBlock(committee *types.Committee, n types.GlobalBlockNumber, block *types.Block) error { |
There was a problem hiding this comment.
nit: can we deduplicate logic of insertBlock with PushQC/PushBlock?
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace zeroCommittee with types.GenCommittee (random FirstBlock). All block numbers are now relative to committee.FirstBlock(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
GlobalBlockPersisterandFullCommitQCPersisterbacked byindexedWALfor data-layer crash recoveryDataWALparameter struct consumed bydata.NewStateNewStaterecovery verifies all loaded data viainsertQC/insertBlock(signatures + hash matching), treating WAL data as untrusted. Blocks outside QC range are ignored. Cursors fast-forward after pruningDataWAL.reconcile()fixes WAL cursor inconsistencies (crash between parallel truncations) at construction time.NewStatejust asserts consistencyPersistedWrapperTruncateWhiletoindexedWALfor predicate-based truncation (single-pass scan+truncate)indexedWAL.TruncateBeforehandlewalIdx >= nextIdxviaTruncateAll(removes special-case branches)BlockStoreinterface (superseded byDataWAL)GlobalCommitQCPersister→FullCommitQCPersister; on-disk dir tofullcommitqcsLoadNext→Next()on both persistersConsumeLoaded()(thread-safe, clearer ownership)PushQC/PushBlockare pure in-memory operations — errors only reflect verification failures. BackgroundrunPersistgoroutine writes QCs (eagerly up tonextQC) and blocks (up tonextBlock) in parallel viascope.Parallel. Persistence errors propagate vertically viaRun()nextBlockToPersistcursor advances tomin(persistedQC, persistedBlock)after both are durable.PushAppHash(now takesctx) waits on this cursor, ensuring AppVotes are only issued for persisted dataDataWAL.TruncateBeforeTest plan
globalblocks_test.go: persist & reload, truncate & reload, truncate all, no-op, duplicate ignored, gap error, continue after reloadfullcommitqcs_test.go: persist & reload, truncate & reload, truncate all, no-op, duplicate ignored, gap error, mid-range truncation, continue after reloadwal_test.go:TruncateWhile— empty, none match, partial, all, reopen afterstate_test.go:TestStateRecoveryFromWAL— full recovery; third restart verifies WALs not wipedTestStateRecoveryBlocksOnly— QCs WAL lost, blocks re-pushed with QCTestStateRecoveryQCsOnly— blocks WAL lost, cursor sync via reconcileTestStateRecoveryAfterPruning— both WALs truncated, only tail survivesTestStateRecoverySkipsStaleBlocks— blocks before first QC range ignoredTestStateRecoveryBlocksBehindQCs— QCs ahead of blocks, gap re-fetchedTestStateRecoveryIgnoresBlocksBeyondQC— blocks beyond QC range ignoredTestPruningKeepsLastEntry— pruning never empties state; restart recoversTestExecution— async persistence + PushAppHash wait semanticsdata,avail,consensus,p2p/giga)🤖 Generated with Claude Code