From 1ae425b0ce15f1841221bdd7c41ee07ca716a127 Mon Sep 17 00:00:00 2001 From: Ganesh Vanahalli Date: Tue, 3 Dec 2024 15:04:49 -0600 Subject: [PATCH 1/3] Create Live Snapshot without shutting down node --- arbitrum/recordingdb.go | 4 ++ core/rawdb/database.go | 10 +++++ core/rawdb/freezer.go | 64 ++++++++++++++++++++++++++++++++ core/rawdb/freezer_memory.go | 4 ++ core/rawdb/freezer_resettable.go | 5 +++ core/rawdb/table.go | 6 +++ ethdb/database.go | 1 + ethdb/leveldb/leveldb.go | 4 ++ ethdb/memorydb/memorydb.go | 4 ++ ethdb/pebble/pebble.go | 6 +++ ethdb/remotedb/remotedb.go | 6 +++ ethdb/snapshot.go | 3 ++ trie/trie_test.go | 3 ++ 13 files changed, 120 insertions(+) diff --git a/arbitrum/recordingdb.go b/arbitrum/recordingdb.go index f7fd017520..c4156b18db 100644 --- a/arbitrum/recordingdb.go +++ b/arbitrum/recordingdb.go @@ -39,6 +39,10 @@ func newRecordingKV(inner *triedb.Database, diskDb ethdb.KeyValueStore) *Recordi return &RecordingKV{inner, diskDb, make(map[common.Hash][]byte), false} } +func (db *RecordingKV) CreateDBSnapshot(dir string) error { + return errors.New("createDBSnapshot method is not supported") +} + func (db *RecordingKV) Has(key []byte) (bool, error) { return false, errors.New("recording KV doesn't support Has") } diff --git a/core/rawdb/database.go b/core/rawdb/database.go index e0d76fabc7..fe2a39e113 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -86,6 +86,16 @@ func (frdb *freezerdb) Freeze() error { return nil } +func (db *freezerdb) CreateDBSnapshot(dir string) error { + if err := db.KeyValueStore.CreateDBSnapshot(dir); err != nil { + return err + } + if err := db.AncientStore.CreateDBSnapshot(dir); err != nil { + return err + } + return nil +} + // nofreezedb is a database wrapper that disables freezer data retrievals. type nofreezedb struct { ethdb.KeyValueStore diff --git a/core/rawdb/freezer.go b/core/rawdb/freezer.go index 4d78460920..35ab287214 100644 --- a/core/rawdb/freezer.go +++ b/core/rawdb/freezer.go @@ -19,6 +19,7 @@ package rawdb import ( "errors" "fmt" + "io" "math" "os" "path/filepath" @@ -73,6 +74,9 @@ type Freezer struct { tables map[string]*freezerTable // Data tables for storing everything instanceLock FileLock // File-system lock to prevent double opens closeOnce sync.Once + + datadir string + createSnapshotMutex sync.Mutex } // NewFreezer creates a freezer instance for maintaining immutable ordered @@ -115,6 +119,7 @@ func NewFreezer(datadir string, namespace string, readonly bool, maxTableSize ui readonly: readonly, tables: make(map[string]*freezerTable), instanceLock: lock, + datadir: datadir, } // Create the tables. @@ -316,6 +321,8 @@ func (f *Freezer) TruncateTail(tail uint64) (uint64, error) { // Sync flushes all data tables to disk. func (f *Freezer) Sync() error { + f.createSnapshotMutex.Lock() + defer f.createSnapshotMutex.Unlock() var errs []error for _, table := range f.tables { if err := table.Sync(); err != nil { @@ -328,6 +335,63 @@ func (f *Freezer) Sync() error { return nil } +func (f *Freezer) CreateDBSnapshot(dir string) error { + f.createSnapshotMutex.Lock() + defer f.createSnapshotMutex.Unlock() + snapshotDir := filepath.Join(dir, "l2chaindata", "ancient", "chain") // Format currently used + if err := os.MkdirAll(snapshotDir, os.ModePerm); err != nil { + return fmt.Errorf("failed to create snapshot of ancient directory: %w", err) + } + // Manually copy contents of ancient to the snapshotDir, createSnapshotMutex makes sure ancient is not updated while copying contents + err := filepath.Walk(f.datadir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("error accessing path %s: %w", path, err) + } + relPath, err := filepath.Rel(f.datadir, path) + if err != nil { + return fmt.Errorf("error calculating relative path: %w", err) + } + destPath := filepath.Join(snapshotDir, relPath) + if info.IsDir() { + if err := os.MkdirAll(destPath, info.Mode()); err != nil { + return fmt.Errorf("failed to create directory %s: %w", destPath, err) + } + } else { + if err := copyFile(path, destPath); err != nil { + return fmt.Errorf("failed to copy file %s: %w", path, err) + } + } + return nil + }) + return err +} + +func copyFile(src, dest string) error { + srcFile, err := os.Open(src) + if err != nil { + return fmt.Errorf("failed to open source file: %w", err) + } + defer srcFile.Close() + destFile, err := os.Create(dest) + if err != nil { + return fmt.Errorf("failed to create destination file: %w", err) + } + defer destFile.Close() + _, err = io.Copy(destFile, srcFile) + if err != nil { + return fmt.Errorf("failed to copy contents: %w", err) + } + srcInfo, err := os.Stat(src) + if err != nil { + return fmt.Errorf("failed to stat source file: %w", err) + } + err = os.Chmod(dest, srcInfo.Mode()) + if err != nil { + return fmt.Errorf("failed to set permissions: %w", err) + } + return nil +} + // validate checks that every table has the same boundary. // Used instead of `repair` in readonly mode. func (f *Freezer) validate() error { diff --git a/core/rawdb/freezer_memory.go b/core/rawdb/freezer_memory.go index 954b58e874..25bad1f388 100644 --- a/core/rawdb/freezer_memory.go +++ b/core/rawdb/freezer_memory.go @@ -230,6 +230,10 @@ func NewMemoryFreezer(readonly bool, tableName map[string]bool) *MemoryFreezer { } } +func (f *MemoryFreezer) CreateDBSnapshot(dir string) error { + return errors.New("createDBSnapshot method is not supported by MemoryFreezer") +} + // HasAncient returns an indicator whether the specified data exists. func (f *MemoryFreezer) HasAncient(kind string, number uint64) (bool, error) { f.lock.RLock() diff --git a/core/rawdb/freezer_resettable.go b/core/rawdb/freezer_resettable.go index 7fa59b8d21..17cd3397f1 100644 --- a/core/rawdb/freezer_resettable.go +++ b/core/rawdb/freezer_resettable.go @@ -17,6 +17,7 @@ package rawdb import ( + "errors" "os" "path/filepath" "sync" @@ -92,6 +93,10 @@ func (f *resettableFreezer) Reset() error { return nil } +func (f *resettableFreezer) CreateDBSnapshot(dir string) error { + return errors.New("createDBSnapshot method is not supported by resettableFreezer") +} + // Close terminates the chain freezer, unmapping all the data files. func (f *resettableFreezer) Close() error { f.lock.RLock() diff --git a/core/rawdb/table.go b/core/rawdb/table.go index 7d21e5e5eb..1235ef950b 100644 --- a/core/rawdb/table.go +++ b/core/rawdb/table.go @@ -17,6 +17,8 @@ package rawdb import ( + "errors" + "github.com/ethereum/go-ethereum/ethdb" ) @@ -35,6 +37,10 @@ func NewTable(db ethdb.Database, prefix string) ethdb.Database { } } +func (db *table) CreateDBSnapshot(dir string) error { + return errors.New("createDBSnapshot method is not supported") +} + // Close is a noop to implement the Database interface. func (t *table) Close() error { return nil diff --git a/ethdb/database.go b/ethdb/database.go index 7507358570..07fd704e25 100644 --- a/ethdb/database.go +++ b/ethdb/database.go @@ -180,6 +180,7 @@ type AncientStore interface { AncientReader AncientWriter io.Closer + CreateDBSnapshot(dir string) error } type WasmTarget string diff --git a/ethdb/leveldb/leveldb.go b/ethdb/leveldb/leveldb.go index a4270bfaf2..a819fb8a30 100644 --- a/ethdb/leveldb/leveldb.go +++ b/ethdb/leveldb/leveldb.go @@ -166,6 +166,10 @@ func configureOptions(customizeFn func(*opt.Options)) *opt.Options { return options } +func (db *Database) CreateDBSnapshot(dir string) error { + return errors.New("createDBSnapshot method is not supported by leveldb") +} + // Close stops the metrics collection, flushes any pending data to disk and closes // all io accesses to the underlying key-value store. func (db *Database) Close() error { diff --git a/ethdb/memorydb/memorydb.go b/ethdb/memorydb/memorydb.go index 86570feeec..e915f27643 100644 --- a/ethdb/memorydb/memorydb.go +++ b/ethdb/memorydb/memorydb.go @@ -65,6 +65,10 @@ func NewWithCap(size int) *Database { } } +func (db *Database) CreateDBSnapshot(dir string) error { + return errors.New("createDBSnapshot method is not supported by memorydb") +} + // Close deallocates the internal map and ensures any consecutive data access op // fails with an error. func (db *Database) Close() error { diff --git a/ethdb/pebble/pebble.go b/ethdb/pebble/pebble.go index ec343c88af..edc6677c1e 100644 --- a/ethdb/pebble/pebble.go +++ b/ethdb/pebble/pebble.go @@ -22,6 +22,7 @@ package pebble import ( "bytes" "fmt" + "path/filepath" "runtime" "sync" "sync/atomic" @@ -116,6 +117,11 @@ type Database struct { writeOptions *pebble.WriteOptions } +func (d *Database) CreateDBSnapshot(dir string) error { + snapshotDir := filepath.Join(dir, filepath.Base(d.Path())) + return d.db.Checkpoint(snapshotDir, pebble.WithFlushedWAL()) +} + func (d *Database) onCompactionBegin(info pebble.CompactionInfo) { if d.activeComp == 0 { d.compStartTime = time.Now() diff --git a/ethdb/remotedb/remotedb.go b/ethdb/remotedb/remotedb.go index 0168aefef8..a0a8b48254 100644 --- a/ethdb/remotedb/remotedb.go +++ b/ethdb/remotedb/remotedb.go @@ -22,6 +22,8 @@ package remotedb import ( + "errors" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/rpc" @@ -32,6 +34,10 @@ type Database struct { remote *rpc.Client } +func (t *Database) CreateDBSnapshot(dir string) error { + return errors.New("createDBSnapshot method is not supported by remotedb") +} + func (db *Database) Has(key []byte) (bool, error) { if _, err := db.Get(key); err != nil { return false, nil diff --git a/ethdb/snapshot.go b/ethdb/snapshot.go index 03b7794a77..407c71ab75 100644 --- a/ethdb/snapshot.go +++ b/ethdb/snapshot.go @@ -38,4 +38,7 @@ type Snapshotter interface { // Note don't forget to release the snapshot once it's used up, otherwise // the stale data will never be cleaned up by the underlying compactor. NewSnapshot() (Snapshot, error) + + // CreateDBSnapshot creates a copy of underlying database inside dir + CreateDBSnapshot(dir string) error } diff --git a/trie/trie_test.go b/trie/trie_test.go index da60a7423d..ce694ad8af 100644 --- a/trie/trie_test.go +++ b/trie/trie_test.go @@ -814,6 +814,9 @@ type spongeDb struct { values map[string]string } +func (s *spongeDb) CreateDBSnapshot(dir string) error { + return errors.New("createDBSnapshot method is not supported by spongeDb") +} func (s *spongeDb) Has(key []byte) (bool, error) { panic("implement me") } func (s *spongeDb) Get(key []byte) ([]byte, error) { return nil, errors.New("no such elem") } func (s *spongeDb) Delete(key []byte) error { panic("implement me") } From cf617ffba640edafd04ef8c6e734a2329b1d12e2 Mon Sep 17 00:00:00 2001 From: Ganesh Vanahalli Date: Tue, 3 Dec 2024 15:51:10 -0600 Subject: [PATCH 2/3] add CreateDBSnapshot to dbWithWasmEntry --- core/rawdb/database.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/rawdb/database.go b/core/rawdb/database.go index fe2a39e113..050551dc90 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -199,6 +199,16 @@ type dbWithWasmEntry struct { wasmTargets []ethdb.WasmTarget } +func (db *dbWithWasmEntry) CreateDBSnapshot(dir string) error { + if err := db.Database.CreateDBSnapshot(dir); err != nil { + return err + } + if err := db.wasmDb.CreateDBSnapshot(dir); err != nil { + return err + } + return nil +} + func (db *dbWithWasmEntry) WasmDataBase() (ethdb.KeyValueStore, uint32) { return db.wasmDb, db.wasmCacheTag } From e1a438d81bbf7576ececd62b15e3c1a7a6452cb4 Mon Sep 17 00:00:00 2001 From: Ganesh Vanahalli Date: Tue, 3 Dec 2024 16:28:40 -0600 Subject: [PATCH 3/3] add CreateDBSnapshot to fake_leveldb --- ethdb/leveldb/fake_leveldb.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ethdb/leveldb/fake_leveldb.go b/ethdb/leveldb/fake_leveldb.go index 28f19d1ddf..3641287759 100644 --- a/ethdb/leveldb/fake_leveldb.go +++ b/ethdb/leveldb/fake_leveldb.go @@ -59,6 +59,10 @@ func configureOptions(customizeFn func(*opt.Options)) *opt.Options { } */ +func (db *Database) CreateDBSnapshot(dir string) error { + return errors.New("createDBSnapshot method is not supported by leveldb") +} + // Close stops the metrics collection, flushes any pending data to disk and closes // all io accesses to the underlying key-value store. func (db *Database) Close() error {