Skip to content
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
15 changes: 15 additions & 0 deletions cache/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package cache

import "time"

// Cache interface defines the methods for a cache implementation.
type Cache[K comparable] interface {
// Get retrieves a value from the cache by key.
Get(key K) (value interface{}, found bool)

// Put adds a value to the cache with the specified key and expiration time.
Put(key K, value interface{}, expiration time.Duration)

// Delete removes a value from the cache by key.
Delete(key K)
}
91 changes: 91 additions & 0 deletions cache/map.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package cache

import (
"context"
"sync"
"time"
)

const (
// DefaultSweepInterval Cache default sweep interval
DefaultSweepInterval = time.Second * 3
)

// memoryCache memory cache, support time expired
type memoryCache struct {
data interface{}
cacheDuration time.Duration
startTime time.Time
}

// NewMemoryCache new memory cache instance
func NewMemoryCache(data interface{}, cacheDuration time.Duration) *memoryCache {
mc := &memoryCache{data: data, cacheDuration: cacheDuration, startTime: time.Now()}
return mc
}

// IsExpired whether the cache data expires
func (m *memoryCache) IsExpired() bool {
if m.cacheDuration <= 0 {
return false
}
return time.Now().After(m.startTime.Add(m.cacheDuration))
}

// GetData get cache data
func (m *memoryCache) GetData() interface{} {
return m.data
}

// MemoryCacheStore memory cache store
type MemoryCacheStore[K comparable] struct {
store sync.Map
}

// NewMemoryCacheStore memory cache store
func NewMemoryCacheStore[K comparable](ctx context.Context) *MemoryCacheStore[K] {
mcs := &MemoryCacheStore[K]{
store: sync.Map{},
}
go mcs.run(ctx)
return mcs
}

// run start a goroutine to clear expired cache data
func (m *MemoryCacheStore[K]) run(ctx context.Context) {
ticker := time.NewTicker(DefaultSweepInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
m.store.Range(func(key, value interface{}) bool {
if value.(*memoryCache).IsExpired() {
m.store.CompareAndDelete(key, value)
}
return true
})
}
}
}

// Get cache data from store, if cache data is expired, return nil
func (m *MemoryCacheStore[K]) Get(key K) (value interface{}, found bool) {
mc, ok := m.store.Load(key)
if ok && !mc.(*memoryCache).IsExpired() {
return mc.(*memoryCache).GetData(), true
}
return nil, false
}

// Put cache data, if cacheDuration>0, store will clear data after timeout.
func (m *MemoryCacheStore[K]) Put(key K, value interface{}, cacheDuration time.Duration) {
mc := NewMemoryCache(value, cacheDuration)
m.store.Store(key, mc)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
}

// Delete cache data from store
func (m *MemoryCacheStore[K]) Delete(key K) {
m.store.Delete(key)
}
95 changes: 95 additions & 0 deletions cache/map_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package cache

/*
Copyright 2021 The KubeVela Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import (
"context"
"strconv"
"testing"
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestCache(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Cache Suite")
}

var _ = Describe("Test cache utils", func() {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
It("should return false for IsExpired()", func() {
c := NewMemoryCache("test", 10*time.Hour)
Expect(c.IsExpired()).Should(BeFalse())
})

It("test cache store", func() {
store := NewMemoryCacheStore[string](context.TODO())
store.Put("test", "test data", time.Second*2)
store.Put("test2", "test data", 0)
store.Put("test3", "test data", -1)
time.Sleep(3 * time.Second)

@cubic-dev-ai cubic-dev-ai Bot Jul 11, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The 3-second time.Sleep at line 40 adds a hard delay to every test run. Consider reducing the TTL to e.g. 100ms and sleeping only ~150ms to test expiry, or testing the expiry behavior directly via memoryCache.IsExpired() without wall-clock waiting.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cache/map_test.go, line 40:

<comment>The 3-second `time.Sleep` at line 40 adds a hard delay to every test run. Consider reducing the TTL to e.g. 100ms and sleeping only ~150ms to test expiry, or testing the expiry behavior directly via `memoryCache.IsExpired()` without wall-clock waiting.</comment>

<file context>
@@ -0,0 +1,78 @@
+		store.Put("test", "test data", time.Second*2)
+		store.Put("test2", "test data", 0)
+		store.Put("test3", "test data", -1)
+		time.Sleep(3 * time.Second)
+		Expect(store.Get("test")).Should(BeNil())
+		Expect(store.Get("test2")).Should(Equal("test data"))
</file context>
Fix with cubic

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The delay is intentional in this test because the goal is to validate the end-to-end cache behavior rather than just the expiration check. If we only test IsExpired() directly, we wouldn't be exercising the complete cache workflow that this test is intended to cover, so I'd prefer to keep this.

value, found := store.Get("test")
Expect(value).Should(BeNil())
Expect(found).Should(BeFalse())

value, found = store.Get("test2")
Expect(value).Should(Equal("test data"))
Expect(found).Should(BeTrue())

value, found = store.Get("test3")
Expect(value).Should(Equal("test data"))
Expect(found).Should(BeTrue())
})

It("test cache store delete key", func() {
store := NewMemoryCacheStore[string](context.TODO())
store.Put("test", "test data", time.Minute*2)
store.Delete("test")
value, found := store.Get("test")
Expect(value).Should(BeNil())
Expect(found).Should(BeFalse())
})

It("test cache store with multiple keys", func() {
store := NewMemoryCacheStore[string](context.TODO())
for i := 0; i < 100; i++ {
key := "key-" + strconv.Itoa(i)
store.Put(key, i, 0)
}

for i := 0; i < 100; i++ {
key := "key-" + strconv.Itoa(i)
value, found := store.Get(key)
Expect(found).Should(BeTrue())
Expect(value).Should(Equal(i))
}
})

It("test cache store overwrite value", func() {
store := NewMemoryCacheStore[string](context.TODO())
store.Put("rw", "v1", time.Second)
value, found := store.Get("rw")
Expect(found).Should(BeTrue())
Expect(value).Should(Equal("v1"))

store.Put("rw", "v2", time.Second)
value, found = store.Get("rw")
Expect(found).Should(BeTrue())
Expect(value).Should(Equal("v2"))
})
})
Loading