From e1f26269324de2f0af8c7f74d1e472ace8a2e8a0 Mon Sep 17 00:00:00 2001 From: Herbert Jordan Date: Fri, 28 Jun 2024 09:57:59 +0200 Subject: [PATCH 1/6] Add benchmark for dirty-node scan --- go/database/mpt/node_flusher_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/go/database/mpt/node_flusher_test.go b/go/database/mpt/node_flusher_test.go index 2e3079ec4..eb2c9a87e 100644 --- a/go/database/mpt/node_flusher_test.go +++ b/go/database/mpt/node_flusher_test.go @@ -362,3 +362,22 @@ func checkThatNodeIsNotLocked(t *testing.T, node *shared.Shared[Node]) { } handle.Release() } + +func BenchmarkNodeFlusher_CollectDirtyNodes(b *testing.B) { + sizes := []int{1_000, 10_000, 100_000, 1_000_000, 10_000_000} + for _, size := range sizes { + b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) { + // Fill a cache with clean nodes. + cache := NewNodeCache(size) + for i := 0; i < size; i++ { + node := shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: true}}) + ref := NewNodeReference(ValueId(uint64(i))) + cache.GetOrSet(&ref, node) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + tryFlushDirtyNodes(cache, nil) + } + }) + } +} From 2161e403d02f68ba22d633a6affe2146067f7460 Mon Sep 17 00:00:00 2001 From: Herbert Jordan Date: Fri, 28 Jun 2024 09:58:50 +0200 Subject: [PATCH 2/6] Add reuse pool for dirty node candidate lists --- go/database/mpt/node_flusher.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/go/database/mpt/node_flusher.go b/go/database/mpt/node_flusher.go index d8e17f9da..e1f91aed8 100644 --- a/go/database/mpt/node_flusher.go +++ b/go/database/mpt/node_flusher.go @@ -13,6 +13,7 @@ package mpt import ( "errors" "slices" + "sync" "time" "github.com/Fantom-foundation/Carmen/go/database/mpt/shared" @@ -72,9 +73,19 @@ func (f *nodeFlusher) Stop() error { return errors.Join(f.errs...) } +var dirtyIdListPool = sync.Pool{ + New: func() interface{} { + list := make([]NodeId, 0, 1_000_000) + return &list + }, +} + func tryFlushDirtyNodes(cache NodeCache, sink NodeSink) error { // Collect a list of dirty nodes to be flushed. - dirtyIds := make([]NodeId, 0, 1_000_000) + dirtyIdsPtr := dirtyIdListPool.Get().(*[]NodeId) + defer dirtyIdListPool.Put(dirtyIdsPtr) + dirtyIds := *(dirtyIdsPtr) + dirtyIds = dirtyIds[:0] cache.ForEach(func(id NodeId, node *shared.Shared[Node]) { handle, success := node.TryGetViewHandle() if !success { From 47b3c7518507652f90811d50395a4225a8e5fd89 Mon Sep 17 00:00:00 2001 From: Herbert Jordan Date: Sun, 23 Jun 2024 10:48:01 +0200 Subject: [PATCH 3/6] Use atomic flag to speed up dirty node check --- go/database/mpt/forest.go | 9 +++++---- go/database/mpt/nodes.go | 19 ++++++++++++------- go/database/mpt/nodes_test.go | 18 ++++++++++-------- go/database/mpt/shared/shared.go | 7 +++++++ go/database/mpt/shared/shared_test.go | 8 ++++++++ go/database/mpt/state_test.go | 8 ++++---- 6 files changed, 46 insertions(+), 23 deletions(-) diff --git a/go/database/mpt/forest.go b/go/database/mpt/forest.go index a052283ad..bf3142084 100644 --- a/go/database/mpt/forest.go +++ b/go/database/mpt/forest.go @@ -523,10 +523,7 @@ func (s *Forest) Flush() error { // Get snapshot of set of dirty Node IDs. ids := make([]NodeId, 0, 1<<16) s.nodeCache.ForEach(func(id NodeId, node *shared.Shared[Node]) { - handle := node.GetViewHandle() - dirty := handle.Get().IsDirty() - handle.Release() - if dirty { + if node.GetUnprotected().IsDirty() { ids = append(ids, id) } }) @@ -553,6 +550,10 @@ func (s *Forest) flushDirtyIds(ids []NodeId) error { if present { handle := node.GetWriteHandle() node := handle.Get() + if !node.IsDirty() { + handle.Release() + continue + } err := s.flushNode(id, node) if err == nil { node.MarkClean() diff --git a/go/database/mpt/nodes.go b/go/database/mpt/nodes.go index 95ac7020d..47582b487 100644 --- a/go/database/mpt/nodes.go +++ b/go/database/mpt/nodes.go @@ -16,11 +16,13 @@ import ( "encoding/binary" "errors" "fmt" + "io" + "slices" + "sync/atomic" + "github.com/Fantom-foundation/Carmen/go/common" "github.com/Fantom-foundation/Carmen/go/common/tribool" "github.com/Fantom-foundation/Carmen/go/database/mpt/shared" - "io" - "slices" ) // This file defines the interface and implementation of all node types in a @@ -473,7 +475,7 @@ func (c *nodeCheckContext) isCompatible(other *nodeCheckContext) bool { type nodeBase struct { hash common.Hash // the hash of this node (may be dirty) hashStatus hashStatus // indicating whether this node's hash is valid - clean bool // by default nodes are dirty (clean == false) + clean atomic.Bool // by default nodes are dirty (clean == false) frozen bool // a flag marking the node as immutable (default: mutable) } @@ -525,22 +527,25 @@ func (n *nodeBase) markMutable() { n.frozen = false } +// IsDirty checks whether the in-memory version is in sync with the on-disk version. +// This function is thread safe and does not need any specific shared node access +// permissions to be accessed safely. func (n *nodeBase) IsDirty() bool { - return !n.clean + return !n.clean.Load() } func (n *nodeBase) MarkClean() { - n.clean = true + n.clean.Store(true) } func (n *nodeBase) markDirty() { - n.clean = false + n.clean.Store(false) n.hashStatus = hashStatusDirty } func (n *nodeBase) Release() { // The node is disconnected from the disk version and thus clean. - n.clean = true + n.clean.Store(true) n.hashStatus = hashStatusClean } diff --git a/go/database/mpt/nodes_test.go b/go/database/mpt/nodes_test.go index 4f1ffe318..1b71dcb6c 100644 --- a/go/database/mpt/nodes_test.go +++ b/go/database/mpt/nodes_test.go @@ -7806,9 +7806,8 @@ func (a *Account) Build(ctx *nodeContext) (NodeReference, *shared.Shared[Node]) if a.hashStatus != nil { hashStatus = *a.hashStatus } - return NewNodeReference(AccountId(ctx.nextIndex())), shared.MakeShared[Node](&AccountNode{ + res := &AccountNode{ nodeBase: nodeBase{ - clean: !a.dirty, frozen: a.frozen, hashStatus: hashStatus, }, @@ -7818,7 +7817,9 @@ func (a *Account) Build(ctx *nodeContext) (NodeReference, *shared.Shared[Node]) storage: storage, storageHashDirty: a.storageHashDirty, storageHash: storageHash, - }) + } + res.nodeBase.clean.Store(!a.dirty) + return NewNodeReference(AccountId(ctx.nextIndex())), shared.MakeShared[Node](res) } type Children map[Nibble]NodeDesc @@ -7839,7 +7840,7 @@ type Branch struct { func (b *Branch) Build(ctx *nodeContext) (NodeReference, *shared.Shared[Node]) { ref := NewNodeReference(BranchId(ctx.nextIndex())) res := &BranchNode{} - res.nodeBase.clean = !b.dirty + res.nodeBase.clean.Store(!b.dirty) res.frozen = b.frozen for i, desc := range b.children { ref, _ := ctx.Build(desc) @@ -7883,7 +7884,7 @@ type Extension struct { func (e *Extension) Build(ctx *nodeContext) (NodeReference, *shared.Shared[Node]) { ref := NewNodeReference(ExtensionId(ctx.nextIndex())) res := &ExtensionNode{} - res.nodeBase.clean = !e.dirty + res.nodeBase.clean.Store(!e.dirty) res.frozen = e.frozen res.path = CreatePathFromNibbles(e.path) res.next, _ = ctx.Build(e.next) @@ -7933,16 +7934,17 @@ func (v *Value) Build(ctx *nodeContext) (NodeReference, *shared.Shared[Node]) { if v.hashStatus != nil { hashStatus = *v.hashStatus } - return NewNodeReference(ValueId(ctx.nextIndex())), shared.MakeShared[Node](&ValueNode{ + res := &ValueNode{ nodeBase: nodeBase{ - clean: !v.dirty, frozen: v.frozen, hashStatus: hashStatus, }, key: v.key, value: v.value, pathLength: v.length, - }) + } + res.nodeBase.clean.Store(!v.dirty) + return NewNodeReference(ValueId(ctx.nextIndex())), shared.MakeShared[Node](res) } type entry struct { diff --git a/go/database/mpt/shared/shared.go b/go/database/mpt/shared/shared.go index a495234b1..f8f442789 100644 --- a/go/database/mpt/shared/shared.go +++ b/go/database/mpt/shared/shared.go @@ -56,6 +56,13 @@ func MakeShared[T any](value T) *Shared[T] { } } +// GetUnprotected provides direct access to the shared value. It is not +// synchronized with any other access and is intended for situation where T +// itself provides synchronization. Any concurrent access is possible. +func (p *Shared[T]) GetUnprotected() T { + return p.value +} + // TryGetReadHandle tries to get read access to the shared value's content. If // successful, indicated by the second return value, shared access to the object // is granted until the provided ReadHandle is released again. Other readers, diff --git a/go/database/mpt/shared/shared_test.go b/go/database/mpt/shared/shared_test.go index c19265b6e..b87ec5bd7 100644 --- a/go/database/mpt/shared/shared_test.go +++ b/go/database/mpt/shared/shared_test.go @@ -46,6 +46,14 @@ func TestShared_LifeCycle(t *testing.T) { read3.Release() } +func TestShared_GetUnprotectedReturnsSharedValue(t *testing.T) { + var data = new(int) + shared := MakeShared(data) + if got, want := shared.GetUnprotected(), data; got != want { + t.Errorf("unexpected shared value, wanted %v, got %v", want, got) + } +} + func TestShared_ReadAccessDoesNotBlocksReadAccess(t *testing.T) { shared := MakeShared(10) read := shared.GetReadHandle() diff --git a/go/database/mpt/state_test.go b/go/database/mpt/state_test.go index 2f7848d5c..275b21d9a 100644 --- a/go/database/mpt/state_test.go +++ b/go/database/mpt/state_test.go @@ -772,13 +772,13 @@ func runFlushBenchmark(b *testing.B, config MptConfig, forceDirtyNodes bool) { handle := node.GetWriteHandle() switch node := handle.Get().(type) { case *BranchNode: - node.clean = false + node.clean.Store(false) case *ExtensionNode: - node.clean = false + node.clean.Store(false) case *ValueNode: - node.clean = false + node.clean.Store(false) case *AccountNode: - node.clean = false + node.clean.Store(false) } handle.Release() }) From b470cb15fc013bdecd213d9c1297e33f3f56cf19 Mon Sep 17 00:00:00 2001 From: Herbert Jordan Date: Fri, 28 Jun 2024 10:28:42 +0200 Subject: [PATCH 4/6] Refined implementation of atomic dirty flag --- go/database/mpt/node_flusher.go | 11 ++-------- go/database/mpt/node_flusher_test.go | 30 +++++++++++++++------------- go/database/mpt/nodes.go | 15 +++++++++----- go/database/mpt/nodes_test.go | 19 +++++++++++++----- go/database/mpt/state_test.go | 8 ++++---- 5 files changed, 46 insertions(+), 37 deletions(-) diff --git a/go/database/mpt/node_flusher.go b/go/database/mpt/node_flusher.go index e1f91aed8..9e01e1d66 100644 --- a/go/database/mpt/node_flusher.go +++ b/go/database/mpt/node_flusher.go @@ -87,16 +87,9 @@ func tryFlushDirtyNodes(cache NodeCache, sink NodeSink) error { dirtyIds := *(dirtyIdsPtr) dirtyIds = dirtyIds[:0] cache.ForEach(func(id NodeId, node *shared.Shared[Node]) { - handle, success := node.TryGetViewHandle() - if !success { - return - } - dirty := handle.Get().IsDirty() - handle.Release() - if !dirty { - return + if node.GetUnprotected().IsDirty() { + dirtyIds = append(dirtyIds, id) } - dirtyIds = append(dirtyIds, id) }) // The IDs are sorted to increase the chance of sequential diff --git a/go/database/mpt/node_flusher_test.go b/go/database/mpt/node_flusher_test.go index eb2c9a87e..4d66bf33b 100644 --- a/go/database/mpt/node_flusher_test.go +++ b/go/database/mpt/node_flusher_test.go @@ -82,7 +82,7 @@ func TestNodeFlusher_ErrorsAreCollected(t *testing.T) { sink := NewMockNodeSink(ctrl) id := ValueId(1) - node := shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: false}}) + node := shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: cleanStatusDirty}}) done := make(chan struct{}) counter := 0 @@ -115,10 +115,10 @@ func TestNodeFlusher_FlushesOnlyDirtyNodes(t *testing.T) { sink := NewMockNodeSink(ctrl) nodes := map[NodeId]*shared.Shared[Node]{ - ValueId(1): shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: true}}), - ValueId(2): shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: false}}), - ValueId(3): shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: false}}), - ValueId(4): shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: true}}), + ValueId(1): shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: cleanStatusClean}}), + ValueId(2): shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: cleanStatusDirty}}), + ValueId(3): shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: cleanStatusDirty}}), + ValueId(4): shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: cleanStatusClean}}), } // All nodes are checked. @@ -151,7 +151,7 @@ func TestNodeFlusher_FlushedNodesAreMarkedClean(t *testing.T) { sink := NewMockNodeSink(ctrl) id := ValueId(1) - node := shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: false}}) + node := shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: cleanStatusDirty}}) cache.EXPECT().ForEach(gomock.Any()).Do(func(f func(NodeId, *shared.Shared[Node])) { f(id, node) }) @@ -177,11 +177,13 @@ func TestNodeFlusher_NodesInUseAreIgnored(t *testing.T) { sink := NewMockNodeSink(ctrl) id := ValueId(1) - node := shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: false}}) + node := shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: cleanStatusDirty}}) cache.EXPECT().ForEach(gomock.Any()).Do(func(f func(NodeId, *shared.Shared[Node])) { f(id, node) }) + cache.EXPECT().Get(RefTo(id)).Return(node, true) + // There shall be no write events (which is the default, but spelled out explicitly here). sink.EXPECT().Write(gomock.Any(), gomock.Any()).Times(0) @@ -206,7 +208,7 @@ func TestNodeFlusher_NodesThatAreAccessedAfterBeingIdentifiedAsDirtyAreIgnored(t sink := NewMockNodeSink(ctrl) id := ValueId(1) - node := shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: false}}) + node := shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: cleanStatusDirty}}) handle := shared.WriteHandle[Node]{} cache.EXPECT().ForEach(gomock.Any()).Do(func(f func(NodeId, *shared.Shared[Node])) { f(id, node) @@ -232,7 +234,7 @@ func TestNodeFlusher_EvictedNodesAreIgnored(t *testing.T) { sink := NewMockNodeSink(ctrl) id := ValueId(1) - node := shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: false}}) + node := shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: cleanStatusDirty}}) cache.EXPECT().ForEach(gomock.Any()).Do(func(f func(NodeId, *shared.Shared[Node])) { f(id, node) }) @@ -256,7 +258,7 @@ func TestNodeFlusher_NodesThatGetMarkedCleanByThirdPartyAreIgnored(t *testing.T) sink := NewMockNodeSink(ctrl) id := ValueId(1) - node := shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: false}}) + node := shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: cleanStatusDirty}}) cache.EXPECT().ForEach(gomock.Any()).Do(func(f func(NodeId, *shared.Shared[Node])) { f(id, node) handle := node.GetWriteHandle() @@ -282,7 +284,7 @@ func TestNodeFlusher_NodesWithDirtyHashesAreIgnored(t *testing.T) { sink := NewMockNodeSink(ctrl) id := ValueId(1) - node := shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: false, hashStatus: hashStatusDirty}}) + node := shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: cleanStatusDirty, hashStatus: hashStatusDirty}}) cache.EXPECT().ForEach(gomock.Any()).Do(func(f func(NodeId, *shared.Shared[Node])) { f(id, node) }) @@ -305,7 +307,7 @@ func TestNodeFlusher_FlushErrorsArePropagated(t *testing.T) { sink := NewMockNodeSink(ctrl) id := ValueId(1) - node := shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: false}}) + node := shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: cleanStatusDirty}}) cache.EXPECT().ForEach(gomock.Any()).Do(func(f func(NodeId, *shared.Shared[Node])) { f(id, node) }) @@ -333,7 +335,7 @@ func TestNodeFlusher_FlushErrorsAreAggregated(t *testing.T) { id1 := ValueId(1) id2 := ValueId(2) - node := shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: false}}) + node := shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: cleanStatusDirty}}) cache.EXPECT().ForEach(gomock.Any()).Do(func(f func(NodeId, *shared.Shared[Node])) { f(id1, node) f(id2, node) @@ -370,7 +372,7 @@ func BenchmarkNodeFlusher_CollectDirtyNodes(b *testing.B) { // Fill a cache with clean nodes. cache := NewNodeCache(size) for i := 0; i < size; i++ { - node := shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: true}}) + node := shared.MakeShared[Node](&ValueNode{nodeBase: nodeBase{clean: cleanStatusClean}}) ref := NewNodeReference(ValueId(uint64(i))) cache.GetOrSet(&ref, node) } diff --git a/go/database/mpt/nodes.go b/go/database/mpt/nodes.go index 47582b487..76b248aae 100644 --- a/go/database/mpt/nodes.go +++ b/go/database/mpt/nodes.go @@ -475,10 +475,15 @@ func (c *nodeCheckContext) isCompatible(other *nodeCheckContext) bool { type nodeBase struct { hash common.Hash // the hash of this node (may be dirty) hashStatus hashStatus // indicating whether this node's hash is valid - clean atomic.Bool // by default nodes are dirty (clean == false) + clean int32 // by default nodes are dirty frozen bool // a flag marking the node as immutable (default: mutable) } +const ( + cleanStatusDirty int32 = 0 // by default, a node is considered dirty + cleanStatusClean int32 = 1 // a node is considered clean if it is in sync with the disk +) + type hashStatus byte const ( @@ -531,21 +536,21 @@ func (n *nodeBase) markMutable() { // This function is thread safe and does not need any specific shared node access // permissions to be accessed safely. func (n *nodeBase) IsDirty() bool { - return !n.clean.Load() + return atomic.LoadInt32(&n.clean) != cleanStatusClean } func (n *nodeBase) MarkClean() { - n.clean.Store(true) + atomic.StoreInt32(&n.clean, cleanStatusClean) } func (n *nodeBase) markDirty() { - n.clean.Store(false) + atomic.StoreInt32(&n.clean, cleanStatusDirty) n.hashStatus = hashStatusDirty } func (n *nodeBase) Release() { // The node is disconnected from the disk version and thus clean. - n.clean.Store(true) + n.MarkClean() n.hashStatus = hashStatusClean } diff --git a/go/database/mpt/nodes_test.go b/go/database/mpt/nodes_test.go index 1b71dcb6c..fb5a35f6b 100644 --- a/go/database/mpt/nodes_test.go +++ b/go/database/mpt/nodes_test.go @@ -14,12 +14,13 @@ import ( "bytes" "errors" "fmt" - "github.com/Fantom-foundation/Carmen/go/common/tribool" "reflect" "slices" "strings" "testing" + "github.com/Fantom-foundation/Carmen/go/common/tribool" + "github.com/Fantom-foundation/Carmen/go/common" "github.com/Fantom-foundation/Carmen/go/database/mpt/shared" gomock "go.uber.org/mock/gomock" @@ -7818,7 +7819,9 @@ func (a *Account) Build(ctx *nodeContext) (NodeReference, *shared.Shared[Node]) storageHashDirty: a.storageHashDirty, storageHash: storageHash, } - res.nodeBase.clean.Store(!a.dirty) + if !a.dirty { + res.MarkClean() + } return NewNodeReference(AccountId(ctx.nextIndex())), shared.MakeShared[Node](res) } @@ -7840,7 +7843,9 @@ type Branch struct { func (b *Branch) Build(ctx *nodeContext) (NodeReference, *shared.Shared[Node]) { ref := NewNodeReference(BranchId(ctx.nextIndex())) res := &BranchNode{} - res.nodeBase.clean.Store(!b.dirty) + if !b.dirty { + res.MarkClean() + } res.frozen = b.frozen for i, desc := range b.children { ref, _ := ctx.Build(desc) @@ -7884,7 +7889,9 @@ type Extension struct { func (e *Extension) Build(ctx *nodeContext) (NodeReference, *shared.Shared[Node]) { ref := NewNodeReference(ExtensionId(ctx.nextIndex())) res := &ExtensionNode{} - res.nodeBase.clean.Store(!e.dirty) + if !e.dirty { + res.MarkClean() + } res.frozen = e.frozen res.path = CreatePathFromNibbles(e.path) res.next, _ = ctx.Build(e.next) @@ -7943,7 +7950,9 @@ func (v *Value) Build(ctx *nodeContext) (NodeReference, *shared.Shared[Node]) { value: v.value, pathLength: v.length, } - res.nodeBase.clean.Store(!v.dirty) + if !v.dirty { + res.MarkClean() + } return NewNodeReference(ValueId(ctx.nextIndex())), shared.MakeShared[Node](res) } diff --git a/go/database/mpt/state_test.go b/go/database/mpt/state_test.go index 275b21d9a..d9225d099 100644 --- a/go/database/mpt/state_test.go +++ b/go/database/mpt/state_test.go @@ -772,13 +772,13 @@ func runFlushBenchmark(b *testing.B, config MptConfig, forceDirtyNodes bool) { handle := node.GetWriteHandle() switch node := handle.Get().(type) { case *BranchNode: - node.clean.Store(false) + node.markDirty() case *ExtensionNode: - node.clean.Store(false) + node.markDirty() case *ValueNode: - node.clean.Store(false) + node.markDirty() case *AccountNode: - node.clean.Store(false) + node.markDirty() } handle.Release() }) From 1cf2b59707f9f7766bb52191524aa8195797fcf3 Mon Sep 17 00:00:00 2001 From: Herbert Jordan Date: Sat, 29 Jun 2024 15:51:20 +0200 Subject: [PATCH 5/6] Fix node reset race condition --- go/database/mpt/forest.go | 8 +++--- go/database/mpt/nodes.go | 45 ++++++++++++++++++++++++++++++++++ go/database/mpt/nodes_mocks.go | 24 ++++++++++++++++++ 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/go/database/mpt/forest.go b/go/database/mpt/forest.go index bf3142084..d0db34400 100644 --- a/go/database/mpt/forest.go +++ b/go/database/mpt/forest.go @@ -911,7 +911,7 @@ func (s *Forest) createAccount() (NodeReference, shared.WriteHandle[Node], error instance, present := s.addToCache(&ref, shared.MakeShared[Node](node)) if present { write := instance.GetWriteHandle() - *write.Get().(*AccountNode) = *node + write.Get().Reset() write.Release() } return ref, instance.GetWriteHandle(), err @@ -927,7 +927,7 @@ func (s *Forest) createBranch() (NodeReference, shared.WriteHandle[Node], error) instance, present := s.addToCache(&ref, shared.MakeShared[Node](node)) if present { write := instance.GetWriteHandle() - *write.Get().(*BranchNode) = *node + write.Get().Reset() write.Release() } return ref, instance.GetWriteHandle(), err @@ -943,7 +943,7 @@ func (s *Forest) createExtension() (NodeReference, shared.WriteHandle[Node], err instance, present := s.addToCache(&ref, shared.MakeShared[Node](node)) if present { write := instance.GetWriteHandle() - *write.Get().(*ExtensionNode) = *node + write.Get().Reset() write.Release() } return ref, instance.GetWriteHandle(), err @@ -959,7 +959,7 @@ func (s *Forest) createValue() (NodeReference, shared.WriteHandle[Node], error) instance, present := s.addToCache(&ref, shared.MakeShared[Node](node)) if present { write := instance.GetWriteHandle() - *write.Get().(*ValueNode) = *node + write.Get().Reset() write.Release() } return ref, instance.GetWriteHandle(), err diff --git a/go/database/mpt/nodes.go b/go/database/mpt/nodes.go index 76b248aae..c9561b630 100644 --- a/go/database/mpt/nodes.go +++ b/go/database/mpt/nodes.go @@ -173,6 +173,9 @@ type Node interface { // rooted by this node. Only non-frozen nodes can be released. Release(manager NodeManager, thisRef *NodeReference, this shared.WriteHandle[Node]) error + // Reset resets the node to its zero state. + Reset() + // IsDirty returns whether this node's state is different in memory than it // is on disk. All nodes are created dirty and may only be cleaned by marking // them as such. @@ -554,6 +557,12 @@ func (n *nodeBase) Release() { n.hashStatus = hashStatusClean } +func (n *nodeBase) reset() { + n.markDirty() + n.hashStatus = hashStatusDirty + n.frozen = false +} + func (n *nodeBase) check(thisRef *NodeReference) error { var errs []error if !n.IsDirty() && n.hashStatus == hashStatusDirty { @@ -636,6 +645,10 @@ func (e EmptyNode) Release(NodeManager, *NodeReference, shared.WriteHandle[Node] return nil } +func (e EmptyNode) Reset() { + // nothing to do +} + func (e EmptyNode) IsDirty() bool { return false } @@ -929,6 +942,14 @@ func (n *BranchNode) Release(manager NodeManager, thisRef *NodeReference, this s return manager.release(thisRef) } +func (n *BranchNode) Reset() { + n.nodeBase.reset() + n.children = [16]NodeReference{} + n.dirtyHashes = 0 + n.embeddedChildren = 0 + n.frozenChildren = 0 +} + func (n *BranchNode) MarkFrozen() { n.nodeBase.MarkFrozen() n.frozenChildren = ^uint16(0) @@ -1404,6 +1425,14 @@ func (n *ExtensionNode) Release(manager NodeManager, thisRef *NodeReference, thi return manager.release(thisRef) } +func (n *ExtensionNode) Reset() { + n.nodeBase.reset() + n.path = Path{} + n.next = NodeReference{} + n.nextHashDirty = true + n.nextIsEmbedded = false +} + func (n *ExtensionNode) Freeze(manager NodeManager, this shared.WriteHandle[Node]) error { if n.IsFrozen() { return nil @@ -1802,6 +1831,15 @@ func (n *AccountNode) Release(manager NodeManager, thisRef *NodeReference, this return manager.release(thisRef) } +func (n *AccountNode) Reset() { + n.nodeBase.reset() + n.address = common.Address{} + n.info = AccountInfo{} + n.storage = NodeReference{} + n.storageHashDirty = true + n.pathLength = 0 +} + func (n *AccountNode) setPathLength(manager NodeManager, thisRef *NodeReference, this shared.WriteHandle[Node], length byte) (NodeReference, bool, error) { if n.pathLength == length { return *thisRef, false, nil @@ -2041,6 +2079,13 @@ func (n *ValueNode) Release(manager NodeManager, thisRef *NodeReference, this sh return manager.release(thisRef) } +func (n *ValueNode) Reset() { + n.nodeBase.reset() + n.key = common.Key{} + n.value = common.Value{} + n.pathLength = 0 +} + func (n *ValueNode) setPathLength(manager NodeManager, thisRef *NodeReference, this shared.WriteHandle[Node], length byte) (NodeReference, bool, error) { if n.pathLength == length { return *thisRef, false, nil diff --git a/go/database/mpt/nodes_mocks.go b/go/database/mpt/nodes_mocks.go index 714a1db64..4e227f687 100644 --- a/go/database/mpt/nodes_mocks.go +++ b/go/database/mpt/nodes_mocks.go @@ -237,6 +237,18 @@ func (mr *MockNodeMockRecorder) Release(manager, thisRef, this any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Release", reflect.TypeOf((*MockNode)(nil).Release), manager, thisRef, this) } +// Reset mocks base method. +func (m *MockNode) Reset() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Reset") +} + +// Reset indicates an expected call of Reset. +func (mr *MockNodeMockRecorder) Reset() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reset", reflect.TypeOf((*MockNode)(nil).Reset)) +} + // SetAccount mocks base method. func (m *MockNode) SetAccount(manager NodeManager, thisRef *NodeReference, this shared.WriteHandle[Node], address common.Address, path []Nibble, info AccountInfo) (NodeReference, bool, error) { m.ctrl.T.Helper() @@ -862,6 +874,18 @@ func (mr *MockleafNodeMockRecorder) Release(manager, thisRef, this any) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Release", reflect.TypeOf((*MockleafNode)(nil).Release), manager, thisRef, this) } +// Reset mocks base method. +func (m *MockleafNode) Reset() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Reset") +} + +// Reset indicates an expected call of Reset. +func (mr *MockleafNodeMockRecorder) Reset() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reset", reflect.TypeOf((*MockleafNode)(nil).Reset)) +} + // SetAccount mocks base method. func (m *MockleafNode) SetAccount(manager NodeManager, thisRef *NodeReference, this shared.WriteHandle[Node], address common.Address, path []Nibble, info AccountInfo) (NodeReference, bool, error) { m.ctrl.T.Helper() From 37a8a05404f69dfa79c801d8e049fb9cfaa5f005 Mon Sep 17 00:00:00 2001 From: Herbert Jordan Date: Sat, 29 Jun 2024 17:04:59 +0200 Subject: [PATCH 6/6] Fix race condition in frozen node copy operation --- go/database/mpt/nodes.go | 67 +++++++++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/go/database/mpt/nodes.go b/go/database/mpt/nodes.go index c9561b630..25fbf0569 100644 --- a/go/database/mpt/nodes.go +++ b/go/database/mpt/nodes.go @@ -781,9 +781,7 @@ func (n *BranchNode) setNextNode( } defer handle.Release() newNode := handle.Get().(*BranchNode) - *newNode = *n - newNode.markDirty() - newNode.markMutable() + newNode.assign(n) n = newNode thisRef = &newRef isClone = true @@ -950,6 +948,16 @@ func (n *BranchNode) Reset() { n.frozenChildren = 0 } +func (n *BranchNode) assign(other *BranchNode) { + n.nodeBase.markDirty() + n.nodeBase.markMutable() + n.children = other.children + n.hashes = other.hashes + n.dirtyHashes = other.dirtyHashes + n.embeddedChildren = other.embeddedChildren + n.frozenChildren = other.frozenChildren +} + func (n *BranchNode) MarkFrozen() { n.nodeBase.MarkFrozen() n.frozenChildren = ^uint16(0) @@ -1209,9 +1217,7 @@ func (n *ExtensionNode) setNextNode( } defer handle.Release() newNode := handle.Get().(*ExtensionNode) - *newNode = *n - newNode.markDirty() - newNode.markMutable() + newNode.assign(n) thisRef, n = &newRef, newNode isClone = true } @@ -1292,9 +1298,7 @@ func (n *ExtensionNode) setNextNode( } defer handle.Release() newNode := handle.Get().(*ExtensionNode) - *newNode = *n - newNode.markDirty() - newNode.markMutable() + newNode.assign(n) thisRef, n = &newRef, newNode isClone = true } @@ -1433,6 +1437,16 @@ func (n *ExtensionNode) Reset() { n.nextIsEmbedded = false } +func (n *ExtensionNode) assign(other *ExtensionNode) { + n.nodeBase.markDirty() + n.nodeBase.markMutable() + n.path = other.path + n.next = other.next + n.nextHash = other.nextHash + n.nextHashDirty = other.nextHashDirty + n.nextIsEmbedded = other.nextIsEmbedded +} + func (n *ExtensionNode) Freeze(manager NodeManager, this shared.WriteHandle[Node]) error { if n.IsFrozen() { return nil @@ -1608,9 +1622,7 @@ func (n *AccountNode) SetAccount(manager NodeManager, thisRef *NodeReference, th } defer handle.Release() newNode := handle.Get().(*AccountNode) - *newNode = *n - newNode.markDirty() - newNode.markMutable() + newNode.assign(n) newNode.info = info return newRef, false, nil } @@ -1762,9 +1774,7 @@ func (n *AccountNode) SetSlot(manager NodeManager, thisRef *NodeReference, this } defer newHandle.Release() newNode := newHandle.Get().(*AccountNode) - *newNode = *n - newNode.markDirty() - newNode.markMutable() + newNode.assign(n) newNode.storage = root newNode.storageHashDirty = true return newRef, false, nil @@ -1794,9 +1804,7 @@ func (n *AccountNode) ClearStorage(manager NodeManager, thisRef *NodeReference, } defer newHandle.Release() newNode := newHandle.Get().(*AccountNode) - *newNode = *n - newNode.markDirty() - newNode.markMutable() + newNode.assign(n) newNode.storage = NewNodeReference(EmptyId()) newNode.storageHashDirty = true return newRef, false, nil @@ -1840,6 +1848,17 @@ func (n *AccountNode) Reset() { n.pathLength = 0 } +func (n *AccountNode) assign(other *AccountNode) { + n.nodeBase.markDirty() + n.nodeBase.markMutable() + n.address = other.address + n.info = other.info + n.storage = other.storage + n.storageHash = other.storageHash + n.storageHashDirty = other.storageHashDirty + n.pathLength = other.pathLength +} + func (n *AccountNode) setPathLength(manager NodeManager, thisRef *NodeReference, this shared.WriteHandle[Node], length byte) (NodeReference, bool, error) { if n.pathLength == length { return *thisRef, false, nil @@ -1851,9 +1870,7 @@ func (n *AccountNode) setPathLength(manager NodeManager, thisRef *NodeReference, } defer newHandle.Release() newNode := newHandle.Get().(*AccountNode) - *newNode = *n - newNode.markDirty() - newNode.markMutable() + newNode.assign(n) newNode.pathLength = length return newRef, false, nil } @@ -2086,6 +2103,14 @@ func (n *ValueNode) Reset() { n.pathLength = 0 } +func (n *ValueNode) assign(other *ValueNode) { + n.nodeBase.markDirty() + n.nodeBase.markMutable() + n.key = other.key + n.value = other.value + n.pathLength = other.pathLength +} + func (n *ValueNode) setPathLength(manager NodeManager, thisRef *NodeReference, this shared.WriteHandle[Node], length byte) (NodeReference, bool, error) { if n.pathLength == length { return *thisRef, false, nil