Skip to content

[FSSDK-11151] Add remove method to lru cache #400

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion pkg/odp/cache/lru_cache.go → pkg/cache/lru_cache.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/****************************************************************************
* Copyright 2022, Optimizely, Inc. and contributors *
* Copyright 2022-2025, Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
Expand Down Expand Up @@ -30,6 +30,13 @@ type Cache interface {
Reset()
}

// CacheWithRemove extends the Cache interface with removal capability
// nolint:golint // Keeping name consistent with other language SDKs
type CacheWithRemove interface {
Cache
Remove(key string)
}

type cacheElement struct {
data interface{}
time time.Time
Expand Down Expand Up @@ -102,6 +109,19 @@ func (l *LRUCache) Reset() {
l.items = make(map[string]*cacheElement)
}

// Remove deletes an element from the cache by key
func (l *LRUCache) Remove(key string) {
if l.maxSize <= 0 {
return
}
l.lock.Lock()
defer l.lock.Unlock()
if item, ok := l.items[key]; ok {
l.queue.Remove(item.keyPtr)
delete(l.items, key)
}
}

func (l *LRUCache) isValid(e *cacheElement) bool {
if l.timeout <= 0 {
return true
Expand Down
121 changes: 120 additions & 1 deletion pkg/odp/cache/lru_cache_test.go → pkg/cache/lru_cache_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/****************************************************************************
* Copyright 2022, Optimizely, Inc. and contributors *
* Copyright 2022-2025, Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
Expand Down Expand Up @@ -204,3 +204,122 @@ func TestTimeout(t *testing.T) {
assert.Equal(t, 200, cache2.Lookup("2"))
assert.Equal(t, 300, cache2.Lookup("3"))
}

func TestRemove(t *testing.T) {
// Test removing an existing key
t.Run("Remove existing key", func(t *testing.T) {
cache := NewLRUCache(3, 1000*time.Second)

// Add items to cache
cache.Save("1", 100)
cache.Save("2", 200)
cache.Save("3", 300)

// Verify items exist
assert.Equal(t, 100, cache.Lookup("1"))
assert.Equal(t, 200, cache.Lookup("2"))
assert.Equal(t, 300, cache.Lookup("3"))
assert.Equal(t, 3, cache.queue.Len())
assert.Equal(t, 3, len(cache.items))

// Remove an item
cache.Remove("2")

// Verify item was removed
assert.Equal(t, 100, cache.Lookup("1"))
assert.Nil(t, cache.Lookup("2"))
assert.Equal(t, 300, cache.Lookup("3"))
assert.Equal(t, 2, cache.queue.Len())
assert.Equal(t, 2, len(cache.items))
})

// Test removing a non-existent key
t.Run("Remove non-existent key", func(t *testing.T) {
cache := NewLRUCache(3, 1000*time.Second)

// Add items to cache
cache.Save("1", 100)
cache.Save("2", 200)

// Remove a non-existent key
cache.Remove("3")

// Verify state remains unchanged
assert.Equal(t, 100, cache.Lookup("1"))
assert.Equal(t, 200, cache.Lookup("2"))
assert.Equal(t, 2, cache.queue.Len())
assert.Equal(t, 2, len(cache.items))
})

// Test removing from a zero-sized cache
t.Run("Remove from zero-sized cache", func(t *testing.T) {
cache := NewLRUCache(0, 1000*time.Second)

// Try to add and remove items
cache.Save("1", 100)
cache.Remove("1")

// Verify nothing happened
assert.Nil(t, cache.Lookup("1"))
assert.Equal(t, 0, cache.queue.Len())
assert.Equal(t, 0, len(cache.items))
})

// Test removing and then adding back
t.Run("Remove and add back", func(t *testing.T) {
cache := NewLRUCache(3, 1000*time.Second)

// Add items to cache
cache.Save("1", 100)
cache.Save("2", 200)
cache.Save("3", 300)

// Remove an item
cache.Remove("2")

// Add it back with a different value
cache.Save("2", 201)

// Verify item was added back
assert.Equal(t, 100, cache.Lookup("1"))
assert.Equal(t, 201, cache.Lookup("2"))
assert.Equal(t, 300, cache.Lookup("3"))
assert.Equal(t, 3, cache.queue.Len())
assert.Equal(t, 3, len(cache.items))
})

// Test thread safety of Remove
t.Run("Thread safety", func(t *testing.T) {
maxSize := 100
cache := NewLRUCache(maxSize, 1000*time.Second)
wg := sync.WaitGroup{}

// Add entries
for i := 1; i <= maxSize; i++ {
cache.Save(fmt.Sprintf("%d", i), i*100)
}

// Concurrently remove half the entries
wg.Add(maxSize / 2)
for i := 1; i <= maxSize/2; i++ {
go func(k int) {
defer wg.Done()
cache.Remove(fmt.Sprintf("%d", k))
}(i)
}
wg.Wait()

// Verify first half is removed, second half remains
for i := 1; i <= maxSize; i++ {
if i <= maxSize/2 {
assert.Nil(t, cache.Lookup(fmt.Sprintf("%d", i)))
} else {
assert.Equal(t, i*100, cache.Lookup(fmt.Sprintf("%d", i)))
}
}

// Verify cache size
assert.Equal(t, maxSize/2, cache.queue.Len())
assert.Equal(t, maxSize/2, len(cache.items))
})
}
4 changes: 2 additions & 2 deletions pkg/client/factory_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/****************************************************************************
* Copyright 2019-2020,2022,2024 Optimizely, Inc. and contributors *
* Copyright 2019-2020,2022,2025 Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
Expand Down Expand Up @@ -27,14 +27,14 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"

"github.com/optimizely/go-sdk/v2/pkg/cache"
"github.com/optimizely/go-sdk/v2/pkg/config"
"github.com/optimizely/go-sdk/v2/pkg/decide"
"github.com/optimizely/go-sdk/v2/pkg/decision"
"github.com/optimizely/go-sdk/v2/pkg/event"
"github.com/optimizely/go-sdk/v2/pkg/metrics"
"github.com/optimizely/go-sdk/v2/pkg/notification"
"github.com/optimizely/go-sdk/v2/pkg/odp"
"github.com/optimizely/go-sdk/v2/pkg/odp/cache"
pkgOdpEvent "github.com/optimizely/go-sdk/v2/pkg/odp/event"
pkgOdpSegment "github.com/optimizely/go-sdk/v2/pkg/odp/segment"
pkgOdpUtils "github.com/optimizely/go-sdk/v2/pkg/odp/utils"
Expand Down
4 changes: 2 additions & 2 deletions pkg/odp/odp_manager.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/****************************************************************************
* Copyright 2022, Optimizely, Inc. and contributors *
* Copyright 2022-2025, Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
Expand All @@ -21,8 +21,8 @@ import (
"errors"
"time"

"github.com/optimizely/go-sdk/v2/pkg/cache"
"github.com/optimizely/go-sdk/v2/pkg/logging"
"github.com/optimizely/go-sdk/v2/pkg/odp/cache"
"github.com/optimizely/go-sdk/v2/pkg/odp/config"
"github.com/optimizely/go-sdk/v2/pkg/odp/event"
"github.com/optimizely/go-sdk/v2/pkg/odp/segment"
Expand Down
4 changes: 2 additions & 2 deletions pkg/odp/odp_manager_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/****************************************************************************
* Copyright 2022-2023, Optimizely, Inc. and contributors *
* Copyright 2022-2025, Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
Expand All @@ -23,7 +23,7 @@ import (
"testing"
"time"

"github.com/optimizely/go-sdk/v2/pkg/odp/cache"
"github.com/optimizely/go-sdk/v2/pkg/cache"
"github.com/optimizely/go-sdk/v2/pkg/odp/config"
"github.com/optimizely/go-sdk/v2/pkg/odp/event"
"github.com/optimizely/go-sdk/v2/pkg/odp/segment"
Expand Down
4 changes: 2 additions & 2 deletions pkg/odp/segment/segment_manager.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/****************************************************************************
* Copyright 2022, Optimizely, Inc. and contributors *
* Copyright 2022-2025, Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
Expand All @@ -21,7 +21,7 @@ import (
"fmt"
"time"

"github.com/optimizely/go-sdk/v2/pkg/odp/cache"
"github.com/optimizely/go-sdk/v2/pkg/cache"
"github.com/optimizely/go-sdk/v2/pkg/odp/utils"
)

Expand Down
2 changes: 2 additions & 0 deletions pkg/odp/segment/segment_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,5 @@ func (l *TestCache) Lookup(key string) interface{} {
}
func (l *TestCache) Reset() {
}
func (l *TestCache) Remove(key string) {
}