diff --git a/op-service/locks/rwmap.go b/op-service/locks/rwmap.go index 98eecad91f1..3830a2fcc49 100644 --- a/op-service/locks/rwmap.go +++ b/op-service/locks/rwmap.go @@ -1,6 +1,9 @@ package locks -import "sync" +import ( + "maps" + "sync" +) // RWMap is a simple wrapper around a map, with global Read-Write protection. // For many concurrent reads/writes a sync.Map may be more performant, @@ -12,8 +15,14 @@ type RWMap[K comparable, V any] struct { mu sync.RWMutex } -// Default creates a value at the given key, if the key is not set yet. -func (m *RWMap[K, V]) Default(key K, fn func() V) (changed bool) { +// RWMapFromMap creates a RWMap from the given map. +// This shallow-copies the map, changes to the original map will not affect the new RWMap. +func RWMapFromMap[K comparable, V any](m map[K]V) *RWMap[K, V] { + return &RWMap[K, V]{inner: maps.Clone(m)} +} + +// CreateIfMissing creates a value at the given key, if the key is not set yet. +func (m *RWMap[K, V]) CreateIfMissing(key K, fn func() V) (changed bool) { m.mu.Lock() defer m.mu.Unlock() if m.inner == nil { @@ -26,6 +35,14 @@ func (m *RWMap[K, V]) Default(key K, fn func() V) (changed bool) { return !ok // if it exists, nothing changed } +// SetIfMissing is a convenience function to set a missing value if it does not already exist. +// To lazy-init the value, see CreateIfMissing. +func (m *RWMap[K, V]) SetIfMissing(key K, v V) (changed bool) { + return m.CreateIfMissing(key, func() V { + return v + }) +} + func (m *RWMap[K, V]) Has(key K) (ok bool) { m.mu.RLock() defer m.mu.RUnlock() @@ -73,22 +90,31 @@ func (m *RWMap[K, V]) Range(f func(key K, value V) bool) { } } +// Keys returns an unsorted list of keys of the map. +func (m *RWMap[K, V]) Keys() (out []K) { + m.mu.RLock() + defer m.mu.RUnlock() + out = make([]K, 0, len(m.inner)) + for k := range m.inner { + out = append(out, k) + } + return out +} + +// Values returns an unsorted list of values of the map. +func (m *RWMap[K, V]) Values() (out []V) { + m.mu.RLock() + defer m.mu.RUnlock() + out = make([]V, 0, len(m.inner)) + for _, v := range m.inner { + out = append(out, v) + } + return out +} + // Clear removes all key-value pairs from the map. func (m *RWMap[K, V]) Clear() { m.mu.Lock() defer m.mu.Unlock() clear(m.inner) } - -// InitPtrMaybe sets a pointer-value in the map, if it's not set yet, to a new object. -func InitPtrMaybe[K comparable, V any](m *RWMap[K, *V], key K) { - m.mu.Lock() - defer m.mu.Unlock() - if m.inner == nil { - m.inner = make(map[K]*V) - } - _, ok := m.inner[key] - if !ok { - m.inner[key] = new(V) - } -} diff --git a/op-service/locks/rwmap_test.go b/op-service/locks/rwmap_test.go index c2d49384e2a..b624a428675 100644 --- a/op-service/locks/rwmap_test.go +++ b/op-service/locks/rwmap_test.go @@ -1,6 +1,7 @@ package locks import ( + "slices" "testing" "github.com/stretchr/testify/require" @@ -62,25 +63,68 @@ func TestRWMap(t *testing.T) { m.Delete(132983213) m.Set(10001, 100) - m.Default(10001, func() int64 { + m.CreateIfMissing(10001, func() int64 { t.Fatal("should not replace existing value") return 0 }) - m.Default(10002, func() int64 { + m.CreateIfMissing(10002, func() int64 { return 42 }) v, ok = m.Get(10002) require.True(t, ok) require.Equal(t, int64(42), v) + + require.True(t, m.SetIfMissing(10003, 111)) + require.False(t, m.SetIfMissing(10003, 123)) + v, ok = m.Get(10003) + require.True(t, ok) + require.Equal(t, int64(111), v) } func TestRWMap_DefaultOnEmpty(t *testing.T) { m := &RWMap[uint64, int64]{} // this should work, even if the first call to the map. - m.Default(10002, func() int64 { + m.CreateIfMissing(10002, func() int64 { return 42 }) v, ok := m.Get(10002) require.True(t, ok) require.Equal(t, int64(42), v) } + +func TestRWMap_KeysValues(t *testing.T) { + m := &RWMap[uint64, int64]{} + + require.Empty(t, m.Keys()) + require.Empty(t, m.Values()) + + m.Set(1, 100) + m.Set(2, 200) + m.Set(3, 300) + + length := m.Len() + + keys := m.Keys() + require.Equal(t, length, len(keys)) + slices.Sort(keys) + require.Equal(t, []uint64{1, 2, 3}, keys) + + values := m.Values() + require.Equal(t, length, len(values)) + slices.Sort(values) + require.Equal(t, []int64{100, 200, 300}, values) + + m.Clear() + + require.Empty(t, m.Keys()) + require.Empty(t, m.Values()) +} + +func TestRWMapFromMap(t *testing.T) { + m := RWMapFromMap(map[uint64]int64{ + 1: 10, + 2: 20, + 3: 30, + }) + require.Equal(t, 3, m.Len()) +} diff --git a/op-supervisor/supervisor/backend/syncnode/controller.go b/op-supervisor/supervisor/backend/syncnode/controller.go index 85b4677fded..c9866d5d5eb 100644 --- a/op-supervisor/supervisor/backend/syncnode/controller.go +++ b/op-supervisor/supervisor/backend/syncnode/controller.go @@ -69,7 +69,7 @@ func (snc *SyncNodesController) AttachNodeController(chainID eth.ChainID, ctrl S return nil, fmt.Errorf("chain %v not in dependency set: %w", chainID, types.ErrUnknownChain) } // lazy init the controllers map for this chain - snc.controllers.Default(chainID, func() *locks.RWMap[*ManagedNode, struct{}] { + snc.controllers.CreateIfMissing(chainID, func() *locks.RWMap[*ManagedNode, struct{}] { return &locks.RWMap[*ManagedNode, struct{}]{} }) controllersForChain, _ := snc.controllers.Get(chainID)