Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 42 additions & 16 deletions op-service/locks/rwmap.go
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 {
Expand All @@ -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()
Expand Down Expand Up @@ -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)
}
}
50 changes: 47 additions & 3 deletions op-service/locks/rwmap_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package locks

import (
"slices"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -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())
}
2 changes: 1 addition & 1 deletion op-supervisor/supervisor/backend/syncnode/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down