Skip to content

Commit 6199e5e

Browse files
committed
chain: Overhaul persistence layer
1 parent 1df20a4 commit 6199e5e

15 files changed

Lines changed: 1312 additions & 733 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
default: minor
3+
---
4+
5+
# Support concurrent chain.Manager reads via MVCC snapshots
6+
7+
The Manager previously serialized all of its methods -- including read-only ones -- with a single mutex, so heavy read load (e.g. from the syncer or subscribers) contended with block processing. The Manager now follows an MVCC scheme: writes accumulate in the Store's "scratchpad" -- which includes the tip state -- and are committed by Flush, at which point they become visible to "snapshots": self-contained, read-only captures of the committed state. Read-only Manager methods operate on snapshots, so they proceed concurrently with each other and with block processing, and never stall writers. bbolt supports snapshots natively via read-only transactions; `MemDB` now implements them with pin-aware copy-on-flush generations (`CacheDB` delegates to its underlying DB).
8+
9+
This is a breaking change for implementors and consumers of the `chain.DB` and `chain.Store` interfaces:
10+
11+
- `Store` is now `Snapshot() (StoreSnapshot, func())` + `Scratchpad() StoreScratchpad`. `StoreSnapshot` carries the read methods plus `TipState`; `StoreScratchpad` embeds it and adds the write methods and `Flush`, with `ApplyBlock`/`RevertBlock` updating the scratchpad's tip. Implementations may flush autonomously between operations (e.g. to bound the size or age of the accumulated writes), handling errors internally; the Manager explicitly calls Flush to publish the new tip after processing blocks, before notifying OnReorg listeners.
12+
- `DB` mirrors `Store`: `Snapshot() (DBSnapshot, release func())` + `Scratchpad() DBScratchpad`. The scratchpad carries `Bucket`/`CreateBucket`/`Flush`/`Cancel` and must not flush autonomously; snapshots are read-only and remain valid until released, even across concurrent Flushes. A single `DBBucket` interface serves both sides; its `Put`/`Delete` methods may return errors on read-only (snapshot) buckets.
13+
- `NewManager` no longer takes a `consensus.State` parameter, and `NewDBStore`/`NewDBStoreAtCheckpoint` no longer return one; the tip state is provided by the Store itself (via a snapshot's or scratchpad's `TipState`).
14+
- Reads reflect committed state. Pruned blocks remain visible to read-only methods until the next commit, and blocks added to non-best chains remain *invisible* to them until the next commit. Blocks that extend the best chain are always committed before `AddBlocks` returns and before `OnReorg` listeners fire, so subscribers observe the tip they are notified of.

chain/chain_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,11 @@ func TestV2Attestations(t *testing.T) {
129129
}
130130

131131
t.Run("arbitrary data", func(t *testing.T) {
132-
store, tipState, err := chain.NewDBStore(chain.NewMemDB(), n, genesisBlock, nil)
132+
store, err := chain.NewDBStore(chain.NewMemDB(), n, genesisBlock, nil)
133133
if err != nil {
134134
t.Fatal(err)
135135
}
136-
cm := chain.NewManager(store, tipState)
136+
cm := chain.NewManager(store)
137137
ms := newMemState()
138138

139139
// mine until a utxo is spendable
@@ -164,11 +164,11 @@ func TestV2Attestations(t *testing.T) {
164164
})
165165

166166
t.Run("arbitrary data + attestation + no change output", func(t *testing.T) {
167-
store, tipState, err := chain.NewDBStore(chain.NewMemDB(), n, genesisBlock, nil)
167+
store, err := chain.NewDBStore(chain.NewMemDB(), n, genesisBlock, nil)
168168
if err != nil {
169169
t.Fatal(err)
170170
}
171-
cm := chain.NewManager(store, tipState)
171+
cm := chain.NewManager(store)
172172
ms := newMemState()
173173

174174
// mine until a utxo is spendable
@@ -213,11 +213,11 @@ func TestV2Attestations(t *testing.T) {
213213
})
214214

215215
t.Run("arbitrary data + attestation", func(t *testing.T) {
216-
store, tipState, err := chain.NewDBStore(chain.NewMemDB(), n, genesisBlock, nil)
216+
store, err := chain.NewDBStore(chain.NewMemDB(), n, genesisBlock, nil)
217217
if err != nil {
218218
t.Fatal(err)
219219
}
220-
cm := chain.NewManager(store, tipState)
220+
cm := chain.NewManager(store)
221221
ms := newMemState()
222222

223223
// mine until a utxo is spendable
@@ -257,11 +257,11 @@ func TestV2Attestations(t *testing.T) {
257257
})
258258

259259
t.Run("arbitrary data + attestation + change output", func(t *testing.T) {
260-
store, tipState, err := chain.NewDBStore(chain.NewMemDB(), n, genesisBlock, nil)
260+
store, err := chain.NewDBStore(chain.NewMemDB(), n, genesisBlock, nil)
261261
if err != nil {
262262
t.Fatal(err)
263263
}
264-
cm := chain.NewManager(store, tipState)
264+
cm := chain.NewManager(store)
265265
ms := newMemState()
266266

267267
// mine until a utxo is spendable

0 commit comments

Comments
 (0)