Skip to content

Commit 3d12ddc

Browse files
committed
fix: add decay map
1 parent fb6f90b commit 3d12ddc

2 files changed

Lines changed: 102 additions & 0 deletions

File tree

decaymap/decaymap.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package decaymap
2+
3+
import (
4+
"sync"
5+
"time"
6+
)
7+
8+
// DecayMap is a coarse-grained paired map that bulk-frees outdated entries.
9+
type DecayMap[K comparable, V any] struct {
10+
mu sync.RWMutex
11+
maps [2]map[K]V
12+
epoch time.Time
13+
lastInsert time.Time
14+
generation uint64
15+
period time.Duration
16+
}
17+
18+
func NewDecayMap[K comparable, V any](epoch time.Time, period time.Duration) *DecayMap[K, V] {
19+
return &DecayMap[K, V]{
20+
maps: [2]map[K]V{make(map[K]V), make(map[K]V)},
21+
epoch: epoch,
22+
generation: 0,
23+
period: period,
24+
}
25+
}
26+
27+
// Attempts to retrieve a value from the store, does not guarantee the value is unexpired.
28+
func (m *DecayMap[K, V]) Get(key K) (res V, ok bool) {
29+
m.mu.RLock()
30+
defer m.mu.RUnlock()
31+
res, ok = m.maps[0][key]
32+
if ok {
33+
return
34+
}
35+
res, ok = m.maps[1][key]
36+
return
37+
}
38+
39+
// Sets a value in the store, overwriting the existing value if exists.
40+
//
41+
// Inserting values in non monotonic order is a no op.
42+
func (m *DecayMap[K, V]) Set(now time.Time, key K, value V) {
43+
m.mu.Lock()
44+
defer m.mu.Unlock()
45+
if now.Before(m.lastInsert) {
46+
return
47+
}
48+
m.lastInsert = now
49+
curGen := uint64(now.Sub(m.epoch) / m.period)
50+
genDelta := curGen - m.generation
51+
if genDelta >= 2 {
52+
// both are outdated, clear both
53+
m.maps[0] = make(map[K]V)
54+
m.maps[1] = make(map[K]V)
55+
m.generation = curGen
56+
} else if genDelta == 1 {
57+
// one is outdated, clear the outdated one
58+
m.maps[curGen&1] = make(map[K]V)
59+
m.generation = curGen
60+
}
61+
delete(m.maps[(curGen&1)^1], key)
62+
m.maps[curGen&1][key] = value
63+
}

decaymap/decaymap_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
2+
package decaymap
3+
4+
import (
5+
"fmt"
6+
"testing"
7+
"time"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestDecayMap2(t *testing.T) {
13+
epoch := time.Now()
14+
dm := NewDecayMap[string, string](epoch, 10*time.Millisecond)
15+
16+
now := epoch
17+
for ts := 0; ts < 100; ts++ {
18+
dm.Set(now, fmt.Sprintf("key%d", ts), fmt.Sprintf("value%d", ts))
19+
for backts := 0; backts < ts; backts++ {
20+
res, ok := dm.Get(fmt.Sprintf("key%d", backts))
21+
if ts-backts <= 10 {
22+
assert.True(t, ok)
23+
assert.Equal(t, fmt.Sprintf("value%d", backts), res)
24+
} else if ts-backts >= 20 {
25+
assert.False(t, ok)
26+
assert.Equal(t, "", res)
27+
}
28+
}
29+
now = now.Add(1 * time.Millisecond)
30+
}
31+
32+
now = now.Add(20 * time.Millisecond)
33+
dm.Set(now, "dummy", "dummy") // rachet internal state
34+
for ts := 0; ts < 100; ts++ {
35+
res, ok := dm.Get(fmt.Sprintf("key%d", ts))
36+
assert.False(t, ok)
37+
assert.Equal(t, "", res)
38+
}
39+
}

0 commit comments

Comments
 (0)