├── .gitignore ├── README.md ├── go.mod ├── go.sum ├── map.go ├── map_bench_test.go ├── map_test.go ├── pkg └── oplog │ ├── entry.go │ ├── entry_test.go │ ├── log.go │ └── log_test.go └── reader.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-evmap 2 | #### Note: this is not a production-ready data structure by any-means. It is currently a work-in-progress exploration of a left-right-backed concurrent map. 3 | 4 | A Go implementation of Rust's [evmap](https://github.com/jonhoo/evmap). This implementation is more of a naive implementation that does not support writer/reader handles and iterators, but this also means that the implementation is extremely simple (<200 lines). It has no direct dependencies. 5 | 6 | ## Usage 7 | ```go 8 | cache := eventual.NewMap[string, int]() 9 | reader := cache.Reader() 10 | 11 | // Insert a key 12 | cache.Insert("foo", 0) 13 | reader.Has("foo") // false 14 | 15 | // Explicitly expose the current state of the map to the reads 16 | cache.Refresh() 17 | reader.Has("foo") // true 18 | ``` 19 | 20 | ## Why? 21 | This data structure is optimized for high-read, low-write workloads where readers never have to coordinate with writers. This lack of coordination comes at a cost, "The trade-off exposed by this module is one of eventual consistency: writes are not visible to readers except following explicit synchronization. Specifically, readers only see the operations that preceded the last call to `Refresh` by a writer. This lets writers decide how stale they are willing to let reads get. They can refresh the map after every write to emulate a regular map, or they can refresh only occasionally to reduce the synchronization overhead at the cost of stale reads." ([evmap readme](https://github.com/jonhoo/evmap)) 22 | 23 | ## Features 24 | * Readers never block writers 25 | * Writers never block readers 26 | * Reads and writes are completely thread-safe 27 | * 100% test coverage 28 | * Utilizes Go 1.18 generics 29 | 30 | ## Caveats 31 | * Readers do not observe writes as they occur (eventual consistency) 32 | * Writers block other writers (writes are guarded by a mutex). 33 | 34 | ## Help Needed 35 | I do not have the expertise to benchmark this. I've implemented a crude benchmark in [map_bench_test.go](./map_bench_test.go) but the results are all across the board. -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/clarkmcc/go-evmap 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.0 // indirect 7 | github.com/pmezard/go-difflib v1.0.0 // indirect 8 | github.com/stretchr/testify v1.7.0 // indirect 9 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 4 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 8 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 9 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 12 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /map.go: -------------------------------------------------------------------------------- 1 | package eventual 2 | 3 | import ( 4 | "github.com/clarkmcc/go-evmap/pkg/oplog" 5 | "sync" 6 | "sync/atomic" 7 | "unsafe" 8 | ) 9 | 10 | // Map is a generic hashmap that provides low-contention, concurrent access 11 | // to the underlying values. Readers don't block writers and vice versa with 12 | // makes this data structure optimal for high-read, low-write scenarios. It 13 | // does this by introducing eventual consistency, where readers are exposed 14 | // to writes only when you explicitly say so. 15 | // 16 | // The underlying data structure is two maps (readable and writable). Writes 17 | // are written to writable and reads are read from readable. At the point where 18 | // a writer wants to expose it's writes to the reader, the writer calls Refresh. 19 | // At this moment, the pointers to the readable and writable maps are atomically 20 | // swapped, the readers now perform all their reads against (what was) the 21 | // writable map and the writes written since the last Refresh are applied to 22 | // (what was) the readable map, after which the writers start applying reads 23 | // to the new (what was the readable map) writable map. 24 | type Map[K comparable, V any] struct { 25 | // readable contains the values that are currently visible to the readers 26 | // and which is not being modified by the writer. 27 | readable *map[K]*V 28 | 29 | // writable contains the values that are currently being modified by the 30 | // writer(s). 31 | writable *map[K]*V 32 | 33 | // A slice of references to every reader that we need to monitor 34 | readers []*Reader[K, V] 35 | readersLock sync.Mutex 36 | 37 | // This should be acquired as soon as we swapLocked readable and writable pointers 38 | // and should be released when we can prove that all readers are now looking 39 | // at writable. 40 | writeLock sync.Mutex 41 | 42 | // Used for replicating writes to m.writable after it's just been swapped 43 | // from m.readable 44 | oplog *oplog.Log[K, V] 45 | } 46 | 47 | // swapLocked takes the pointers to the readable and writable maps and swaps them 48 | // so that the map that was previously used by the readers is now used by 49 | // the writers and the map that was previously written to by the writers is 50 | // now being read by the readers. 51 | func (m *Map[K, V]) swapLocked() { 52 | readable := unsafe.Pointer(m.readable) 53 | writable := unsafe.Pointer(m.writable) 54 | m.readable = (*map[K]*V)(atomic.SwapPointer(&writable, readable)) 55 | m.writable = (*map[K]*V)(atomic.SwapPointer(&readable, writable)) 56 | } 57 | 58 | // syncLocked ensures that the value pointed to by m.readable is up-to-date with the 59 | // value pointed to by m.writable. The only reason to call this function is after 60 | // first calling swapLocked which causes the map that is most up to date (the map pointed 61 | // to by m.writable before the swapLocked) to be switched to reader mode and the map 62 | // that is least up to date (the map pointed to by m.readable before the swapLocked) 63 | // to be switched to writer mode. After performing the swapLocked, we want to replicate 64 | // of our writes syncLocked the previous syncLocked to the map that is now (after the swapLocked) 65 | // pointed to by m.writable. 66 | func (m *Map[K, V]) syncLocked() { 67 | // Clear the oplog after the syncLocked because we don't want to re-apply the same 68 | // operations more than once. 69 | defer m.oplog.Clear() 70 | 71 | // Apply the operations from the oplog to the map currently pointed to by 72 | // m.writable. 73 | m.oplog.Apply(m.writable) 74 | } 75 | 76 | // Refresh exposes the current state of the map to the readers. Under the hood 77 | // refreshing causes the readable and writable maps to be swapped and the new 78 | // writable map to be synced with the old writable map (now m.readable) using 79 | // an internal oplog. 80 | func (m *Map[K, V]) Refresh() { 81 | // Writers should be unable to apply writes to the map while we're getting up 82 | // to syncLocked. This same lock protects the oplog from being modified since all 83 | // modifications to this map are also applied to the oplog. 84 | m.writeLock.Lock() 85 | defer m.writeLock.Unlock() 86 | 87 | // Swap the readable and writable maps globally. This only swaps the pointers 88 | // in this data structure, but does not touch any of the readers. 89 | m.swapLocked() 90 | 91 | // Swap each reader's readable pointer with the new readable pointer 92 | for _, r := range m.readers { 93 | r.swapReadable(m.readable) 94 | } 95 | 96 | // We can assume at this point that all readers are now looking at the new 97 | // readable map which means the writable map is safe to perform writes against. 98 | m.syncLocked() 99 | } 100 | 101 | func (m *Map[K, V]) Reader() *Reader[K, V] { 102 | m.readersLock.Lock() 103 | defer m.readersLock.Unlock() 104 | r := NewReader(m) 105 | m.readers = append(m.readers, r) 106 | return r 107 | } 108 | 109 | func (m *Map[K, V]) Insert(key K, value *V) { 110 | m.writeLock.Lock() 111 | defer m.writeLock.Unlock() 112 | 113 | // This is a map modification so push the insert to the oplog and then apply 114 | // the same modification to the map itself 115 | m.oplog.PushAndApply(oplog.Insert[K, V](key, value), m.writable) 116 | } 117 | 118 | // Delete attempts to delete the key from the map and returns a boolean representing 119 | // whether the key existed. 120 | func (m *Map[K, V]) Delete(key K) bool { 121 | m.writeLock.Lock() 122 | defer m.writeLock.Unlock() 123 | 124 | // Check if the key exists before applying the deletion for obvious reasons 125 | _, ok := (*m.writable)[key] 126 | 127 | // This is a map modification so push the insert to the oplog and then apply 128 | // the same modification to the map itself 129 | m.oplog.PushAndApply(oplog.Delete[K, V](key), m.writable) 130 | return ok 131 | } 132 | 133 | // Clear removes all the keys from the map. Under-the-hood this function does 134 | // not change the map pointer. 135 | func (m *Map[K, V]) Clear() { 136 | m.writeLock.Lock() 137 | defer m.writeLock.Unlock() 138 | 139 | m.oplog.PushAndApply(oplog.Clear[K, V](), m.writable) 140 | } 141 | 142 | // NewMap creates a new Map of the given type with the provided options. 143 | func NewMap[K comparable, V any]() *Map[K, V] { 144 | r := make(map[K]*V) 145 | w := make(map[K]*V) 146 | return &Map[K, V]{ 147 | readable: &r, 148 | writable: &w, 149 | readers: []*Reader[K, V]{}, 150 | oplog: oplog.NewLog[K, V](), 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /map_bench_test.go: -------------------------------------------------------------------------------- 1 | package eventual 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func init() { 11 | rand.Seed(time.Now().UnixNano()) 12 | } 13 | 14 | func BenchmarkReads(b *testing.B) { 15 | b.Run("std", func(b *testing.B) { 16 | m := newStdMap() 17 | 18 | // Fill the map 19 | for i := 0; i < 1_000_000; i++ { 20 | m.Insert(i, &i) 21 | } 22 | 23 | // Read from the map 24 | b.ResetTimer() 25 | for i := 0; i < b.N; i++ { 26 | m.Get(i) 27 | } 28 | }) 29 | b.Run("evmap", func(b *testing.B) { 30 | m := NewMap[int, int]() 31 | reader := m.Reader() 32 | 33 | // Fill the map 34 | for i := 0; i < 1_000_000; i++ { 35 | m.Insert(i, &i) 36 | } 37 | 38 | // Expose the writes to the readers 39 | m.Refresh() 40 | 41 | // Read from the map 42 | b.ResetTimer() 43 | for i := 0; i < b.N; i++ { 44 | reader.Get(i) 45 | } 46 | }) 47 | } 48 | 49 | func BenchmarkParallelReads(b *testing.B) { 50 | b.Run("std", func(b *testing.B) { 51 | m := newStdMap() 52 | 53 | // Fill the map 54 | for i := 0; i < 1_000_000; i++ { 55 | m.Insert(i, &i) 56 | } 57 | 58 | // Read from the map 59 | b.ResetTimer() 60 | b.RunParallel(func(pb *testing.PB) { 61 | for pb.Next() { 62 | m.Get(rand.Intn(1_000_000)) 63 | } 64 | }) 65 | }) 66 | b.Run("evmap", func(b *testing.B) { 67 | m := NewMap[int, int]() 68 | reader := m.Reader() 69 | 70 | // Fill the map 71 | for i := 0; i < 1_000_000; i++ { 72 | m.Insert(i, &i) 73 | } 74 | 75 | // Expose the writes to the readers 76 | m.Refresh() 77 | 78 | // Read from the map 79 | b.ResetTimer() 80 | b.RunParallel(func(pb *testing.PB) { 81 | for pb.Next() { 82 | reader.Get(rand.Intn(1_000_000)) 83 | } 84 | }) 85 | }) 86 | } 87 | 88 | func BenchmarkWrites(b *testing.B) { 89 | b.Run("std", func(b *testing.B) { 90 | m := newStdMap() 91 | 92 | // Fill the map 93 | b.ResetTimer() 94 | for i := 0; i < 1_000_000; i++ { 95 | m.Insert(i, &i) 96 | } 97 | }) 98 | b.Run("evmap", func(b *testing.B) { 99 | m := NewMap[int, int]() 100 | 101 | // Fill the map 102 | b.ResetTimer() 103 | for i := 0; i < 1_000_000; i++ { 104 | m.Insert(i, &i) 105 | } 106 | }) 107 | } 108 | 109 | func BenchmarkReadsAndWrites(b *testing.B) { 110 | b.Run("std", func(b *testing.B) { 111 | m := newStdMap() 112 | 113 | // Single Writer 114 | done := make(chan struct{}) 115 | go func() { 116 | for { 117 | select { 118 | case <-done: 119 | return 120 | default: 121 | key := rand.Intn(1_000_000) 122 | m.Insert(key, &key) 123 | } 124 | } 125 | }() 126 | 127 | // Multiple Readers 128 | b.RunParallel(func(pb *testing.PB) { 129 | for pb.Next() { 130 | m.Get(rand.Intn(1_000_000)) 131 | } 132 | }) 133 | close(done) 134 | }) 135 | b.Run("evmap", func(b *testing.B) { 136 | m := NewMap[int, int]() 137 | 138 | // Single Writer 139 | done := make(chan struct{}) 140 | go func() { 141 | i := 0 142 | for { 143 | select { 144 | case <-done: 145 | return 146 | default: 147 | key := rand.Intn(1_000_000) 148 | m.Insert(key, &key) 149 | i++ 150 | // Replicate every 10,000 writes 151 | if i%10000 == 0 { 152 | m.Refresh() 153 | } 154 | } 155 | } 156 | }() 157 | 158 | // Multiple Readers 159 | b.RunParallel(func(pb *testing.PB) { 160 | reader := m.Reader() 161 | for pb.Next() { 162 | reader.Get(rand.Intn(1_000_000)) 163 | } 164 | }) 165 | }) 166 | } 167 | 168 | type stdMap struct { 169 | lock sync.RWMutex 170 | m map[int]*int 171 | } 172 | 173 | func (t *stdMap) Insert(key int, value *int) { 174 | t.lock.Lock() 175 | defer t.lock.Unlock() 176 | t.m[key] = value 177 | } 178 | 179 | func (t *stdMap) Get(key int) (*int, bool) { 180 | t.lock.RLock() 181 | defer t.lock.RUnlock() 182 | v, ok := t.m[key] 183 | return v, ok 184 | } 185 | 186 | func newStdMap() *stdMap { 187 | return &stdMap{ 188 | m: map[int]*int{}, 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /map_test.go: -------------------------------------------------------------------------------- 1 | package eventual 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestMap(t *testing.T) { 9 | m := NewMap[string, any]() 10 | 11 | t.Run("Insert", func(t *testing.T) { 12 | m.Insert("foo", nil) 13 | m.Insert("bar", nil) 14 | 15 | // Check that these keys are in the writable map 16 | assert.Len(t, *m.writable, 2) 17 | assert.Len(t, *m.readable, 0) 18 | }) 19 | t.Run("Refresh", func(t *testing.T) { 20 | m.Refresh() 21 | 22 | // Check that the keys have been moved to the readable map 23 | assert.Len(t, *m.readable, 2) 24 | 25 | // Check that the keys have been re-applied to the new writable map 26 | assert.Len(t, *m.writable, 2) 27 | }) 28 | t.Run("Delete", func(t *testing.T) { 29 | m.Delete("foo") 30 | 31 | // Check that the readers haven't seen this change 32 | assert.Len(t, *m.readable, 2) 33 | 34 | // But the writers have 35 | assert.Len(t, *m.writable, 1) 36 | }) 37 | t.Run("has & get", func(t *testing.T) { 38 | reader := m.Reader() 39 | v, ok := reader.Get("foo") 40 | 41 | // Readers haven't seen this key deleted yet 42 | assert.True(t, ok) 43 | assert.True(t, reader.Has("foo")) 44 | 45 | // Run a refresh 46 | m.Refresh() 47 | 48 | // Readers should see the key missing now 49 | v, ok = reader.Get("foo") 50 | assert.Nil(t, v) 51 | assert.False(t, ok) 52 | assert.False(t, reader.Has("foo")) 53 | }) 54 | t.Run("Clear", func(t *testing.T) { 55 | m.Clear() 56 | 57 | // Readers shouldn't see the clear yet 58 | assert.Len(t, *m.readable, 1, "reader shouldn't see the clear yet") 59 | assert.Len(t, *m.writable, 0, "writer should have seen the clear") 60 | 61 | m.Refresh() 62 | 63 | assert.Len(t, *m.readable, 0, "reader should see the clear after refresh") 64 | }) 65 | } 66 | 67 | func TestMap_swap(t *testing.T) { 68 | m := NewMap[string, any]() 69 | 70 | // Check the pointers 71 | ptr1 := m.writable 72 | ptr2 := m.readable 73 | 74 | // Swap 75 | m.swapLocked() 76 | 77 | // Check the pointers again 78 | assert.Equal(t, m.writable, ptr2) 79 | assert.Equal(t, m.readable, ptr1) 80 | } 81 | 82 | func TestMap_sync(t *testing.T) { 83 | m := NewMap[string, any]() 84 | 85 | // Add a value 86 | m.Insert("foo", nil) 87 | assert.Equal(t, m.oplog.Len(), 1) 88 | 89 | // Check the writable map 90 | assert.Len(t, *m.writable, 1, "one value should have been written to writable") 91 | 92 | // Perform the swapLocked 93 | m.swapLocked() 94 | assert.Len(t, *m.writable, 0, "writable has been swapped with readable and the new writable should be empty") 95 | 96 | // Perform the syncLocked 97 | m.syncLocked() 98 | assert.Len(t, *m.writable, 1, "the new writable has been synced with the old writable and should have the inserted value") 99 | } 100 | -------------------------------------------------------------------------------- /pkg/oplog/entry.go: -------------------------------------------------------------------------------- 1 | package oplog 2 | 3 | // Indicates the supported types of oplog entries that can be stored in the oplog. These 4 | // types are limited to the modifications that can be made to a map. 5 | type entryType uint8 6 | 7 | const ( 8 | entryTypeInsert entryType = iota 9 | entryTypeDelete 10 | entryTypeClear 11 | ) 12 | 13 | // entry is an oplog entry that may (but not always) be associated with a v 14 | type entry[K comparable, V any] struct { 15 | t entryType 16 | k K 17 | v *V 18 | } 19 | 20 | // newEntry creates a new oplog entry with the associated type and v 21 | func newEntry[K comparable, V any](t entryType, key K, value *V) *entry[K, V] { 22 | return &entry[K, V]{ 23 | t: t, 24 | k: key, 25 | v: value, 26 | } 27 | } 28 | 29 | // Insert creates an oplog entry that inserts a v into the map 30 | func Insert[K comparable, V any](key K, value *V) *entry[K, V] { 31 | return newEntry(entryTypeInsert, key, value) 32 | } 33 | 34 | // Delete creates an oplog entry that deletes a v from the map 35 | func Delete[K comparable, V any](key K) *entry[K, V] { 36 | return newEntry[K, V](entryTypeDelete, key, nil) 37 | } 38 | 39 | // Clear clears the entire contents from the map 40 | func Clear[K comparable, V any]() *entry[K, V] { 41 | return &entry[K, V]{ 42 | t: entryTypeClear, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pkg/oplog/entry_test.go: -------------------------------------------------------------------------------- 1 | package oplog 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func Test(t *testing.T) { 9 | v := "bar" 10 | e := Insert("foo", &v) 11 | fmt.Println(*e.v) 12 | } 13 | -------------------------------------------------------------------------------- /pkg/oplog/log.go: -------------------------------------------------------------------------------- 1 | package oplog 2 | 3 | // Log stores a slice of oplog entries that can be applied to a map. This 4 | // data structure is not thread-safe, which means that any implementors 5 | // should provide the concurrency synchronization guarantees. 6 | type Log[K comparable, V any] struct { 7 | entries []*entry[K, V] 8 | 9 | // The most recent entry applied to the log 10 | latest *entry[K, V] 11 | } 12 | 13 | // Push pushes a new entry into the oplog and updates the oplog's latest entry 14 | func (l *Log[K, V]) Push(e *entry[K, V]) { 15 | l.entries = append(l.entries, e) 16 | l.latest = e 17 | } 18 | 19 | // PushAndApply pushes a new entry to the oplog and applies that same entry to 20 | // the provided map. 21 | func (l *Log[K, V]) PushAndApply(e *entry[K, V], m *map[K]*V) { 22 | l.entries = append(l.entries, e) 23 | l.latest = e 24 | applyEntry(e, m) 25 | } 26 | 27 | // Apply applies the oplog to the specified map 28 | func (l *Log[K, V]) Apply(m *map[K]*V) { 29 | for _, e := range l.entries { 30 | applyEntry(e, m) 31 | } 32 | } 33 | 34 | // Clear empties the oplog 35 | func (l *Log[K, V]) Clear() { 36 | l.entries = []*entry[K, V]{} 37 | } 38 | 39 | // Len returns the current length of the oplog 40 | func (l *Log[K, V]) Len() int { 41 | return len(l.entries) 42 | } 43 | 44 | // NewLog creates a new oplog with the given types 45 | func NewLog[K comparable, V any]() *Log[K, V] { 46 | return &Log[K, V]{entries: []*entry[K, V]{}} 47 | } 48 | 49 | // applyEntry is a helper function for applying a single oplog entry to 50 | // the destination map. 51 | func applyEntry[K comparable, V any](e *entry[K, V], m *map[K]*V) { 52 | switch e.t { 53 | case entryTypeInsert: 54 | (*m)[e.k] = e.v 55 | case entryTypeDelete: 56 | delete(*m, e.k) 57 | case entryTypeClear: 58 | for k := range *m { 59 | delete(*m, k) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pkg/oplog/log_test.go: -------------------------------------------------------------------------------- 1 | package oplog 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestLog(t *testing.T) { 9 | log := NewLog[string, int]() 10 | m := map[string]*int{} 11 | 12 | // Each of these tests piggyback on each other and cannot be run separately 13 | t.Run("Insert", func(t *testing.T) { 14 | v1 := 1 15 | v2 := 2 16 | log.Push(Insert("foo", &v1)) 17 | log.Push(Insert("bar", &v2)) 18 | log.Apply(&m) 19 | log.Clear() 20 | 21 | assert.Len(t, m, 2) 22 | assert.Equal(t, v1, *m["foo"]) 23 | }) 24 | t.Run("Delete", func(t *testing.T) { 25 | log.Push(Delete[string, int]("foo")) 26 | log.Apply(&m) 27 | log.Clear() 28 | 29 | assert.Len(t, m, 1) 30 | }) 31 | t.Run("Clear", func(t *testing.T) { 32 | log.Push(Clear[string, int]()) 33 | log.Apply(&m) 34 | 35 | assert.Len(t, m, 0) 36 | }) 37 | t.Run("PushAndApply", func(t *testing.T) { 38 | v1 := 1 39 | log.PushAndApply(Insert("foo", &v1), &m) 40 | assert.Len(t, m, 1) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /reader.go: -------------------------------------------------------------------------------- 1 | package eventual 2 | 3 | import ( 4 | "sync" 5 | "unsafe" 6 | ) 7 | 8 | type Reader[K comparable, V any] struct { 9 | closed bool 10 | m *Map[K, V] 11 | lock sync.Mutex 12 | 13 | readable unsafe.Pointer 14 | } 15 | 16 | func (r *Reader[K, V]) Get(key K) (*V, bool) { 17 | r.lock.Lock() 18 | defer r.lock.Unlock() 19 | 20 | if r.closed { 21 | panic("reader closed") 22 | } 23 | v, ok := (*((*map[K]*V)(r.readable)))[key] 24 | return v, ok 25 | } 26 | 27 | func (r *Reader[K, V]) Has(key K) bool { 28 | r.lock.Lock() 29 | defer r.lock.Unlock() 30 | 31 | if r.closed { 32 | panic("reader closed") 33 | } 34 | _, ok := (*((*map[K]*V)(r.readable)))[key] 35 | return ok 36 | } 37 | 38 | // Close removes the reader from the map. The caller will not be able 39 | // to use the reader anymore. Reading after close will result in a panic 40 | func (r *Reader[K, V]) Close() { 41 | r.m.readersLock.Lock() 42 | defer r.m.readersLock.Unlock() 43 | for idx, reader := range r.m.readers { 44 | if unsafe.Pointer(reader) == unsafe.Pointer(r) { 45 | remove[*Reader[K, V]](r.m.readers, idx) 46 | break 47 | } 48 | } 49 | } 50 | 51 | func (r *Reader[K, V]) swapReadable(m *map[K]*V) { 52 | r.lock.Lock() 53 | defer r.lock.Unlock() 54 | r.readable = unsafe.Pointer(m) 55 | } 56 | 57 | func NewReader[K comparable, V any](m *Map[K, V]) *Reader[K, V] { 58 | return &Reader[K, V]{m: m, readable: unsafe.Pointer(m.readable)} 59 | } 60 | 61 | func remove[V any](s []V, i int) []V { 62 | s[i] = s[len(s)-1] 63 | return s[:len(s)-1] 64 | } 65 | --------------------------------------------------------------------------------