Skip to content

Commit 4586688

Browse files
authored
Merge pull request #16 from mailsac/fix-optimize
feat: optimize for occasional slowness under load
2 parents 180d267 + 99ae2c3 commit 4586688

File tree

5 files changed

+90
-41
lines changed

5 files changed

+90
-41
lines changed

server/restserver.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,7 @@ func PutHandler(s *Server, w http.ResponseWriter, r *http.Request) {
6161
json.NewEncoder(w).Encode(resp)
6262
return
6363
}
64-
s.store.Put(namespace, key)
65-
count := s.store.Count(namespace, key)
64+
count := s.store.Put(namespace, key)
6665
resp := CountResponse{Count: count}
6766
json.NewEncoder(w).Encode(resp)
6867
}

store/store.go

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ func (m *Metrics) ListenAndServe(promHostPort string) error {
4343
type Store struct {
4444
sync.Mutex // mutext locks namespaces
4545
namespaces *hashmap.Map
46+
namespaceSnapshot []string
4647
expireAfterSecs int64
4748
cleanupServiceEnabled bool
4849
LastMetrics *Metrics
@@ -78,8 +79,9 @@ func NewStore(expireAfterSecs int64) *Store {
7879
registry.MustRegister(maxNamespacesDenomGauge, namespacesTotalCount, namespacesGarbageCollected, keysRemainingInGCNamespaces, countTotalRemainingInGCNamespaces, gcPauseTime)
7980

8081
s := &Store{
81-
expireAfterSecs: expireAfterSecs,
82-
namespaces: hashmap.New(),
82+
expireAfterSecs: expireAfterSecs,
83+
namespaces: hashmap.New(),
84+
lastGCdNamespaces: map[string]bool{},
8385
LastMetrics: &Metrics{
8486
registry: registry,
8587
maxNamespacesDenom: maxNamespacesDenomGauge,
@@ -191,24 +193,27 @@ func (s *Store) runCleanup() []string {
191193
for _, key := range keys {
192194
keyList = append(keyList, key.(string))
193195
}
196+
197+
s.Lock()
198+
s.namespaceSnapshot = append([]string(nil), keyList...)
199+
s.Unlock()
194200
return keyList
195201
}
196202

197-
func (s *Store) Put(ns, entryKey string) {
203+
func (s *Store) Put(ns, entryKey string) int {
198204
var subtree *tree.Tree
199205
s.Lock()
200206
subtreeI, found := s.namespaces.Get(ns)
201-
s.Unlock()
202207
if !found {
203208
subtree = tree.NewTree(s.expireAfterSecs)
204-
s.Lock()
205209
s.namespaces.Put(ns, subtree)
206-
s.Unlock()
210+
s.namespaceSnapshot = append(s.namespaceSnapshot, ns)
207211
} else {
208212
subtree = subtreeI.(*tree.Tree)
209213
}
214+
s.Unlock()
210215

211-
subtree.Put(entryKey)
216+
return subtree.Put(entryKey)
212217
}
213218

214219
// Count returns the number of entries at a namespace and key, returning
@@ -227,8 +232,12 @@ func (s *Store) Count(ns, entryKey string) int {
227232

228233
// Namespaces returns the approximate current namespaces list
229234
func (s *Store) Namespaces() []string {
230-
keys := s.runCleanup()
231-
return keys
235+
s.Lock()
236+
defer s.Unlock()
237+
if len(s.namespaceSnapshot) == 0 {
238+
return []string{}
239+
}
240+
return append([]string(nil), s.namespaceSnapshot...)
232241
}
233242

234243
// CountEntries returns the count of all entries for the entire namespace.

store/store_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package store
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestStore_NamespacesUsesSnapshot(t *testing.T) {
10+
s := NewStore(60)
11+
s.DisableCleanup()
12+
13+
assert.Equal(t, []string{}, s.Namespaces())
14+
15+
assert.Equal(t, 1, s.Put("namespace0", "key0"))
16+
assert.Equal(t, 2, s.Put("namespace0", "key0"))
17+
assert.Equal(t, 1, s.Put("namespace1", "key1"))
18+
19+
assert.ElementsMatch(t, []string{"namespace0", "namespace1"}, s.Namespaces())
20+
}

store/tree/tree.go

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import (
1010

1111
type ExpireAtSecs int64
1212

13+
type entry struct {
14+
expiresAt []int64
15+
liveCount int
16+
}
17+
1318
// Tree is a a thread-safe data structure for tracking expirable items. It automatically expires old entries and keys.
1419
// It does not garbage collect. Items are only expired when interacting with the data structure.
1520
type Tree struct {
@@ -60,20 +65,24 @@ func (n *Tree) Count(entryKey string) int {
6065
n.Lock()
6166
defer n.Unlock()
6267

63-
datesSecs := n.getAndCleanupUnsafe(entryKey)
64-
if datesSecs == nil {
68+
item, found := n.getAndCleanupUnsafe(entryKey)
69+
if !found {
70+
return 0
71+
}
72+
73+
if item.liveCount == 0 {
74+
n.tree.Remove(entryKey)
6575
return 0
6676
}
6777

68-
if len(*datesSecs) == 0 {
78+
item = removeExpired(item)
79+
if item.liveCount == 0 {
6980
n.tree.Remove(entryKey)
7081
return 0
7182
}
7283

73-
datesSecs = removeExpired(datesSecs)
74-
count := len(*datesSecs)
75-
n.tree.Put(entryKey, *datesSecs)
76-
return count
84+
n.tree.Put(entryKey, item)
85+
return item.liveCount
7786
}
7887

7988
// KeyMatch crawls the subtree to return keys starting with the `keyPattern` string.
@@ -110,47 +119,52 @@ func (n *Tree) KeyMatch(keyPattern string) []string {
110119
return out
111120
}
112121

113-
func (n *Tree) Put(entryKey string) {
122+
func (n *Tree) Put(entryKey string) int {
114123
n.Lock()
115124
defer n.Unlock()
116125

117-
datesSecs := n.getAndCleanupUnsafe(entryKey)
118-
if datesSecs == nil {
119-
datesSecs = &[]int64{}
126+
item, found := n.getAndCleanupUnsafe(entryKey)
127+
if !found {
128+
item = entry{}
120129
}
121-
datesSecs = removeExpired(datesSecs)
130+
item = removeExpired(item)
122131
secs := time.Now().Unix()
123-
nextDatesSecs := append(*datesSecs, secs+n.defaultExpireAfterSecs)
124-
n.tree.Put(entryKey, nextDatesSecs)
132+
item.expiresAt = append(item.expiresAt, secs+n.defaultExpireAfterSecs)
133+
item.liveCount = len(item.expiresAt)
134+
n.tree.Put(entryKey, item)
135+
return item.liveCount
125136
}
126137

127138
// getAndCleanupUnsafe does not lock the mutex, so it can be used inside a lock
128-
func (n *Tree) getAndCleanupUnsafe(entryKey string) *[]int64 {
139+
func (n *Tree) getAndCleanupUnsafe(entryKey string) (entry, bool) {
129140
val, found := n.tree.Get(entryKey)
130141
if !found {
131-
return nil
142+
return entry{}, false
132143
}
133-
dates := val.([]int64)
134-
if len(dates) == 0 {
144+
item := val.(entry)
145+
if item.liveCount == 0 {
135146
// cleanup empty entry
136147
n.tree.Remove(entryKey)
137-
return nil
148+
return entry{}, false
138149
}
139-
return &dates // not extra copy
150+
return item, true
140151
}
141152

142-
func removeExpired(datesSecs *[]int64) *[]int64 {
143-
if len(*datesSecs) == 0 {
144-
return datesSecs
153+
func removeExpired(item entry) entry {
154+
if len(item.expiresAt) == 0 {
155+
item.liveCount = 0
156+
return item
145157
}
146158
currentTime := time.Now().Unix()
147159
var out []int64
148160
// TODO: these are already sorted, so we can discard earlier entries
149-
for _, removeAt := range *datesSecs {
161+
for _, removeAt := range item.expiresAt {
150162
if removeAt > currentTime {
151163
// KEEP - not expired
152164
out = append(out, removeAt)
153165
}
154166
}
155-
return &out
167+
item.expiresAt = out
168+
item.liveCount = len(out)
169+
return item
156170
}

store/tree/tree_test.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ func TestTree_removeExpired(t *testing.T) {
1818
time.Now().Unix() - 1, // expired
1919
}
2020

21-
result := removeExpired(&entries)
22-
assert.Equal(t, 2, len(*result))
23-
assert.Equal(t, (*result)[0], keep1)
24-
assert.Equal(t, (*result)[1], keep2)
21+
result := removeExpired(entry{expiresAt: entries, liveCount: len(entries)})
22+
assert.Equal(t, 2, result.liveCount)
23+
assert.Equal(t, result.expiresAt[0], keep1)
24+
assert.Equal(t, result.expiresAt[1], keep2)
2525
}
2626

2727
func TestTree_Count(t *testing.T) {
@@ -78,6 +78,13 @@ func TestTree_Count(t *testing.T) {
7878
assert.Equal(t, 0, len(keys), "Keys() should expire keys")
7979
assert.Equal(t, 0, entryCount, "Keys() should expire entries")
8080
})
81+
t.Run("put returns live count without needing a second scan", func(t *testing.T) {
82+
tr := NewTree(2)
83+
assert.Equal(t, 1, tr.Put("willy"))
84+
assert.Equal(t, 2, tr.Put("willy"))
85+
assert.Equal(t, 1, tr.Put("other"))
86+
assert.Equal(t, 2, tr.Count("willy"))
87+
})
8188
}
8289

8390
func TestTree_KeyMatch(t *testing.T) {
@@ -125,7 +132,7 @@ func TestTree_KeyMatch(t *testing.T) {
125132
t.Run("it does not return the key when all its values are expired", func(t *testing.T) {
126133
tr := NewTree(1)
127134
tr.Put("a")
128-
tr.tree.Put("a", []string{})
135+
tr.tree.Put("a", entry{})
129136

130137
tr.Put("cdbe")
131138
tr.Put("cd:aa")

0 commit comments

Comments
 (0)