Skip to content

Commit cd76858

Browse files
committed
syncmap
1 parent f14d849 commit cd76858

File tree

3 files changed

+209
-11
lines changed

3 files changed

+209
-11
lines changed

internal/verifier/compare.go

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"time"
77

88
"github.com/10gen/migration-verifier/internal/types"
9+
"github.com/10gen/migration-verifier/syncmap"
910
"github.com/pkg/errors"
1011
"go.mongodb.org/mongo-driver/bson"
1112
"go.mongodb.org/mongo-driver/mongo"
@@ -15,6 +16,8 @@ import (
1516

1617
const readTimeout = 10 * time.Minute
1718

19+
type docCacheMap = syncmap.SyncMap[string, bson.Raw]
20+
1821
func (verifier *Verifier) FetchAndCompareDocuments(
1922
givenCtx context.Context,
2023
task *VerificationTask,
@@ -73,8 +76,8 @@ func (verifier *Verifier) compareDocsFromChannels(
7376

7477
namespace := task.QueryFilter.Namespace
7578

76-
srcCache := map[string]bson.Raw{}
77-
dstCache := map[string]bson.Raw{}
79+
srcCache := &docCacheMap{}
80+
dstCache := &docCacheMap{}
7881

7982
// This is the core document-handling logic. It either:
8083
//
@@ -84,7 +87,7 @@ func (verifier *Verifier) compareDocsFromChannels(
8487
handleNewDoc := func(doc bson.Raw, isSrc bool) error {
8588
mapKey := getMapKey(doc, mapKeyFieldNames)
8689

87-
var ourMap, theirMap map[string]bson.Raw
90+
var ourMap, theirMap *docCacheMap
8891

8992
if isSrc {
9093
ourMap = srcCache
@@ -95,22 +98,22 @@ func (verifier *Verifier) compareDocsFromChannels(
9598
}
9699
// See if we've already cached a document with this
97100
// mapKey from the other channel.
98-
theirDoc, exists := theirMap[mapKey]
101+
theirDoc, exists := theirMap.Load(mapKey)
99102

100103
// If there is no such cached document, then cache the newly-received
101104
// document in our map then proceed to the next document.
102105
//
103106
// (We'll remove the cache entry when/if the other channel yields a
104107
// document with the same mapKey.)
105108
if !exists {
106-
ourMap[mapKey] = doc
109+
ourMap.Store(mapKey, doc)
107110
return nil
108111
}
109112

110113
// We have two documents! First we remove the cache entry. This saves
111114
// memory, but more importantly, it lets us know, once we exhaust the
112115
// channels, which documents were missing on one side or the other.
113-
delete(theirMap, mapKey)
116+
theirMap.Delete(mapKey)
114117

115118
// Now we determine which document came from whom.
116119
var srcDoc, dstDoc bson.Raw
@@ -227,9 +230,9 @@ func (verifier *Verifier) compareDocsFromChannels(
227230
// missing on the other side. We add results for those.
228231

229232
// We might as well pre-grow the slice:
230-
results = slices.Grow(results, len(srcCache)+len(dstCache))
233+
results = slices.Grow(results, srcCache.Len()+dstCache.Len())
231234

232-
for _, doc := range srcCache {
235+
srcCache.Range(func(_ string, doc bson.Raw) bool {
233236
results = append(
234237
results,
235238
VerificationResult{
@@ -240,9 +243,11 @@ func (verifier *Verifier) compareDocsFromChannels(
240243
dataSize: len(doc),
241244
},
242245
)
243-
}
244246

245-
for _, doc := range dstCache {
247+
return true
248+
})
249+
250+
dstCache.Range(func(_ string, doc bson.Raw) bool {
246251
results = append(
247252
results,
248253
VerificationResult{
@@ -253,7 +258,9 @@ func (verifier *Verifier) compareDocsFromChannels(
253258
dataSize: len(doc),
254259
},
255260
)
256-
}
261+
262+
return true
263+
})
257264

258265
return results, docCount, byteCount, nil
259266
}

syncmap/syncmap.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package syncmap
2+
3+
// This package wraps sync.Map to provide type safety via generics.
4+
5+
// Surprisingly, as of May 2023 there doesn’t appear to be a “go-to”
6+
// public implementation of this. Initially we considered gopkg.in/typ.v4,
7+
// but this largely reimplements sync.Map, which seems unideal for
8+
// long-term maintenance and seems to lack wide adoption.
9+
// github.com/puzpuzpuz/xsync is widely used but, because it attempts
10+
// to iterate on sync.Map’s internals, requires map keys to be strings.
11+
//
12+
// Assumedly Go’s standard library will eventually add something like
13+
// this, but for now this is a small, useful enough effort for us to
14+
// have for ourselves.
15+
//
16+
// See this module’s tests for example usage.
17+
18+
import (
19+
"sync"
20+
)
21+
22+
// SyncMap is a generics-powered replacement for sync.Map.
23+
type SyncMap[KT comparable, VT any] struct {
24+
internalMap sync.Map
25+
}
26+
27+
// Load behaves like sync.Map’s corresponding method, but
28+
// if the value doesn’t exist then this returns the value type’s
29+
// zero-value.
30+
func (m *SyncMap[KT, VT]) Load(key KT) (VT, bool) {
31+
return m.returnLoad(m.internalMap.Load(key))
32+
}
33+
34+
// Delete behaves like sync.Map’s corresponding method.
35+
func (m *SyncMap[KT, VT]) Delete(key KT) {
36+
m.internalMap.Delete(key)
37+
}
38+
39+
// Store behaves like sync.Map’s corresponding method.
40+
func (m *SyncMap[KT, VT]) Store(key KT, value VT) {
41+
m.internalMap.Store(key, value)
42+
}
43+
44+
// LoadAndDelete behaves like sync.Map’s corresponding method.
45+
func (m *SyncMap[KT, VT]) LoadAndDelete(key KT) (VT, bool) {
46+
return m.returnLoad(m.internalMap.LoadAndDelete(key))
47+
}
48+
49+
// LoadOrStore behaves like sync.Map’s corresponding method.
50+
func (m *SyncMap[KT, VT]) LoadOrStore(key KT, value VT) (VT, bool) {
51+
actual, loaded := m.internalMap.LoadOrStore(key, value)
52+
53+
return actual.(VT), loaded
54+
}
55+
56+
// Range behaves like sync.Map’s corresponding method.
57+
func (m *SyncMap[KT, VT]) Range(f func(KT, VT) bool) {
58+
m.internalMap.Range(func(key, value any) bool {
59+
return f(key.(KT), value.(VT))
60+
})
61+
}
62+
63+
// ----------------------------------------------------------------------
64+
65+
// Len is a convenience method that returns the number of elements
66+
// the map stores. It has no counterpart in sync.Map.
67+
// This method is O(n); i.e., larger maps take longer.
68+
func (m *SyncMap[KT, VT]) Len() int {
69+
mapLen := 0
70+
71+
m.internalMap.Range(func(_, _ any) bool {
72+
mapLen++
73+
return true
74+
})
75+
76+
return mapLen
77+
}
78+
79+
// ----------------------------------------------------------------------
80+
81+
func (_ *SyncMap[KT, VT]) returnLoad(val any, loaded bool) (VT, bool) {
82+
if !loaded {
83+
return *new(VT), false
84+
}
85+
86+
return val.(VT), true
87+
}

syncmap/syncmap_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package syncmap
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/suite"
7+
)
8+
9+
type mySuite struct {
10+
suite.Suite
11+
}
12+
13+
func TestUnitTestSuite(t *testing.T) {
14+
suite.Run(t, &mySuite{})
15+
}
16+
17+
func (s *mySuite) TestSyncMap_NonexistentString() {
18+
strMap := SyncMap[string, string]{}
19+
20+
nadaStr, myOk := strMap.Load("foo")
21+
s.Assert().Empty(nadaStr, "Load() of string when key is nonexistent")
22+
s.Assert().False(myOk, "Load() indicates nonexistence")
23+
}
24+
25+
func (s *mySuite) TestSyncMap_Range() {
26+
keys := []int32{}
27+
vals := []int64{}
28+
otherMap := SyncMap[int32, int64]{}
29+
otherMap.Store(1, 11)
30+
otherMap.Store(2, 22)
31+
otherMap.Store(3, 33)
32+
33+
otherMap.Range(func(k int32, v int64) bool {
34+
keys = append(keys, k)
35+
vals = append(vals, v)
36+
return true
37+
})
38+
39+
s.Assert().ElementsMatch(
40+
[]int32{1, 2, 3},
41+
keys,
42+
"Range(): keys as expected",
43+
)
44+
45+
s.Assert().ElementsMatch(
46+
[]int64{11, 22, 33},
47+
vals,
48+
"Range(): values as expected",
49+
)
50+
}
51+
52+
func (s *mySuite) TestSyncMap_Len() {
53+
theMap := SyncMap[string, []byte]{}
54+
55+
theMap.Store("foo", []byte{})
56+
theMap.Store("bar", []byte{})
57+
theMap.Store("baz", []byte{})
58+
theMap.Store("qux", []byte{})
59+
60+
s.Assert().Equal(4, theMap.Len(), "Len() gives the size")
61+
}
62+
63+
func (s *mySuite) TestSyncMap_LoadOrStore() {
64+
theMap := SyncMap[string, []byte]{}
65+
66+
theVal, ok := theMap.LoadOrStore("foo", []byte{2})
67+
s.Assert().Equal([]byte{2}, theVal, "LoadOrStore() returned new value if no old one exists")
68+
s.Assert().False(ok, "LoadOrStore() indicates nonexistence")
69+
70+
theVal, ok = theMap.LoadOrStore("foo", []byte{3})
71+
s.Assert().Equal([]byte{2}, theVal, "LoadOrStore() returned old value")
72+
s.Assert().True(ok, "LoadOrStore() indicates existence")
73+
}
74+
75+
func (s *mySuite) TestSyncMap_LoadAndDelete() {
76+
theMap := SyncMap[string, []byte]{}
77+
78+
theVal, ok := theMap.LoadAndDelete("foo")
79+
s.Assert().Nil(theVal, "LoadAndDelete() returns nil on nonexistence")
80+
s.Assert().False(ok, "boolean indicates nonexistence")
81+
82+
theMap.Store("foo", []byte{1})
83+
84+
theVal, ok = theMap.LoadAndDelete("foo")
85+
s.Assert().Equal([]byte{1}, theVal, "LoadAndDelete() returned Store()d value")
86+
s.Assert().True(ok, "boolean indicates existence")
87+
}
88+
89+
func (s *mySuite) TestSyncMap_Basics() {
90+
theMap := SyncMap[string, []byte]{}
91+
92+
nada, ok := theMap.Load("foo")
93+
s.Assert().Nil(nada, "Load() of array when key is nonexistent")
94+
s.Assert().False(ok, "Load() indicates nonexistence")
95+
96+
theMap.Store("foo", []byte{1})
97+
theVal, ok := theMap.Load("foo")
98+
s.Assert().Equal([]byte{1}, theVal, "Load() returned Store()d value")
99+
s.Assert().True(ok, "Load() indicates existence")
100+
101+
theMap.Delete("foo")
102+
_, ok = theMap.Load("foo")
103+
s.Assert().False(ok, "Load() indicates nonexistence after Delete()")
104+
}

0 commit comments

Comments
 (0)