mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-25 06:06:44 +00:00
cmd, core, eth, triedb/pathdb: improve journal file flushing
Signed-off-by: jsvisa <delweng@gmail.com>
This commit is contained in:
parent
c7d2d30f49
commit
22e1c629a0
11 changed files with 184 additions and 70 deletions
|
|
@ -79,6 +79,7 @@ var (
|
||||||
utils.BlobPoolDataDirFlag,
|
utils.BlobPoolDataDirFlag,
|
||||||
utils.BlobPoolDataCapFlag,
|
utils.BlobPoolDataCapFlag,
|
||||||
utils.BlobPoolPriceBumpFlag,
|
utils.BlobPoolPriceBumpFlag,
|
||||||
|
utils.TrieDBJournalFlag,
|
||||||
utils.SyncModeFlag,
|
utils.SyncModeFlag,
|
||||||
utils.SyncTargetFlag,
|
utils.SyncTargetFlag,
|
||||||
utils.ExitWhenSyncedFlag,
|
utils.ExitWhenSyncedFlag,
|
||||||
|
|
@ -97,7 +98,7 @@ var (
|
||||||
utils.CacheFlag,
|
utils.CacheFlag,
|
||||||
utils.CacheDatabaseFlag,
|
utils.CacheDatabaseFlag,
|
||||||
utils.CacheTrieFlag,
|
utils.CacheTrieFlag,
|
||||||
utils.CacheTrieJournalFlag,
|
utils.CacheTrieJournalFlag, // deprecated
|
||||||
utils.CacheTrieRejournalFlag, // deprecated
|
utils.CacheTrieRejournalFlag, // deprecated
|
||||||
utils.CacheGCFlag,
|
utils.CacheGCFlag,
|
||||||
utils.CacheSnapshotFlag,
|
utils.CacheSnapshotFlag,
|
||||||
|
|
|
||||||
|
|
@ -446,6 +446,15 @@ var (
|
||||||
Value: ethconfig.Defaults.BlobPool.PriceBump,
|
Value: ethconfig.Defaults.BlobPool.PriceBump,
|
||||||
Category: flags.BlobPoolCategory,
|
Category: flags.BlobPoolCategory,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trie database settings
|
||||||
|
TrieDBJournalFlag = &cli.StringFlag{
|
||||||
|
Name: "triedb.journal",
|
||||||
|
Usage: "Path to the journal used for persisting trie data across node restarts",
|
||||||
|
Value: ethconfig.Defaults.TrieDBJournal,
|
||||||
|
Category: flags.TrieDatabaseCategory,
|
||||||
|
}
|
||||||
|
|
||||||
// Performance tuning settings
|
// Performance tuning settings
|
||||||
CacheFlag = &cli.IntFlag{
|
CacheFlag = &cli.IntFlag{
|
||||||
Name: "cache",
|
Name: "cache",
|
||||||
|
|
@ -465,11 +474,6 @@ var (
|
||||||
Value: 15,
|
Value: 15,
|
||||||
Category: flags.PerfCategory,
|
Category: flags.PerfCategory,
|
||||||
}
|
}
|
||||||
CacheTrieJournalFlag = &cli.StringFlag{
|
|
||||||
Name: "cache.trie.journal",
|
|
||||||
Usage: "Disk journal directory for trie cache to survive node restarts (experimental, default is disabled)",
|
|
||||||
Category: flags.PerfCategory,
|
|
||||||
}
|
|
||||||
CacheGCFlag = &cli.IntFlag{
|
CacheGCFlag = &cli.IntFlag{
|
||||||
Name: "cache.gc",
|
Name: "cache.gc",
|
||||||
Usage: "Percentage of cache memory allowance to use for trie pruning (default = 25% full mode, 0% archive mode)",
|
Usage: "Percentage of cache memory allowance to use for trie pruning (default = 25% full mode, 0% archive mode)",
|
||||||
|
|
@ -988,6 +992,7 @@ var (
|
||||||
DataDirFlag,
|
DataDirFlag,
|
||||||
AncientFlag,
|
AncientFlag,
|
||||||
EraFlag,
|
EraFlag,
|
||||||
|
TrieDBJournalFlag,
|
||||||
RemoteDBFlag,
|
RemoteDBFlag,
|
||||||
DBEngineFlag,
|
DBEngineFlag,
|
||||||
StateSchemeFlag,
|
StateSchemeFlag,
|
||||||
|
|
@ -1685,8 +1690,8 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
|
||||||
if ctx.IsSet(CacheFlag.Name) || ctx.IsSet(CacheSnapshotFlag.Name) {
|
if ctx.IsSet(CacheFlag.Name) || ctx.IsSet(CacheSnapshotFlag.Name) {
|
||||||
cfg.SnapshotCache = ctx.Int(CacheFlag.Name) * ctx.Int(CacheSnapshotFlag.Name) / 100
|
cfg.SnapshotCache = ctx.Int(CacheFlag.Name) * ctx.Int(CacheSnapshotFlag.Name) / 100
|
||||||
}
|
}
|
||||||
if journal := ctx.String(CacheTrieJournalFlag.Name); journal != "" {
|
if ctx.IsSet(TrieDBJournalFlag.Name) {
|
||||||
cfg.TrieJournal = stack.ResolvePath(journal)
|
cfg.TrieDBJournal = stack.ResolvePath(ctx.String(TrieDBJournalFlag.Name))
|
||||||
}
|
}
|
||||||
if ctx.IsSet(CacheLogSizeFlag.Name) {
|
if ctx.IsSet(CacheLogSizeFlag.Name) {
|
||||||
cfg.FilterLogCacheSize = ctx.Int(CacheLogSizeFlag.Name)
|
cfg.FilterLogCacheSize = ctx.Int(CacheLogSizeFlag.Name)
|
||||||
|
|
@ -2211,8 +2216,8 @@ func MakeChain(ctx *cli.Context, stack *node.Node, readonly bool) (*core.BlockCh
|
||||||
options.Preimages = true
|
options.Preimages = true
|
||||||
log.Info("Enabling recording of key preimages since archive mode is used")
|
log.Info("Enabling recording of key preimages since archive mode is used")
|
||||||
}
|
}
|
||||||
if journal := ctx.String(CacheTrieJournalFlag.Name); journal != "" {
|
if ctx.IsSet(TrieDBJournalFlag.Name) {
|
||||||
options.TrieJournal = stack.ResolvePath(journal)
|
options.TrieDBJournal = stack.ResolvePath(ctx.String(TrieDBJournalFlag.Name))
|
||||||
}
|
}
|
||||||
if !ctx.Bool(SnapshotFlag.Name) {
|
if !ctx.Bool(SnapshotFlag.Name) {
|
||||||
options.SnapshotLimit = 0 // Disabled
|
options.SnapshotLimit = 0 // Disabled
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ var ShowDeprecated = &cli.Command{
|
||||||
var DeprecatedFlags = []cli.Flag{
|
var DeprecatedFlags = []cli.Flag{
|
||||||
NoUSBFlag,
|
NoUSBFlag,
|
||||||
LegacyWhitelistFlag,
|
LegacyWhitelistFlag,
|
||||||
|
CacheTrieJournalFlag,
|
||||||
CacheTrieRejournalFlag,
|
CacheTrieRejournalFlag,
|
||||||
LegacyDiscoveryV5Flag,
|
LegacyDiscoveryV5Flag,
|
||||||
TxLookupLimitFlag,
|
TxLookupLimitFlag,
|
||||||
|
|
@ -59,6 +60,11 @@ var (
|
||||||
Category: flags.DeprecatedCategory,
|
Category: flags.DeprecatedCategory,
|
||||||
}
|
}
|
||||||
// Deprecated July 2023
|
// Deprecated July 2023
|
||||||
|
CacheTrieJournalFlag = &cli.StringFlag{
|
||||||
|
Name: "cache.trie.journal",
|
||||||
|
Usage: "Disk journal directory for trie cache to survive node restarts",
|
||||||
|
Category: flags.DeprecatedCategory,
|
||||||
|
}
|
||||||
CacheTrieRejournalFlag = &cli.DurationFlag{
|
CacheTrieRejournalFlag = &cli.DurationFlag{
|
||||||
Name: "cache.trie.rejournal",
|
Name: "cache.trie.rejournal",
|
||||||
Usage: "Time interval to regenerate the trie cache journal",
|
Usage: "Time interval to regenerate the trie cache journal",
|
||||||
|
|
|
||||||
|
|
@ -166,7 +166,7 @@ type BlockChainConfig struct {
|
||||||
TrieDirtyLimit int // Memory limit (MB) at which to start flushing dirty trie nodes to disk
|
TrieDirtyLimit int // Memory limit (MB) at which to start flushing dirty trie nodes to disk
|
||||||
TrieTimeLimit time.Duration // Time limit after which to flush the current in-memory trie to disk
|
TrieTimeLimit time.Duration // Time limit after which to flush the current in-memory trie to disk
|
||||||
TrieNoAsyncFlush bool // Whether the asynchronous buffer flushing is disallowed
|
TrieNoAsyncFlush bool // Whether the asynchronous buffer flushing is disallowed
|
||||||
TrieJournal string // Path used to store the trie pathdb journal
|
TrieDBJournal string // Path to the journal used for persisting trie data across node restarts
|
||||||
|
|
||||||
Preimages bool // Whether to store preimage of trie key to the disk
|
Preimages bool // Whether to store preimage of trie key to the disk
|
||||||
StateHistory uint64 // Number of blocks from head whose state histories are reserved.
|
StateHistory uint64 // Number of blocks from head whose state histories are reserved.
|
||||||
|
|
@ -247,6 +247,7 @@ func (cfg *BlockChainConfig) triedbConfig(isVerkle bool) *triedb.Config {
|
||||||
EnableStateIndexing: cfg.ArchiveMode,
|
EnableStateIndexing: cfg.ArchiveMode,
|
||||||
TrieCleanSize: cfg.TrieCleanLimit * 1024 * 1024,
|
TrieCleanSize: cfg.TrieCleanLimit * 1024 * 1024,
|
||||||
StateCleanSize: cfg.SnapshotLimit * 1024 * 1024,
|
StateCleanSize: cfg.SnapshotLimit * 1024 * 1024,
|
||||||
|
JournalPath: cfg.TrieDBJournal,
|
||||||
|
|
||||||
// TODO(rjl493456442): The write buffer represents the memory limit used
|
// TODO(rjl493456442): The write buffer represents the memory limit used
|
||||||
// for flushing both trie data and state data to disk. The config name
|
// for flushing both trie data and state data to disk. The config name
|
||||||
|
|
|
||||||
|
|
@ -227,7 +227,6 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
|
||||||
TrieDirtyLimit: config.TrieDirtyCache,
|
TrieDirtyLimit: config.TrieDirtyCache,
|
||||||
ArchiveMode: config.NoPruning,
|
ArchiveMode: config.NoPruning,
|
||||||
TrieTimeLimit: config.TrieTimeout,
|
TrieTimeLimit: config.TrieTimeout,
|
||||||
TrieJournal: config.TrieJournal,
|
|
||||||
SnapshotLimit: config.SnapshotCache,
|
SnapshotLimit: config.SnapshotCache,
|
||||||
Preimages: config.Preimages,
|
Preimages: config.Preimages,
|
||||||
StateHistory: config.StateHistory,
|
StateHistory: config.StateHistory,
|
||||||
|
|
@ -239,6 +238,11 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if config.TrieDBJournal != "" {
|
||||||
|
options.TrieDBJournal = stack.ResolvePath(config.TrieDBJournal)
|
||||||
|
} else {
|
||||||
|
log.Warn("Trie database journal is persisted within the database")
|
||||||
|
}
|
||||||
|
|
||||||
if config.VMTrace != "" {
|
if config.VMTrace != "" {
|
||||||
traceConfig := json.RawMessage("{}")
|
traceConfig := json.RawMessage("{}")
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ var Defaults = Config{
|
||||||
TrieCleanCache: 154,
|
TrieCleanCache: 154,
|
||||||
TrieDirtyCache: 256,
|
TrieDirtyCache: 256,
|
||||||
TrieTimeout: 60 * time.Minute,
|
TrieTimeout: 60 * time.Minute,
|
||||||
|
TrieDBJournal: "triedb.journal",
|
||||||
SnapshotCache: 102,
|
SnapshotCache: 102,
|
||||||
FilterLogCacheSize: 32,
|
FilterLogCacheSize: 32,
|
||||||
Miner: miner.DefaultConfig,
|
Miner: miner.DefaultConfig,
|
||||||
|
|
@ -103,12 +104,6 @@ type Config struct {
|
||||||
LogHistory uint64 `toml:",omitempty"` // The maximum number of blocks from head where a log search index is maintained.
|
LogHistory uint64 `toml:",omitempty"` // The maximum number of blocks from head where a log search index is maintained.
|
||||||
LogNoHistory bool `toml:",omitempty"` // No log search index is maintained.
|
LogNoHistory bool `toml:",omitempty"` // No log search index is maintained.
|
||||||
LogExportCheckpoints string // export log index checkpoints to file
|
LogExportCheckpoints string // export log index checkpoints to file
|
||||||
StateHistory uint64 `toml:",omitempty"` // The maximum number of blocks from head whose state histories are reserved.
|
|
||||||
|
|
||||||
// State scheme represents the scheme used to store ethereum states and trie
|
|
||||||
// nodes on top. It can be 'hash', 'path', or none which means use the scheme
|
|
||||||
// consistent with persistent state.
|
|
||||||
StateScheme string `toml:",omitempty"`
|
|
||||||
|
|
||||||
// RequiredBlocks is a set of block number -> hash mappings which must be in the
|
// RequiredBlocks is a set of block number -> hash mappings which must be in the
|
||||||
// canonical chain of all remote peers. Setting the option makes geth verify the
|
// canonical chain of all remote peers. Setting the option makes geth verify the
|
||||||
|
|
@ -122,12 +117,20 @@ type Config struct {
|
||||||
DatabaseFreezer string
|
DatabaseFreezer string
|
||||||
DatabaseEra string
|
DatabaseEra string
|
||||||
|
|
||||||
|
// Trie database options. TODO(rjl493456442) move all trie database options
|
||||||
|
// into a separate config structure.
|
||||||
TrieCleanCache int
|
TrieCleanCache int
|
||||||
TrieDirtyCache int
|
TrieDirtyCache int
|
||||||
TrieTimeout time.Duration
|
TrieTimeout time.Duration
|
||||||
SnapshotCache int
|
SnapshotCache int
|
||||||
Preimages bool
|
Preimages bool
|
||||||
TrieJournal string
|
StateHistory uint64 `toml:",omitempty"` // The maximum number of blocks from head whose state histories are reserved.
|
||||||
|
TrieDBJournal string // Path to the journal used for persisting trie data across node restarts
|
||||||
|
|
||||||
|
// State scheme represents the scheme used to store ethereum states and trie
|
||||||
|
// nodes on top. It can be 'hash', 'path', or none which means use the scheme
|
||||||
|
// consistent with persistent state.
|
||||||
|
StateScheme string `toml:",omitempty"`
|
||||||
|
|
||||||
// This is the number of blocks for which logs will be cached in the filter system.
|
// This is the number of blocks for which logs will be cached in the filter system.
|
||||||
FilterLogCacheSize int
|
FilterLogCacheSize int
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ const (
|
||||||
APICategory = "API AND CONSOLE"
|
APICategory = "API AND CONSOLE"
|
||||||
NetworkingCategory = "NETWORKING"
|
NetworkingCategory = "NETWORKING"
|
||||||
MinerCategory = "MINER"
|
MinerCategory = "MINER"
|
||||||
|
TrieDatabaseCategory = "TRIE DATABASE"
|
||||||
GasPriceCategory = "GAS PRICE ORACLE"
|
GasPriceCategory = "GAS PRICE ORACLE"
|
||||||
VMCategory = "VIRTUAL MACHINE"
|
VMCategory = "VIRTUAL MACHINE"
|
||||||
LoggingCategory = "LOGGING AND DEBUGGING"
|
LoggingCategory = "LOGGING AND DEBUGGING"
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,7 @@ type Config struct {
|
||||||
StateCleanSize int // Maximum memory allowance (in bytes) for caching clean state data
|
StateCleanSize int // Maximum memory allowance (in bytes) for caching clean state data
|
||||||
WriteBufferSize int // Maximum memory allowance (in bytes) for write buffer
|
WriteBufferSize int // Maximum memory allowance (in bytes) for write buffer
|
||||||
ReadOnly bool // Flag whether the database is opened in read only mode
|
ReadOnly bool // Flag whether the database is opened in read only mode
|
||||||
|
JournalPath string // Absolute path of journal file (null means the journal data is persisted in key-value store)
|
||||||
|
|
||||||
// Testing configurations
|
// Testing configurations
|
||||||
SnapshotNoBuild bool // Flag Whether the state generation is allowed
|
SnapshotNoBuild bool // Flag Whether the state generation is allowed
|
||||||
|
|
@ -156,6 +157,9 @@ func (c *Config) fields() []interface{} {
|
||||||
} else {
|
} else {
|
||||||
list = append(list, "history", fmt.Sprintf("last %d blocks", c.StateHistory))
|
list = append(list, "history", fmt.Sprintf("last %d blocks", c.StateHistory))
|
||||||
}
|
}
|
||||||
|
if c.JournalPath != "" {
|
||||||
|
list = append(list, "journal", c.JournalPath)
|
||||||
|
}
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,15 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/ethereum/go-ethereum/ethdb"
|
||||||
"github.com/ethereum/go-ethereum/internal/testrand"
|
"github.com/ethereum/go-ethereum/internal/testrand"
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
"github.com/ethereum/go-ethereum/rlp"
|
||||||
"github.com/ethereum/go-ethereum/trie"
|
"github.com/ethereum/go-ethereum/trie"
|
||||||
|
|
@ -121,7 +124,7 @@ type tester struct {
|
||||||
snapStorages map[common.Hash]map[common.Hash]map[common.Hash][]byte // Keyed by the hash of account address and the hash of storage key
|
snapStorages map[common.Hash]map[common.Hash]map[common.Hash][]byte // Keyed by the hash of account address and the hash of storage key
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTester(t *testing.T, historyLimit uint64, isVerkle bool, layers int, enableIndex bool) *tester {
|
func newTester(t *testing.T, historyLimit uint64, isVerkle bool, layers int, enableIndex bool, journal string) *tester {
|
||||||
var (
|
var (
|
||||||
disk, _ = rawdb.Open(rawdb.NewMemoryDatabase(), rawdb.OpenOptions{Ancient: t.TempDir()})
|
disk, _ = rawdb.Open(rawdb.NewMemoryDatabase(), rawdb.OpenOptions{Ancient: t.TempDir()})
|
||||||
db = New(disk, &Config{
|
db = New(disk, &Config{
|
||||||
|
|
@ -131,6 +134,7 @@ func newTester(t *testing.T, historyLimit uint64, isVerkle bool, layers int, ena
|
||||||
StateCleanSize: 256 * 1024,
|
StateCleanSize: 256 * 1024,
|
||||||
WriteBufferSize: 256 * 1024,
|
WriteBufferSize: 256 * 1024,
|
||||||
NoAsyncFlush: true,
|
NoAsyncFlush: true,
|
||||||
|
JournalPath: journal,
|
||||||
}, isVerkle)
|
}, isVerkle)
|
||||||
|
|
||||||
obj = &tester{
|
obj = &tester{
|
||||||
|
|
@ -466,7 +470,7 @@ func TestDatabaseRollback(t *testing.T) {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Verify state histories
|
// Verify state histories
|
||||||
tester := newTester(t, 0, false, 32, false)
|
tester := newTester(t, 0, false, 32, false, "")
|
||||||
defer tester.release()
|
defer tester.release()
|
||||||
|
|
||||||
if err := tester.verifyHistory(); err != nil {
|
if err := tester.verifyHistory(); err != nil {
|
||||||
|
|
@ -500,7 +504,7 @@ func TestDatabaseRecoverable(t *testing.T) {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
tester = newTester(t, 0, false, 12, false)
|
tester = newTester(t, 0, false, 12, false, "")
|
||||||
index = tester.bottomIndex()
|
index = tester.bottomIndex()
|
||||||
)
|
)
|
||||||
defer tester.release()
|
defer tester.release()
|
||||||
|
|
@ -544,7 +548,7 @@ func TestDisable(t *testing.T) {
|
||||||
maxDiffLayers = 128
|
maxDiffLayers = 128
|
||||||
}()
|
}()
|
||||||
|
|
||||||
tester := newTester(t, 0, false, 32, false)
|
tester := newTester(t, 0, false, 32, false, "")
|
||||||
defer tester.release()
|
defer tester.release()
|
||||||
|
|
||||||
stored := crypto.Keccak256Hash(rawdb.ReadAccountTrieNode(tester.db.diskdb, nil))
|
stored := crypto.Keccak256Hash(rawdb.ReadAccountTrieNode(tester.db.diskdb, nil))
|
||||||
|
|
@ -586,7 +590,7 @@ func TestCommit(t *testing.T) {
|
||||||
maxDiffLayers = 128
|
maxDiffLayers = 128
|
||||||
}()
|
}()
|
||||||
|
|
||||||
tester := newTester(t, 0, false, 12, false)
|
tester := newTester(t, 0, false, 12, false, "")
|
||||||
defer tester.release()
|
defer tester.release()
|
||||||
|
|
||||||
if err := tester.db.Commit(tester.lastHash(), false); err != nil {
|
if err := tester.db.Commit(tester.lastHash(), false); err != nil {
|
||||||
|
|
@ -610,13 +614,18 @@ func TestCommit(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestJournal(t *testing.T) {
|
func TestJournal(t *testing.T) {
|
||||||
|
testJournal(t, "")
|
||||||
|
testJournal(t, t.TempDir())
|
||||||
|
}
|
||||||
|
|
||||||
|
func testJournal(t *testing.T, journal string) {
|
||||||
// Redefine the diff layer depth allowance for faster testing.
|
// Redefine the diff layer depth allowance for faster testing.
|
||||||
maxDiffLayers = 4
|
maxDiffLayers = 4
|
||||||
defer func() {
|
defer func() {
|
||||||
maxDiffLayers = 128
|
maxDiffLayers = 128
|
||||||
}()
|
}()
|
||||||
|
|
||||||
tester := newTester(t, 0, false, 12, false)
|
tester := newTester(t, 0, false, 12, false, journal)
|
||||||
defer tester.release()
|
defer tester.release()
|
||||||
|
|
||||||
if err := tester.db.Journal(tester.lastHash()); err != nil {
|
if err := tester.db.Journal(tester.lastHash()); err != nil {
|
||||||
|
|
@ -640,13 +649,30 @@ func TestJournal(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCorruptedJournal(t *testing.T) {
|
func TestCorruptedJournal(t *testing.T) {
|
||||||
|
testCorruptedJournal(t, "", func(db ethdb.Database) {
|
||||||
|
// Mutate the journal in disk, it should be regarded as invalid
|
||||||
|
blob := rawdb.ReadTrieJournal(db)
|
||||||
|
blob[0] = 0xa
|
||||||
|
rawdb.WriteTrieJournal(db, blob)
|
||||||
|
})
|
||||||
|
|
||||||
|
path := filepath.Join(t.TempDir(), "journal")
|
||||||
|
testCorruptedJournal(t, path, func(_ ethdb.Database) {
|
||||||
|
f, _ := os.OpenFile(path, os.O_WRONLY, 0644)
|
||||||
|
f.WriteAt([]byte{0xa}, 0)
|
||||||
|
f.Sync()
|
||||||
|
f.Close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCorruptedJournal(t *testing.T, journal string, modifyFn func(database ethdb.Database)) {
|
||||||
// Redefine the diff layer depth allowance for faster testing.
|
// Redefine the diff layer depth allowance for faster testing.
|
||||||
maxDiffLayers = 4
|
maxDiffLayers = 4
|
||||||
defer func() {
|
defer func() {
|
||||||
maxDiffLayers = 128
|
maxDiffLayers = 128
|
||||||
}()
|
}()
|
||||||
|
|
||||||
tester := newTester(t, 0, false, 12, false)
|
tester := newTester(t, 0, false, 12, false, journal)
|
||||||
defer tester.release()
|
defer tester.release()
|
||||||
|
|
||||||
if err := tester.db.Journal(tester.lastHash()); err != nil {
|
if err := tester.db.Journal(tester.lastHash()); err != nil {
|
||||||
|
|
@ -655,13 +681,10 @@ func TestCorruptedJournal(t *testing.T) {
|
||||||
tester.db.Close()
|
tester.db.Close()
|
||||||
root := crypto.Keccak256Hash(rawdb.ReadAccountTrieNode(tester.db.diskdb, nil))
|
root := crypto.Keccak256Hash(rawdb.ReadAccountTrieNode(tester.db.diskdb, nil))
|
||||||
|
|
||||||
// Mutate the journal in disk, it should be regarded as invalid
|
modifyFn(tester.db.diskdb)
|
||||||
blob := rawdb.ReadTrieJournal(tester.db.diskdb)
|
|
||||||
blob[0] = 0xa
|
|
||||||
rawdb.WriteTrieJournal(tester.db.diskdb, blob)
|
|
||||||
|
|
||||||
// Verify states, all not-yet-written states should be discarded
|
// Verify states, all not-yet-written states should be discarded
|
||||||
tester.db = New(tester.db.diskdb, nil, false)
|
tester.db = New(tester.db.diskdb, tester.db.config, false)
|
||||||
for i := 0; i < len(tester.roots); i++ {
|
for i := 0; i < len(tester.roots); i++ {
|
||||||
if tester.roots[i] == root {
|
if tester.roots[i] == root {
|
||||||
if err := tester.verifyState(root); err != nil {
|
if err := tester.verifyState(root); err != nil {
|
||||||
|
|
@ -694,7 +717,7 @@ func TestTailTruncateHistory(t *testing.T) {
|
||||||
maxDiffLayers = 128
|
maxDiffLayers = 128
|
||||||
}()
|
}()
|
||||||
|
|
||||||
tester := newTester(t, 10, false, 12, false)
|
tester := newTester(t, 10, false, 12, false, "")
|
||||||
defer tester.release()
|
defer tester.release()
|
||||||
|
|
||||||
tester.db.Close()
|
tester.db.Close()
|
||||||
|
|
|
||||||
54
triedb/pathdb/fileutils.go
Normal file
54
triedb/pathdb/fileutils.go
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
// Copyright 2025 The go-ethereum Authors
|
||||||
|
// This file is part of the go-ethereum library.
|
||||||
|
//
|
||||||
|
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Lesser General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Lesser General Public License
|
||||||
|
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package pathdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isErrInvalid(err error) bool {
|
||||||
|
if errors.Is(err, os.ErrInvalid) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Go >= 1.8 returns *os.PathError instead
|
||||||
|
if patherr, ok := err.(*os.PathError); ok && patherr.Err == syscall.EINVAL {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncDir(name string) error {
|
||||||
|
// As per fsync manpage, Linux seems to expect fsync on directory, however
|
||||||
|
// some system don't support this, so we will ignore syscall.EINVAL.
|
||||||
|
//
|
||||||
|
// From fsync(2):
|
||||||
|
// Calling fsync() does not necessarily ensure that the entry in the
|
||||||
|
// directory containing the file has also reached disk. For that an
|
||||||
|
// explicit fsync() on a file descriptor for the directory is also needed.
|
||||||
|
f, err := os.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if err := f.Sync(); err != nil && !isErrInvalid(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
|
@ -53,16 +54,17 @@ const journalVersion uint64 = 3
|
||||||
// loadJournal tries to parse the layer journal from the disk.
|
// loadJournal tries to parse the layer journal from the disk.
|
||||||
func (db *Database) loadJournal(diskRoot common.Hash) (layer, error) {
|
func (db *Database) loadJournal(diskRoot common.Hash) (layer, error) {
|
||||||
var reader io.Reader
|
var reader io.Reader
|
||||||
if db.config.Journal != "" && common.FileExist(db.config.Journal) {
|
if db.config.JournalPath != "" && common.FileExist(db.config.JournalPath) {
|
||||||
log.Info("Load pathdb disklayer journal", "path", db.config.Journal)
|
|
||||||
// If a journal file is specified, read it from there
|
// If a journal file is specified, read it from there
|
||||||
f, err := os.OpenFile(db.config.Journal, os.O_RDONLY, 0644)
|
log.Info("Load database journal from file", "path", db.config.JournalPath)
|
||||||
|
f, err := os.OpenFile(db.config.JournalPath, os.O_RDONLY, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read journal file %s: %w", db.config.Journal, err)
|
return nil, fmt.Errorf("failed to read journal file %s: %w", db.config.JournalPath, err)
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
reader = f
|
reader = f
|
||||||
} else {
|
} else {
|
||||||
|
log.Info("Load database journal from database")
|
||||||
journal := rawdb.ReadTrieJournal(db.diskdb)
|
journal := rawdb.ReadTrieJournal(db.diskdb)
|
||||||
if len(journal) == 0 {
|
if len(journal) == 0 {
|
||||||
return nil, errMissJournal
|
return nil, errMissJournal
|
||||||
|
|
@ -331,21 +333,24 @@ func (db *Database) Journal(root common.Hash) error {
|
||||||
return errDatabaseReadOnly
|
return errDatabaseReadOnly
|
||||||
}
|
}
|
||||||
|
|
||||||
var journal io.Writer
|
|
||||||
var file *os.File
|
|
||||||
// Store the journal into the database and return
|
// Store the journal into the database and return
|
||||||
if db.config.Journal != "" {
|
var (
|
||||||
|
file *os.File
|
||||||
|
journal io.Writer
|
||||||
|
tmpName = db.config.JournalPath + ".tmp"
|
||||||
|
)
|
||||||
|
if db.config.JournalPath != "" {
|
||||||
// Write into a temp file first
|
// Write into a temp file first
|
||||||
var err error
|
var err error
|
||||||
file, err = os.OpenFile(db.config.Journal+".new", os.O_WRONLY|os.O_CREATE, 0644)
|
file, err = os.OpenFile(tmpName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to open journal file %s: %w", db.config.Journal, err)
|
return fmt.Errorf("failed to open journal file %s: %w", db.config.JournalPath, err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if file != nil {
|
if file != nil {
|
||||||
file.Close()
|
file.Close()
|
||||||
// Clean up temp file if we didn't successfully rename it
|
os.Remove(tmpName) // Clean up temp file if we didn't successfully rename it
|
||||||
os.Remove(db.config.Journal + ".new")
|
log.Warn("Removed leftover temporary journal file", "path", tmpName)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
journal = file
|
journal = file
|
||||||
|
|
@ -373,21 +378,28 @@ func (db *Database) Journal(root common.Hash) error {
|
||||||
|
|
||||||
// Store the journal into the database and return
|
// Store the journal into the database and return
|
||||||
var size int
|
var size int
|
||||||
if db.config.Journal == "" {
|
if db.config.JournalPath == "" {
|
||||||
data := journal.(*bytes.Buffer)
|
data := journal.(*bytes.Buffer)
|
||||||
size = data.Len()
|
size = data.Len()
|
||||||
rawdb.WriteTrieJournal(db.diskdb, data.Bytes())
|
rawdb.WriteTrieJournal(db.diskdb, data.Bytes())
|
||||||
} else {
|
} else {
|
||||||
// Close the temporary file and atomically rename it
|
|
||||||
if err := file.Close(); err != nil {
|
|
||||||
return fmt.Errorf("failed to close temporary journal file: %w", err)
|
|
||||||
}
|
|
||||||
// Replace the live journal with the newly generated one
|
|
||||||
if err := os.Rename(db.config.Journal+".new", db.config.Journal); err != nil {
|
|
||||||
return fmt.Errorf("failed to rename temporary journal file: %w", err)
|
|
||||||
}
|
|
||||||
stat, _ := journal.(*os.File).Stat()
|
stat, _ := journal.(*os.File).Stat()
|
||||||
size = int(stat.Size())
|
size = int(stat.Size())
|
||||||
|
|
||||||
|
// Close the temporary file and atomically rename it
|
||||||
|
if err := file.Sync(); err != nil {
|
||||||
|
return fmt.Errorf("failed to fsync the journal, %v", err)
|
||||||
|
}
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
return fmt.Errorf("failed to close the journal: %v", err)
|
||||||
|
}
|
||||||
|
// Replace the live journal with the newly generated one
|
||||||
|
if err := os.Rename(tmpName, db.config.JournalPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to rename the journal: %v", err)
|
||||||
|
}
|
||||||
|
if err := syncDir(filepath.Dir(db.config.JournalPath)); err != nil {
|
||||||
|
return fmt.Errorf("failed to fsync the dir: %v", err)
|
||||||
|
}
|
||||||
file = nil
|
file = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue