Skip to content

Commit 3689374

Browse files
authored
Add expiry of old gone entries. in case of dup process, exit the oldest one. fixed some -race issues. Improvement to Sync map: sorted concurrent safe iteration (#12)
* Add expiry of old gone entries. in case of dup process, exit the oldest one. fixed some -race issues * Add sorting * Add NaturalSort even if we can't use it * linters * Somewhat useful overlord comments for once * more ai nits * more lint * reuse the AllSorted instead of copy-pasta
1 parent 319467c commit 3689374

File tree

4 files changed

+378
-12
lines changed

4 files changed

+378
-12
lines changed

main.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,14 +111,17 @@ func Main() int {
111111
err = ap.FPSTicks(func() bool {
112112
// Only refresh if we had (log) output or something changed, so cursor blinks (!).
113113
logHadOutput := ap.FlushLogger()
114+
if srv.Stopped() {
115+
return false
116+
}
114117
curVersion := version.Load()
115118
// log.Debugf("Have %d peers (prev %d), logHadOutput=%v", numPeers, prev, logHadOutput)
116119
if logHadOutput || curVersion != prev {
117120
if !logHadOutput {
118121
ap.StartSyncMode()
119122
}
120123
prev = curVersion
121-
for peer, peerData := range srv.Peers.All() {
124+
for peer, peerData := range srv.Peers.AllSorted(tsnet.PeerSort) {
122125
fmt.Fprintf(&buf, "\n%s", PeerString(peer, peerData))
123126
}
124127
ap.WriteBoxed(1, "%s\n🔗%s", ourLine, buf.String())

smap/smap.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
package smap
33

44
import (
5+
"cmp"
56
"iter"
7+
"sort"
68
"sync"
79
)
810

@@ -138,3 +140,64 @@ func (s *Map[K, V]) Keys() iter.Seq[K] {
138140
}
139141
}
140142
}
143+
144+
// KeysSorted returns an iterator over keys sorted using the provided comparison function.
145+
// The map snapshot occurs under a read lock, then sorting happens without holding the lock.
146+
func (s *Map[K, V]) KeysSorted(less func(a, b K) bool) iter.Seq[K] {
147+
return func(yield func(K) bool) {
148+
s.mu.RLock()
149+
keys := make([]K, 0, len(s.m))
150+
for k := range s.m {
151+
keys = append(keys, k)
152+
}
153+
s.mu.RUnlock()
154+
155+
sort.Slice(keys, func(i, j int) bool {
156+
return less(keys[i], keys[j])
157+
})
158+
159+
for _, k := range keys {
160+
if !yield(k) {
161+
return
162+
}
163+
}
164+
}
165+
}
166+
167+
// AllSorted returns an iterator over key-value pairs where keys are visited in the order defined by less.
168+
// The keys snapshot occurs under a read lock, then sorting and value lookups happen without holding it.
169+
// Because of that, by the time a key is revisited later, it may have been deleted; such entries are skipped.
170+
func (s *Map[K, V]) AllSorted(less func(a, b K) bool) iter.Seq2[K, V] {
171+
return func(yield func(K, V) bool) {
172+
s.mu.RLock()
173+
keys := make([]K, 0, len(s.m))
174+
for k := range s.m {
175+
keys = append(keys, k)
176+
}
177+
s.mu.RUnlock()
178+
179+
sort.Slice(keys, func(i, j int) bool {
180+
return less(keys[i], keys[j])
181+
})
182+
183+
for _, k := range keys {
184+
s.mu.RLock()
185+
v, ok := s.m[k]
186+
s.mu.RUnlock()
187+
if !ok {
188+
continue
189+
}
190+
if !yield(k, v) {
191+
return
192+
}
193+
}
194+
}
195+
}
196+
197+
// NaturalSort returns an iterator that visits key-value pairs in the natural order of Q (using <).
198+
// This requires Q (K from the Map[Q, V]) to be an ordered type.
199+
func NaturalSort[Q cmp.Ordered, V any](s *Map[Q, V]) iter.Seq2[Q, V] {
200+
return s.AllSorted(func(a, b Q) bool {
201+
return a < b
202+
})
203+
}

smap/smap_test.go

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,126 @@ func TestKeysEarlyTermination(t *testing.T) {
308308
}
309309
}
310310

311+
func TestKeysSorted(t *testing.T) {
312+
m := New[string, int]()
313+
m.Set("c", 3)
314+
m.Set("a", 1)
315+
m.Set("b", 2)
316+
317+
expected := []string{"a", "b", "c"}
318+
idx := 0
319+
for k := range m.KeysSorted(func(a, b string) bool { return a < b }) {
320+
if idx >= len(expected) {
321+
t.Fatalf("received more keys than expected; extra key %q", k)
322+
}
323+
if k != expected[idx] {
324+
t.Fatalf("expected key %q at position %d, got %q", expected[idx], idx, k)
325+
}
326+
idx++
327+
}
328+
if idx != len(expected) {
329+
t.Fatalf("expected %d keys, got %d", len(expected), idx)
330+
}
331+
332+
t.Run("earlyTermination", func(t *testing.T) {
333+
seqMap := New[string, int]()
334+
seqMap.Set("y", 2)
335+
seqMap.Set("x", 1)
336+
seq := seqMap.KeysSorted(func(a, b string) bool { return a < b })
337+
calls := 0
338+
seq(func(string) bool {
339+
calls++
340+
return false
341+
})
342+
if calls != 1 {
343+
t.Fatalf("expected to stop after 1 iteration, got %d", calls)
344+
}
345+
})
346+
347+
t.Run("structKeyCustomSort", func(t *testing.T) {
348+
type compoundKey struct {
349+
label string
350+
priority int
351+
}
352+
353+
s := New[compoundKey, int]()
354+
s.Set(compoundKey{label: "gamma", priority: 2}, 20)
355+
s.Set(compoundKey{label: "alpha", priority: 3}, 30)
356+
s.Set(compoundKey{label: "omega", priority: 1}, 10)
357+
358+
less := func(a, b compoundKey) bool {
359+
return a.priority < b.priority
360+
}
361+
362+
order := make([]compoundKey, 0, 3)
363+
for k := range s.KeysSorted(less) {
364+
order = append(order, k)
365+
}
366+
367+
expectedOrder := []compoundKey{
368+
{label: "omega", priority: 1},
369+
{label: "gamma", priority: 2},
370+
{label: "alpha", priority: 3},
371+
}
372+
373+
if len(order) != len(expectedOrder) {
374+
t.Fatalf("expected %d keys, got %d", len(expectedOrder), len(order))
375+
}
376+
377+
for i, got := range order {
378+
want := expectedOrder[i]
379+
if got != want {
380+
t.Fatalf("at position %d expected %v, got %v", i, want, got)
381+
}
382+
}
383+
})
384+
}
385+
386+
func TestAllSorted(t *testing.T) {
387+
m := New[int, string]()
388+
m.Set(3, "three")
389+
m.Set(1, "one")
390+
m.Set(2, "two")
391+
392+
visited := make([]KV[int, string], 0, 3)
393+
for k, v := range m.AllSorted(func(a, b int) bool { return a < b }) {
394+
visited = append(visited, KV[int, string]{Key: k, Value: v})
395+
if k == 1 {
396+
m.Delete(2)
397+
}
398+
}
399+
400+
expected := []KV[int, string]{
401+
{Key: 1, Value: "one"},
402+
{Key: 3, Value: "three"},
403+
}
404+
405+
if len(visited) != len(expected) {
406+
t.Fatalf("expected %d key/value pairs, got %d", len(expected), len(visited))
407+
}
408+
409+
for i, kv := range expected {
410+
if visited[i] != kv {
411+
t.Fatalf("expected pair %v at position %d, got %v", kv, i, visited[i])
412+
}
413+
}
414+
415+
t.Run("earlyTermination", func(t *testing.T) {
416+
seqMap := New[int, string]()
417+
seqMap.Set(2, "two")
418+
seqMap.Set(1, "one")
419+
seq := seqMap.AllSorted(func(a, b int) bool { return a < b })
420+
calls := 0
421+
seq(func(int, string) bool {
422+
calls++
423+
return false
424+
})
425+
if calls != 1 {
426+
t.Fatalf("expected to stop after 1 iteration, got %d", calls)
427+
}
428+
})
429+
}
430+
311431
func TestIteratorWithDifferentTypes(t *testing.T) {
312432
// Test with different types
313433
m := New[int, string]()
@@ -629,3 +749,121 @@ func DeleteDuringIterationDeadlock(t *testing.T) {
629749
wg.Wait()
630750
t.Errorf("Unexpected no hang")
631751
}
752+
753+
func TestAllNaturalSort(t *testing.T) { //nolint:gocognit // it's a test!
754+
t.Run("int", func(t *testing.T) {
755+
m := New[int, string]()
756+
m.Set(3, "three")
757+
m.Set(1, "one")
758+
m.Set(2, "two")
759+
760+
expected := []int{1, 2, 3}
761+
i := 0
762+
for k, v := range NaturalSort(m) {
763+
if k != expected[i] {
764+
t.Errorf("Expected key %d, got %d", expected[i], k)
765+
}
766+
if i == 0 && v != "one" {
767+
t.Errorf("Expected value 'one', got '%s'", v)
768+
}
769+
i++
770+
}
771+
if i != 3 {
772+
t.Errorf("Expected 3 iterations, got %d", i)
773+
}
774+
})
775+
776+
t.Run("string", func(t *testing.T) {
777+
m := New[string, int]()
778+
m.Set("charlie", 3)
779+
m.Set("alice", 1)
780+
m.Set("bob", 2)
781+
782+
expected := []string{"alice", "bob", "charlie"}
783+
i := 0
784+
for k, v := range NaturalSort(m) {
785+
if k != expected[i] {
786+
t.Errorf("Expected key %s, got %s", expected[i], k)
787+
}
788+
if i == 0 && v != 1 {
789+
t.Errorf("Expected value 1, got %d", v)
790+
}
791+
i++
792+
}
793+
if i != 3 {
794+
t.Errorf("Expected 3 iterations, got %d", i)
795+
}
796+
})
797+
798+
t.Run("keyDeletedDuringIteration", func(t *testing.T) {
799+
m := New[int, string]()
800+
m.Set(3, "three")
801+
m.Set(1, "one")
802+
m.Set(2, "two")
803+
m.Set(4, "four")
804+
805+
visited := make([]KV[int, string], 0, 4)
806+
for k, v := range NaturalSort(m) {
807+
visited = append(visited, KV[int, string]{Key: k, Value: v})
808+
// Delete key 2 after visiting key 1
809+
if k == 1 {
810+
m.Delete(2)
811+
}
812+
}
813+
814+
// Expected: 1, 3, 4 (key 2 should be skipped because it was deleted)
815+
expected := []KV[int, string]{
816+
{Key: 1, Value: "one"},
817+
{Key: 3, Value: "three"},
818+
{Key: 4, Value: "four"},
819+
}
820+
821+
if len(visited) != len(expected) {
822+
t.Fatalf("expected %d key/value pairs, got %d", len(expected), len(visited))
823+
}
824+
825+
for i, kv := range expected {
826+
if visited[i] != kv {
827+
t.Fatalf("expected pair %v at position %d, got %v", kv, i, visited[i])
828+
}
829+
}
830+
})
831+
832+
t.Run("earlyTermination", func(t *testing.T) {
833+
m := New[int, string]()
834+
m.Set(3, "three")
835+
m.Set(1, "one")
836+
m.Set(2, "two")
837+
m.Set(4, "four")
838+
839+
calls := 0
840+
for range NaturalSort(m) {
841+
calls++
842+
if calls == 2 {
843+
break
844+
}
845+
}
846+
847+
if calls != 2 {
848+
t.Fatalf("expected to stop after 2 iterations, got %d", calls)
849+
}
850+
})
851+
852+
t.Run("earlyTerminationViaYield", func(t *testing.T) {
853+
m := New[int, string]()
854+
m.Set(5, "five")
855+
m.Set(1, "one")
856+
m.Set(3, "three")
857+
858+
seq := NaturalSort(m)
859+
calls := 0
860+
seq(func(int, string) bool {
861+
calls++
862+
return false // Stop immediately
863+
})
864+
865+
if calls != 1 {
866+
t.Fatalf("expected to stop after 1 iteration when yield returns false, got %d", calls)
867+
}
868+
})
869+
}

0 commit comments

Comments
 (0)