Skip to content

Commit 081b394

Browse files
authored
[FSSDK-11151] Add remove method to lru cache (#400)
* add remove method to lru cache * place remove mthod into separate cache CacheWithRemove that extends Cache interface * exclude from linting * lint * move cache dir to pkg root * cache file moved to pkg root
1 parent 0e57381 commit 081b394

File tree

7 files changed

+151
-10
lines changed

7 files changed

+151
-10
lines changed

pkg/odp/cache/lru_cache.go renamed to pkg/cache/lru_cache.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2022, Optimizely, Inc. and contributors *
2+
* Copyright 2022-2025, Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
@@ -30,6 +30,13 @@ type Cache interface {
3030
Reset()
3131
}
3232

33+
// CacheWithRemove extends the Cache interface with removal capability
34+
// nolint:golint // Keeping name consistent with other language SDKs
35+
type CacheWithRemove interface {
36+
Cache
37+
Remove(key string)
38+
}
39+
3340
type cacheElement struct {
3441
data interface{}
3542
time time.Time
@@ -102,6 +109,19 @@ func (l *LRUCache) Reset() {
102109
l.items = make(map[string]*cacheElement)
103110
}
104111

112+
// Remove deletes an element from the cache by key
113+
func (l *LRUCache) Remove(key string) {
114+
if l.maxSize <= 0 {
115+
return
116+
}
117+
l.lock.Lock()
118+
defer l.lock.Unlock()
119+
if item, ok := l.items[key]; ok {
120+
l.queue.Remove(item.keyPtr)
121+
delete(l.items, key)
122+
}
123+
}
124+
105125
func (l *LRUCache) isValid(e *cacheElement) bool {
106126
if l.timeout <= 0 {
107127
return true

pkg/odp/cache/lru_cache_test.go renamed to pkg/cache/lru_cache_test.go

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2022, Optimizely, Inc. and contributors *
2+
* Copyright 2022-2025, Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
@@ -204,3 +204,122 @@ func TestTimeout(t *testing.T) {
204204
assert.Equal(t, 200, cache2.Lookup("2"))
205205
assert.Equal(t, 300, cache2.Lookup("3"))
206206
}
207+
208+
func TestRemove(t *testing.T) {
209+
// Test removing an existing key
210+
t.Run("Remove existing key", func(t *testing.T) {
211+
cache := NewLRUCache(3, 1000*time.Second)
212+
213+
// Add items to cache
214+
cache.Save("1", 100)
215+
cache.Save("2", 200)
216+
cache.Save("3", 300)
217+
218+
// Verify items exist
219+
assert.Equal(t, 100, cache.Lookup("1"))
220+
assert.Equal(t, 200, cache.Lookup("2"))
221+
assert.Equal(t, 300, cache.Lookup("3"))
222+
assert.Equal(t, 3, cache.queue.Len())
223+
assert.Equal(t, 3, len(cache.items))
224+
225+
// Remove an item
226+
cache.Remove("2")
227+
228+
// Verify item was removed
229+
assert.Equal(t, 100, cache.Lookup("1"))
230+
assert.Nil(t, cache.Lookup("2"))
231+
assert.Equal(t, 300, cache.Lookup("3"))
232+
assert.Equal(t, 2, cache.queue.Len())
233+
assert.Equal(t, 2, len(cache.items))
234+
})
235+
236+
// Test removing a non-existent key
237+
t.Run("Remove non-existent key", func(t *testing.T) {
238+
cache := NewLRUCache(3, 1000*time.Second)
239+
240+
// Add items to cache
241+
cache.Save("1", 100)
242+
cache.Save("2", 200)
243+
244+
// Remove a non-existent key
245+
cache.Remove("3")
246+
247+
// Verify state remains unchanged
248+
assert.Equal(t, 100, cache.Lookup("1"))
249+
assert.Equal(t, 200, cache.Lookup("2"))
250+
assert.Equal(t, 2, cache.queue.Len())
251+
assert.Equal(t, 2, len(cache.items))
252+
})
253+
254+
// Test removing from a zero-sized cache
255+
t.Run("Remove from zero-sized cache", func(t *testing.T) {
256+
cache := NewLRUCache(0, 1000*time.Second)
257+
258+
// Try to add and remove items
259+
cache.Save("1", 100)
260+
cache.Remove("1")
261+
262+
// Verify nothing happened
263+
assert.Nil(t, cache.Lookup("1"))
264+
assert.Equal(t, 0, cache.queue.Len())
265+
assert.Equal(t, 0, len(cache.items))
266+
})
267+
268+
// Test removing and then adding back
269+
t.Run("Remove and add back", func(t *testing.T) {
270+
cache := NewLRUCache(3, 1000*time.Second)
271+
272+
// Add items to cache
273+
cache.Save("1", 100)
274+
cache.Save("2", 200)
275+
cache.Save("3", 300)
276+
277+
// Remove an item
278+
cache.Remove("2")
279+
280+
// Add it back with a different value
281+
cache.Save("2", 201)
282+
283+
// Verify item was added back
284+
assert.Equal(t, 100, cache.Lookup("1"))
285+
assert.Equal(t, 201, cache.Lookup("2"))
286+
assert.Equal(t, 300, cache.Lookup("3"))
287+
assert.Equal(t, 3, cache.queue.Len())
288+
assert.Equal(t, 3, len(cache.items))
289+
})
290+
291+
// Test thread safety of Remove
292+
t.Run("Thread safety", func(t *testing.T) {
293+
maxSize := 100
294+
cache := NewLRUCache(maxSize, 1000*time.Second)
295+
wg := sync.WaitGroup{}
296+
297+
// Add entries
298+
for i := 1; i <= maxSize; i++ {
299+
cache.Save(fmt.Sprintf("%d", i), i*100)
300+
}
301+
302+
// Concurrently remove half the entries
303+
wg.Add(maxSize / 2)
304+
for i := 1; i <= maxSize/2; i++ {
305+
go func(k int) {
306+
defer wg.Done()
307+
cache.Remove(fmt.Sprintf("%d", k))
308+
}(i)
309+
}
310+
wg.Wait()
311+
312+
// Verify first half is removed, second half remains
313+
for i := 1; i <= maxSize; i++ {
314+
if i <= maxSize/2 {
315+
assert.Nil(t, cache.Lookup(fmt.Sprintf("%d", i)))
316+
} else {
317+
assert.Equal(t, i*100, cache.Lookup(fmt.Sprintf("%d", i)))
318+
}
319+
}
320+
321+
// Verify cache size
322+
assert.Equal(t, maxSize/2, cache.queue.Len())
323+
assert.Equal(t, maxSize/2, len(cache.items))
324+
})
325+
}

pkg/client/factory_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2019-2020,2022,2024 Optimizely, Inc. and contributors *
2+
* Copyright 2019-2020,2022,2025 Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
@@ -27,14 +27,14 @@ import (
2727
"github.com/stretchr/testify/assert"
2828
"github.com/stretchr/testify/mock"
2929

30+
"github.com/optimizely/go-sdk/v2/pkg/cache"
3031
"github.com/optimizely/go-sdk/v2/pkg/config"
3132
"github.com/optimizely/go-sdk/v2/pkg/decide"
3233
"github.com/optimizely/go-sdk/v2/pkg/decision"
3334
"github.com/optimizely/go-sdk/v2/pkg/event"
3435
"github.com/optimizely/go-sdk/v2/pkg/metrics"
3536
"github.com/optimizely/go-sdk/v2/pkg/notification"
3637
"github.com/optimizely/go-sdk/v2/pkg/odp"
37-
"github.com/optimizely/go-sdk/v2/pkg/odp/cache"
3838
pkgOdpEvent "github.com/optimizely/go-sdk/v2/pkg/odp/event"
3939
pkgOdpSegment "github.com/optimizely/go-sdk/v2/pkg/odp/segment"
4040
pkgOdpUtils "github.com/optimizely/go-sdk/v2/pkg/odp/utils"

pkg/odp/odp_manager.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2022, Optimizely, Inc. and contributors *
2+
* Copyright 2022-2025, Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
@@ -21,8 +21,8 @@ import (
2121
"errors"
2222
"time"
2323

24+
"github.com/optimizely/go-sdk/v2/pkg/cache"
2425
"github.com/optimizely/go-sdk/v2/pkg/logging"
25-
"github.com/optimizely/go-sdk/v2/pkg/odp/cache"
2626
"github.com/optimizely/go-sdk/v2/pkg/odp/config"
2727
"github.com/optimizely/go-sdk/v2/pkg/odp/event"
2828
"github.com/optimizely/go-sdk/v2/pkg/odp/segment"

pkg/odp/odp_manager_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2022-2023, Optimizely, Inc. and contributors *
2+
* Copyright 2022-2025, Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
@@ -23,7 +23,7 @@ import (
2323
"testing"
2424
"time"
2525

26-
"github.com/optimizely/go-sdk/v2/pkg/odp/cache"
26+
"github.com/optimizely/go-sdk/v2/pkg/cache"
2727
"github.com/optimizely/go-sdk/v2/pkg/odp/config"
2828
"github.com/optimizely/go-sdk/v2/pkg/odp/event"
2929
"github.com/optimizely/go-sdk/v2/pkg/odp/segment"

pkg/odp/segment/segment_manager.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2022, Optimizely, Inc. and contributors *
2+
* Copyright 2022-2025, Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
@@ -21,7 +21,7 @@ import (
2121
"fmt"
2222
"time"
2323

24-
"github.com/optimizely/go-sdk/v2/pkg/odp/cache"
24+
"github.com/optimizely/go-sdk/v2/pkg/cache"
2525
"github.com/optimizely/go-sdk/v2/pkg/odp/utils"
2626
)
2727

pkg/odp/segment/segment_manager_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,5 @@ func (l *TestCache) Lookup(key string) interface{} {
150150
}
151151
func (l *TestCache) Reset() {
152152
}
153+
func (l *TestCache) Remove(key string) {
154+
}

0 commit comments

Comments
 (0)