Skip to content

Commit 0d998e1

Browse files
[API] [Misc]: Support LRU cache with TTL for prefix cache indexer (#905)
* feat: add lru cache to support prefix cache indexer Signed-off-by: vie-serendipity <[email protected]>
1 parent cd9da48 commit 0d998e1

File tree

8 files changed

+395
-81
lines changed

8 files changed

+395
-81
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ require (
2525
github.com/stretchr/testify v1.9.0
2626
google.golang.org/grpc v1.65.0
2727
k8s.io/api v0.31.2
28+
k8s.io/apiextensions-apiserver v0.31.2
2829
k8s.io/apimachinery v0.31.2
2930
k8s.io/client-go v0.31.2
3031
k8s.io/code-generator v0.31.2
@@ -107,7 +108,6 @@ require (
107108
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
108109
gopkg.in/yaml.v2 v2.4.0 // indirect
109110
gopkg.in/yaml.v3 v3.0.1 // indirect
110-
k8s.io/apiextensions-apiserver v0.31.2 // indirect
111111
k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70 // indirect
112112
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
113113
sigs.k8s.io/yaml v1.4.0 // indirect

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,6 @@ github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0
159159
github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
160160
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
161161
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
162-
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
163-
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
164162
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
165163
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
166164
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/*
2+
Copyright 2025 The Aibrix Team.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cache
18+
19+
import (
20+
"sync"
21+
"time"
22+
)
23+
24+
type getCurrentTime func() time.Time
25+
26+
var DefaultGetCurrentTime = func() time.Time {
27+
return time.Now()
28+
}
29+
30+
type LRUStore[K comparable, V any] struct {
31+
sync.RWMutex
32+
freeTable map[K]*entry[K, V]
33+
lruList *list[K, V]
34+
cap int
35+
36+
getCurrentTime
37+
interval time.Duration
38+
ttl time.Duration
39+
}
40+
41+
func NewLRUStore[K comparable, V any](cap int, ttl, interval time.Duration, f getCurrentTime) *LRUStore[K, V] {
42+
store := &LRUStore[K, V]{
43+
freeTable: make(map[K]*entry[K, V]),
44+
lruList: &list[K, V]{head: &entry[K, V]{}, tail: &entry[K, V]{}},
45+
cap: cap,
46+
ttl: ttl,
47+
interval: interval,
48+
getCurrentTime: f,
49+
}
50+
store.lruList.head.next = store.lruList.tail
51+
store.lruList.tail.prev = store.lruList.head
52+
53+
go store.startEviction()
54+
return store
55+
}
56+
57+
func (e *LRUStore[K, V]) startEviction() {
58+
ticker := time.NewTicker(e.interval)
59+
defer ticker.Stop()
60+
for range ticker.C {
61+
e.evict(e.getCurrentTime())
62+
}
63+
}
64+
65+
func (e *LRUStore[K, V]) Put(key K, value V) bool {
66+
e.Lock()
67+
defer e.Unlock()
68+
69+
if entry, exists := e.freeTable[key]; exists {
70+
entry.lastAccessTime = time.Now()
71+
entry.Value = value
72+
e.lruList.moveToHead(entry)
73+
return false
74+
}
75+
76+
entry := &entry[K, V]{Key: key, Value: value, lastAccessTime: time.Now()}
77+
e.lruList.addToHead(entry)
78+
e.freeTable[key] = entry
79+
if len(e.freeTable) > e.cap {
80+
removed := e.lruList.removeTail()
81+
if removed == nil {
82+
return false
83+
}
84+
delete(e.freeTable, removed.Key)
85+
return true
86+
}
87+
return false
88+
}
89+
90+
func (e *LRUStore[K, V]) Get(key K) (V, bool) {
91+
e.Lock()
92+
defer e.Unlock()
93+
94+
if entry, exists := e.freeTable[key]; exists {
95+
entry.lastAccessTime = time.Now()
96+
e.lruList.moveToHead(entry)
97+
return entry.Value, true
98+
}
99+
var zero V
100+
return zero, false
101+
}
102+
103+
func (e *LRUStore[K, V]) Len() int {
104+
e.RLock()
105+
defer e.RUnlock()
106+
return len(e.freeTable)
107+
}
108+
109+
func (e *LRUStore[K, V]) evict(now time.Time) {
110+
var keysToEvict []K
111+
112+
e.RLock()
113+
for key, entry := range e.freeTable {
114+
if now.Sub(entry.lastAccessTime) > e.ttl {
115+
keysToEvict = append(keysToEvict, key)
116+
}
117+
}
118+
e.RUnlock()
119+
120+
for _, key := range keysToEvict {
121+
e.Lock()
122+
if entry, exists := e.freeTable[key]; exists && now.Sub(entry.lastAccessTime) > e.ttl {
123+
e.lruList.remove(entry)
124+
delete(e.freeTable, key)
125+
}
126+
e.Unlock()
127+
}
128+
}
129+
130+
type entry[K comparable, V any] struct {
131+
Key K
132+
Value V
133+
prev *entry[K, V]
134+
next *entry[K, V]
135+
lastAccessTime time.Time
136+
}
137+
138+
type list[K comparable, V any] struct {
139+
head *entry[K, V]
140+
tail *entry[K, V]
141+
}
142+
143+
func (l *list[K, V]) addToHead(e *entry[K, V]) {
144+
e.prev = l.head
145+
e.next = l.head.next
146+
l.head.next.prev = e
147+
l.head.next = e
148+
}
149+
150+
func (l *list[K, V]) moveToHead(e *entry[K, V]) {
151+
l.remove(e)
152+
l.addToHead(e)
153+
}
154+
155+
func (l *list[K, V]) remove(e *entry[K, V]) {
156+
if e.prev != nil {
157+
e.prev.next = e.next
158+
}
159+
if e.next != nil {
160+
e.next.prev = e.prev
161+
}
162+
e.prev = nil
163+
e.next = nil
164+
}
165+
166+
func (l *list[K, V]) removeTail() *entry[K, V] {
167+
if l.tail.prev == l.head {
168+
return nil
169+
}
170+
entry := l.tail.prev
171+
l.remove(entry)
172+
return entry
173+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
Copyright 2025 The Aibrix Team.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cache
18+
19+
import (
20+
"fmt"
21+
"testing"
22+
"time"
23+
)
24+
25+
// TODO: add performance benchmark tests
26+
func TestLRUStore_PutAndGet(t *testing.T) {
27+
store := NewLRUStore[string, string](2, 5*time.Second, 1*time.Second, DefaultGetCurrentTime)
28+
29+
// Test adding and retrieving items
30+
store.Put("key1", "value1")
31+
store.Put("key2", "value2")
32+
33+
if val, ok := store.Get("key1"); !ok || val != "value1" {
34+
t.Errorf("expected value1, got %v", val)
35+
}
36+
37+
if val, ok := store.Get("key2"); !ok || val != "value2" {
38+
t.Errorf("expected value2, got %v", val)
39+
}
40+
41+
store.Put("key3", "value3")
42+
if _, ok := store.Get("key1"); ok {
43+
t.Errorf("expected key1 to be evicted")
44+
}
45+
46+
if val, ok := store.Get("key3"); !ok || val != "value3" {
47+
t.Errorf("expected value3, got %v", val)
48+
}
49+
}
50+
51+
func TestLRUStore_TTL(t *testing.T) {
52+
store := NewLRUStore[string, string](2, 2*time.Second, 1*time.Second, DefaultGetCurrentTime)
53+
54+
// Test TTL expiration
55+
store.Put("key1", "value1")
56+
time.Sleep(3 * time.Second) // Wait for TTL to expire
57+
58+
if _, ok := store.Get("key1"); ok {
59+
t.Errorf("expected key1 to be expired")
60+
}
61+
}
62+
63+
func TestLRUStore_UpdateExistingKey(t *testing.T) {
64+
store := NewLRUStore[string, string](2, 5*time.Second, 1*time.Second, DefaultGetCurrentTime)
65+
66+
// Test updating an existing key
67+
store.Put("key1", "value1")
68+
store.Put("key1", "value2")
69+
70+
if val, ok := store.Get("key1"); !ok || val != "value2" {
71+
t.Errorf("expected value2, got %v", val)
72+
}
73+
}
74+
75+
func TestLRUStore_ConcurrentEvictions(t *testing.T) {
76+
store := NewLRUStore[string, string](5, 5*time.Second, 1*time.Second, DefaultGetCurrentTime) // Small capacity to force evictions
77+
78+
const numGoroutines = 10
79+
const numOperations = 20
80+
done := make(chan error, numGoroutines)
81+
defer close(done)
82+
83+
for i := range numGoroutines {
84+
go func(id int) {
85+
for j := range numOperations {
86+
key := fmt.Sprintf("goroutine%d_key%d", id, j)
87+
value := fmt.Sprintf("value%d", j)
88+
89+
store.Put(key, value)
90+
91+
if store.Len() > store.cap {
92+
done <- fmt.Errorf("store exceeded capacity: expected at most %d, got %d", store.cap, len(store.freeTable))
93+
break
94+
}
95+
}
96+
done <- nil
97+
}(i)
98+
}
99+
100+
for range numGoroutines {
101+
if err := <-done; err != nil {
102+
t.Error(err)
103+
}
104+
}
105+
}

pkg/plugins/gateway/cache/store.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
Copyright 2025 The Aibrix Team.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
package cache
17+
18+
type Store[K comparable, V any] interface {
19+
Put(key K, value V) bool
20+
Get(key K) (V, bool)
21+
Len() int
22+
}

0 commit comments

Comments
 (0)