Skip to content

chain: Overhaul persistence layer#450

Open
lukechampine wants to merge 2 commits into
masterfrom
snapshot
Open

chain: Overhaul persistence layer#450
lukechampine wants to merge 2 commits into
masterfrom
snapshot

Conversation

@lukechampine

Copy link
Copy Markdown
Member

This supersedes #437, implementing true MVCC snapshots for the Manager. Read-only methods (like Tip(), BlocksForHistory(), etc.) see a consistent "snapshot" of the Manager state, while state-mutating methods (like AddBlocks) operate on a "scratchpad," that, once committed, becomes the next snapshot. These are equivalent to BoltDB's View and Update, respectively.

The effect of this is that readers can't block writers, and writers can't block readers. Writers do block other writers -- for example, AddBlocks calls still need to be serialized -- but that's it.

This code is a pretty even split between myself and Claude. I designed the interfaces and refined the APIs to my liking, while Claude handled the mechanical refactoring, the MemDB updates, and writing tests. I suspect there is some slop lurking in here that will turn up in review, but overall I'm happy with the result.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR overhauls the chain persistence and concurrency model by introducing MVCC-style snapshots for read-only access and a scratchpad for serialized writes, aiming to eliminate reader/writer contention in chain.Manager while preserving consistent views of committed chain state.

Changes:

  • Refactors chain.DB and chain.Store into Snapshot() + Scratchpad() APIs, with committed state exposed via snapshots and buffered writes via scratchpads.
  • Updates chain.Manager to use snapshots for read methods and a scratchpad (behind a mutex) for state-mutating methods, plus adds concurrency-focused tests.
  • Updates callers/tests and BoltDB/MemDB/CacheDB implementations to support the new snapshot/scratchpad semantics; includes a changeset documenting the breaking API changes.

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
wallet/wallet_test.go Adapts wallet tests to NewDBStore / NewManager signature changes and new tip-state access.
syncer/syncer_test.go Updates syncer tests for new store/manager initialization APIs.
rhp/v4/rpc_test.go Updates test node setup for new store/manager initialization APIs.
miner_test.go Updates miner tests for new store/manager initialization APIs.
internal/cmd/sync/main.go Updates command to new NewDBStore / NewManager call patterns.
internal/cmd/calcswaps/main.go Switches to scratchpad reads/writes and fixes height loop underflow behavior.
db.go Reworks Bolt-backed DB into snapshot (read tx) + scratchpad (lazy write tx) model.
chain/pool_test.go Updates pool tests for new store/manager initialization APIs.
chain/migrate.go Migrates DB migration logic to operate via the store scratchpad/snapshot helpers.
chain/manager.go Implements MVCC-style snapshot reads and scratchpad writes; updates Store interfaces and manager locking strategy.
chain/manager_test.go Updates existing tests and adds new concurrency/snapshot behavior tests.
chain/db.go Introduces DB snapshot/scratchpad interfaces and implements snapshotting in MemDB/CacheDB/DBStore.
chain/db_test.go Updates DB tests for new APIs and adds snapshot behavior tests for MemDB/CacheDB/BoltDB.
chain/chain_test.go Updates chain tests for new store/manager initialization APIs.
.changeset/support_concurrent_chain_manager_reads_via_mvcc_snapshots.md Documents the breaking interface/API changes and new MVCC behavior.
Files not reviewed (1)
  • internal/cmd/calcswaps/main.go: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread chain/manager.go Outdated
Comment thread chain/db.go Outdated
Comment thread chain/db.go
Comment thread chain/db.go
s.applyElements(cau)
}
s.tip = cs
if s.shouldFlush() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use flushIfFull in this case? A snapshot reader could briefly see the tip height go down whereas previously tip height was monotonic

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deletions also contribute to unflushed so I think using flushIfFull isn't enough here. Either we explicitly skip flushing when the resulting height is lower or we prevent delete from incrementing unflushed.

But also this just makes it feel like dbScratchpad is too low level to be able to decide whether flushing is safe.

Comment thread chain/db_test.go
"lukechampine.com/frand"
)

// NOTE: due to a bug in the transaction validation code, calculating payouts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing this comment seems unrelated to this PR?

@Alrighttt Alrighttt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM; I prefer this over #437 .

@@ -0,0 +1,14 @@
---
default: minor

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably deserves a major since it breaks every single piece of node software we got. e.g. chain.NewManager also has a different signature now without the tipState.

Comment thread chain/db.go
// is called. Its methods observe the accumulated writes. It is not safe for
// concurrent use.
//
// Unlike a StoreScratchpad, a DBScratchpad must not flush autonomously:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nit) slightly confusing that a DBScratchpad must not flush autonomously but a dbScratchpad flushes all the time because it's not actually a DBScratchpad but a StoreScratchpad

Comment thread chain/db.go
s.applyElements(cau)
}
s.tip = cs
if s.shouldFlush() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deletions also contribute to unflushed so I think using flushIfFull isn't enough here. Either we explicitly skip flushing when the resulting height is lower or we prevent delete from incrementing unflushed.

But also this just makes it feel like dbScratchpad is too low level to be able to decide whether flushing is safe.

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.

7 participants