Skip to content

Commit 1ceed0b

Browse files
committed
triedb/pathdb, eth: use double-buffer mechanism in pathdb
1 parent 3cc0e7a commit 1ceed0b

File tree

12 files changed

+302
-122
lines changed

12 files changed

+302
-122
lines changed

core/state/snapshot/generate_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,18 @@ func (t *testHelper) Commit() common.Hash {
242242
}
243243
t.triedb.Update(root, types.EmptyRootHash, 0, t.nodes, t.states)
244244
t.triedb.Commit(root, false)
245+
246+
// re-open the trie database to ensure the frozen buffer
247+
// is not referenced
248+
//config := &triedb.Config{}
249+
//if t.triedb.Scheme() == rawdb.PathScheme {
250+
// config.PathDB = &pathdb.Config{
251+
// SnapshotNoBuild: true,
252+
// } // disable caching
253+
//} else {
254+
// config.HashDB = &hashdb.Config{} // disable caching
255+
//}
256+
//t.triedb = triedb.NewDatabase(t.triedb.Disk(), config)
245257
return root
246258
}
247259

core/state/statedb_test.go

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -974,20 +974,23 @@ func TestMissingTrieNodes(t *testing.T) {
974974
func testMissingTrieNodes(t *testing.T, scheme string) {
975975
// Create an initial state with a few accounts
976976
var (
977-
tdb *triedb.Database
978-
memDb = rawdb.NewMemoryDatabase()
977+
tdb *triedb.Database
978+
memDb = rawdb.NewMemoryDatabase()
979+
openDb = func() *triedb.Database {
980+
if scheme == rawdb.PathScheme {
981+
return triedb.NewDatabase(memDb, &triedb.Config{PathDB: &pathdb.Config{
982+
TrieCleanSize: 0,
983+
StateCleanSize: 0,
984+
WriteBufferSize: 0,
985+
}}) // disable caching
986+
} else {
987+
return triedb.NewDatabase(memDb, &triedb.Config{HashDB: &hashdb.Config{
988+
CleanCacheSize: 0,
989+
}}) // disable caching
990+
}
991+
}
979992
)
980-
if scheme == rawdb.PathScheme {
981-
tdb = triedb.NewDatabase(memDb, &triedb.Config{PathDB: &pathdb.Config{
982-
TrieCleanSize: 0,
983-
StateCleanSize: 0,
984-
WriteBufferSize: 0,
985-
}}) // disable caching
986-
} else {
987-
tdb = triedb.NewDatabase(memDb, &triedb.Config{HashDB: &hashdb.Config{
988-
CleanCacheSize: 0,
989-
}}) // disable caching
990-
}
993+
tdb = openDb()
991994
db := NewDatabase(tdb, nil)
992995

993996
var root common.Hash
@@ -1005,17 +1008,27 @@ func testMissingTrieNodes(t *testing.T, scheme string) {
10051008
tdb.Commit(root, false)
10061009
}
10071010
// Create a new state on the old root
1008-
state, _ = New(root, db)
10091011
// Now we clear out the memdb
10101012
it := memDb.NewIterator(nil, nil)
10111013
for it.Next() {
10121014
k := it.Key()
1013-
// Leave the root intact
1014-
if !bytes.Equal(k, root[:]) {
1015-
t.Logf("key: %x", k)
1016-
memDb.Delete(k)
1015+
if scheme == rawdb.HashScheme {
1016+
if !bytes.Equal(k, root[:]) {
1017+
t.Logf("key: %x", k)
1018+
memDb.Delete(k)
1019+
}
1020+
}
1021+
if scheme == rawdb.PathScheme {
1022+
rk := k[len(rawdb.TrieNodeAccountPrefix):]
1023+
if len(rk) != 0 {
1024+
t.Logf("key: %x", k)
1025+
memDb.Delete(k)
1026+
}
10171027
}
10181028
}
1029+
tdb = openDb()
1030+
db = NewDatabase(tdb, nil)
1031+
state, _ = New(root, db)
10191032
balance := state.GetBalance(addr)
10201033
// The removed elem should lead to it returning zero balance
10211034
if exp, got := uint64(0), balance.Uint64(); got != exp {

triedb/pathdb/buffer.go

Lines changed: 71 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package pathdb
1818

1919
import (
20+
"errors"
2021
"fmt"
2122
"time"
2223

@@ -37,6 +38,9 @@ type buffer struct {
3738
limit uint64 // The maximum memory allowance in bytes
3839
nodes *nodeSet // Aggregated trie node set
3940
states *stateSet // Aggregated state set
41+
42+
done chan struct{} // notifier whether the content in buffer has been flushed or not
43+
flushErr error // error if any exception occurs during flushing
4044
}
4145

4246
// newBuffer initializes the buffer with the provided states and trie nodes.
@@ -124,43 +128,74 @@ func (b *buffer) size() uint64 {
124128

125129
// flush persists the in-memory dirty trie node into the disk if the configured
126130
// memory threshold is reached. Note, all data must be written atomically.
127-
func (b *buffer) flush(root common.Hash, db ethdb.KeyValueStore, freezer ethdb.AncientWriter, progress []byte, nodesCache, statesCache *fastcache.Cache, id uint64) error {
128-
// Ensure the target state id is aligned with the internal counter.
129-
head := rawdb.ReadPersistentStateID(db)
130-
if head+b.layers != id {
131-
return fmt.Errorf("buffer layers (%d) cannot be applied on top of persisted state id (%d) to reach requested state id (%d)", b.layers, head, id)
131+
func (b *buffer) flush(root common.Hash, db ethdb.KeyValueStore, freezer ethdb.AncientWriter, progress []byte, nodesCache, statesCache *fastcache.Cache, id uint64, postFlush func()) {
132+
if b.done != nil {
133+
panic("duplicated flush operation")
132134
}
133-
// Terminate the state snapshot generation if it's active
134-
var (
135-
start = time.Now()
136-
batch = db.NewBatchWithSize((b.nodes.dbsize() + b.states.dbsize()) * 11 / 10) // extra 10% for potential pebble internal stuff
137-
)
138-
// Explicitly sync the state freezer to ensure all written data is persisted to disk
139-
// before updating the key-value store.
140-
//
141-
// This step is crucial to guarantee that the corresponding state history remains
142-
// available for state rollback.
143-
if freezer != nil {
144-
if err := freezer.SyncAncient(); err != nil {
145-
return err
135+
b.done = make(chan struct{}) // allocate the channel for notification
136+
137+
go func() {
138+
defer func() {
139+
if postFlush != nil {
140+
postFlush()
141+
}
142+
close(b.done)
143+
}()
144+
145+
// Ensure the target state id is aligned with the internal counter.
146+
head := rawdb.ReadPersistentStateID(db)
147+
if head+b.layers != id {
148+
b.flushErr = fmt.Errorf("buffer layers (%d) cannot be applied on top of persisted state id (%d) to reach requested state id (%d)", b.layers, head, id)
149+
return
146150
}
151+
152+
// Terminate the state snapshot generation if it's active
153+
var (
154+
start = time.Now()
155+
batch = db.NewBatchWithSize((b.nodes.dbsize() + b.states.dbsize()) * 11 / 10) // extra 10% for potential pebble internal stuff
156+
)
157+
// Explicitly sync the state freezer to ensure all written data is persisted to disk
158+
// before updating the key-value store.
159+
//
160+
// This step is crucial to guarantee that the corresponding state history remains
161+
// available for state rollback.
162+
if freezer != nil {
163+
if err := freezer.SyncAncient(); err != nil {
164+
b.flushErr = err
165+
return
166+
}
167+
}
168+
nodes := b.nodes.write(batch, nodesCache)
169+
accounts, slots := b.states.write(batch, progress, statesCache)
170+
rawdb.WritePersistentStateID(batch, id)
171+
rawdb.WriteSnapshotRoot(batch, root)
172+
173+
// Flush all mutations in a single batch
174+
size := batch.ValueSize()
175+
if err := batch.Write(); err != nil {
176+
b.flushErr = err
177+
return
178+
}
179+
commitBytesMeter.Mark(int64(size))
180+
commitNodesMeter.Mark(int64(nodes))
181+
commitAccountsMeter.Mark(int64(accounts))
182+
commitStoragesMeter.Mark(int64(slots))
183+
commitTimeTimer.UpdateSince(start)
184+
185+
// The content in the frozen buffer is kept for consequent state access,
186+
// TODO (rjl493456442) measure the gc overhead for holding this struct.
187+
// TODO (rjl493456442) can we somehow get rid of it after flushing??
188+
b.reset()
189+
log.Debug("Persisted buffer content", "nodes", nodes, "accounts", accounts, "slots", slots, "bytes", common.StorageSize(size), "elapsed", common.PrettyDuration(time.Since(start)))
190+
}()
191+
}
192+
193+
// waitFlush blocks until the buffer has been fully flushed and returns any
194+
// stored errors that occurred during the process.
195+
func (b *buffer) waitFlush() error {
196+
if b.done == nil {
197+
return errors.New("the buffer is not frozen")
147198
}
148-
nodes := b.nodes.write(batch, nodesCache)
149-
accounts, slots := b.states.write(batch, progress, statesCache)
150-
rawdb.WritePersistentStateID(batch, id)
151-
rawdb.WriteSnapshotRoot(batch, root)
152-
153-
// Flush all mutations in a single batch
154-
size := batch.ValueSize()
155-
if err := batch.Write(); err != nil {
156-
return err
157-
}
158-
commitBytesMeter.Mark(int64(size))
159-
commitNodesMeter.Mark(int64(nodes))
160-
commitAccountsMeter.Mark(int64(accounts))
161-
commitStoragesMeter.Mark(int64(slots))
162-
commitTimeTimer.UpdateSince(start)
163-
b.reset()
164-
log.Debug("Persisted buffer content", "nodes", nodes, "accounts", accounts, "slots", slots, "bytes", common.StorageSize(size), "elapsed", common.PrettyDuration(time.Since(start)))
165-
return nil
199+
<-b.done
200+
return b.flushErr
166201
}

triedb/pathdb/database.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,10 @@ type Config struct {
119119
StateCleanSize int // Maximum memory allowance (in bytes) for caching clean state data
120120
WriteBufferSize int // Maximum memory allowance (in bytes) for write buffer
121121
ReadOnly bool // Flag whether the database is opened in read only mode
122-
SnapshotNoBuild bool // Flag Whether the background generation is allowed
122+
123+
// Testing configurations
124+
SnapshotNoBuild bool // Flag Whether the background generation is allowed
125+
NoAsyncFlush bool // Flag whether the background generation is allowed
123126
}
124127

125128
// sanitize checks the provided user configurations and changes anything that's
@@ -434,6 +437,9 @@ func (db *Database) Disable() error {
434437
// Terminate the state generator if it's active and mark the disk layer
435438
// as stale to prevent access to persistent state.
436439
disk := db.tree.bottom()
440+
if err := disk.waitFlush(); err != nil {
441+
return err
442+
}
437443
if disk.generator != nil {
438444
disk.generator.stop()
439445
}
@@ -592,12 +598,18 @@ func (db *Database) Close() error {
592598
// following mutations.
593599
db.readOnly = true
594600

601+
// Block until the background flushing is finished. It must
602+
// be done before terminating the potential background snapshot
603+
// generator.
604+
dl := db.tree.bottom()
605+
if err := dl.waitFlush(); err != nil {
606+
return err
607+
}
595608
// Terminate the background generation if it's active
596-
disk := db.tree.bottom()
597-
if disk.generator != nil {
598-
disk.generator.stop()
609+
if dl.generator != nil {
610+
dl.generator.stop()
599611
}
600-
disk.resetCache() // release the memory held by clean cache
612+
dl.resetCache() // release the memory held by clean cache
601613

602614
// Close the attached state history freezer.
603615
if db.freezer == nil {

triedb/pathdb/database_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ func newTester(t *testing.T, historyLimit uint64, isVerkle bool, layers int) *te
129129
TrieCleanSize: 256 * 1024,
130130
StateCleanSize: 256 * 1024,
131131
WriteBufferSize: 256 * 1024,
132+
NoAsyncFlush: true,
132133
}, isVerkle)
133134

134135
obj = &tester{

0 commit comments

Comments
 (0)