offheap 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/CI.yml +115 -0
- package/Cargo.toml +35 -0
- package/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/README.md +161 -0
- package/artifacts/bindings-aarch64-apple-darwin/index.d.ts +30 -0
- package/artifacts/bindings-aarch64-apple-darwin/index.js +316 -0
- package/artifacts/bindings-x86_64-apple-darwin/index.d.ts +30 -0
- package/artifacts/bindings-x86_64-apple-darwin/index.js +316 -0
- package/artifacts/bindings-x86_64-pc-windows-msvc/index.d.ts +30 -0
- package/artifacts/bindings-x86_64-pc-windows-msvc/index.js +316 -0
- package/artifacts/bindings-x86_64-unknown-linux-gnu/index.d.ts +30 -0
- package/artifacts/bindings-x86_64-unknown-linux-gnu/index.js +316 -0
- package/benches/rust_bench.rs +96 -0
- package/benchmarks/js_bench.js +67 -0
- package/build.rs +3 -0
- package/index.d.ts +30 -0
- package/index.js +316 -0
- package/package.json +35 -0
- package/src/algorithms/arc.rs +355 -0
- package/src/algorithms/lru.rs +212 -0
- package/src/algorithms/mod.rs +42 -0
- package/src/algorithms/tinylfu.rs +499 -0
- package/src/cache.rs +172 -0
- package/src/lib.rs +6 -0
- package/src/manager.rs +50 -0
- package/tests/index.test.js +135 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
use std::collections::HashMap;
|
|
2
|
+
use super::{CacheImpl, CacheEntry, CacheStats};
|
|
3
|
+
|
|
4
|
+
#[derive(Copy, Clone, PartialEq, Debug)]
|
|
5
|
+
enum ArcList {
|
|
6
|
+
T1,
|
|
7
|
+
B1,
|
|
8
|
+
T2,
|
|
9
|
+
B2,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
struct ArcNode {
|
|
13
|
+
key: String,
|
|
14
|
+
value: Option<CacheEntry>, // Only T1 and T2 store values. B1 and B2 are ghost entries.
|
|
15
|
+
list: ArcList,
|
|
16
|
+
prev: Option<usize>,
|
|
17
|
+
next: Option<usize>,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
struct ArcListMetadata {
|
|
21
|
+
head: Option<usize>,
|
|
22
|
+
tail: Option<usize>,
|
|
23
|
+
len: usize,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
impl ArcListMetadata {
|
|
27
|
+
fn new() -> Self {
|
|
28
|
+
Self {
|
|
29
|
+
head: None,
|
|
30
|
+
tail: None,
|
|
31
|
+
len: 0,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pub struct ArcCache {
|
|
37
|
+
capacity: usize,
|
|
38
|
+
map: HashMap<String, usize>,
|
|
39
|
+
nodes: Vec<ArcNode>,
|
|
40
|
+
free_nodes: Vec<usize>,
|
|
41
|
+
|
|
42
|
+
t1: ArcListMetadata,
|
|
43
|
+
b1: ArcListMetadata,
|
|
44
|
+
t2: ArcListMetadata,
|
|
45
|
+
b2: ArcListMetadata,
|
|
46
|
+
|
|
47
|
+
p: usize, // Target size for T1
|
|
48
|
+
hits: u64,
|
|
49
|
+
misses: u64,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
impl ArcCache {
|
|
53
|
+
pub fn new(capacity: usize) -> Self {
|
|
54
|
+
Self {
|
|
55
|
+
capacity,
|
|
56
|
+
map: HashMap::with_capacity(capacity * 2), // Map contains history as well
|
|
57
|
+
nodes: Vec::with_capacity(capacity * 2),
|
|
58
|
+
free_nodes: Vec::new(),
|
|
59
|
+
t1: ArcListMetadata::new(),
|
|
60
|
+
b1: ArcListMetadata::new(),
|
|
61
|
+
t2: ArcListMetadata::new(),
|
|
62
|
+
b2: ArcListMetadata::new(),
|
|
63
|
+
p: 0,
|
|
64
|
+
hits: 0,
|
|
65
|
+
misses: 0,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fn detach(&mut self, idx: usize) {
|
|
70
|
+
let prev = self.nodes[idx].prev;
|
|
71
|
+
let next = self.nodes[idx].next;
|
|
72
|
+
let list = self.nodes[idx].list;
|
|
73
|
+
|
|
74
|
+
let meta = match list {
|
|
75
|
+
ArcList::T1 => &mut self.t1,
|
|
76
|
+
ArcList::B1 => &mut self.b1,
|
|
77
|
+
ArcList::T2 => &mut self.t2,
|
|
78
|
+
ArcList::B2 => &mut self.b2,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if let Some(p) = prev {
|
|
82
|
+
self.nodes[p].next = next;
|
|
83
|
+
} else {
|
|
84
|
+
meta.head = next;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if let Some(n) = next {
|
|
88
|
+
self.nodes[n].prev = prev;
|
|
89
|
+
} else {
|
|
90
|
+
meta.tail = prev;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
meta.len -= 1;
|
|
94
|
+
self.nodes[idx].prev = None;
|
|
95
|
+
self.nodes[idx].next = None;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fn attach_to_head(&mut self, idx: usize, list: ArcList) {
|
|
99
|
+
self.nodes[idx].list = list;
|
|
100
|
+
let meta = match list {
|
|
101
|
+
ArcList::T1 => &mut self.t1,
|
|
102
|
+
ArcList::B1 => &mut self.b1,
|
|
103
|
+
ArcList::T2 => &mut self.t2,
|
|
104
|
+
ArcList::B2 => &mut self.b2,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
self.nodes[idx].prev = None;
|
|
108
|
+
self.nodes[idx].next = meta.head;
|
|
109
|
+
|
|
110
|
+
if let Some(h) = meta.head {
|
|
111
|
+
self.nodes[h].prev = Some(idx);
|
|
112
|
+
} else {
|
|
113
|
+
meta.tail = Some(idx);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
meta.head = Some(idx);
|
|
117
|
+
meta.len += 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
fn remove_completely(&mut self, idx: usize) -> String {
|
|
121
|
+
self.detach(idx);
|
|
122
|
+
self.free_nodes.push(idx);
|
|
123
|
+
let key = std::mem::take(&mut self.nodes[idx].key);
|
|
124
|
+
self.nodes[idx].value = None;
|
|
125
|
+
key
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fn replace(&mut self, key_in_b2: bool) {
|
|
129
|
+
if self.t1.len > 0 && (self.t1.len > self.p || (key_in_b2 && self.t1.len == self.p)) {
|
|
130
|
+
if let Some(t1_tail) = self.t1.tail {
|
|
131
|
+
self.detach(t1_tail);
|
|
132
|
+
self.nodes[t1_tail].value = None; // Evict value from memory
|
|
133
|
+
self.attach_to_head(t1_tail, ArcList::B1);
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
if let Some(t2_tail) = self.t2.tail {
|
|
137
|
+
self.detach(t2_tail);
|
|
138
|
+
self.nodes[t2_tail].value = None; // Evict value from memory
|
|
139
|
+
self.attach_to_head(t2_tail, ArcList::B2);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
impl CacheImpl for ArcCache {
|
|
146
|
+
fn get(&mut self, key: &str) -> Option<Vec<u8>> {
|
|
147
|
+
if let Some(&idx) = self.map.get(key) {
|
|
148
|
+
let list = self.nodes[idx].list;
|
|
149
|
+
match list {
|
|
150
|
+
ArcList::T1 | ArcList::T2 => {
|
|
151
|
+
let expired = if let Some(ref entry) = self.nodes[idx].value {
|
|
152
|
+
entry.is_expired()
|
|
153
|
+
} else {
|
|
154
|
+
true
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
if expired {
|
|
158
|
+
self.remove_completely(idx);
|
|
159
|
+
self.map.remove(key);
|
|
160
|
+
self.misses += 1;
|
|
161
|
+
None
|
|
162
|
+
} else {
|
|
163
|
+
self.detach(idx);
|
|
164
|
+
self.attach_to_head(idx, ArcList::T2);
|
|
165
|
+
self.hits += 1;
|
|
166
|
+
self.nodes[idx].value.as_ref().map(|e| e.value.clone())
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
ArcList::B1 | ArcList::B2 => {
|
|
170
|
+
// History hit. We can't return value (evicted), so counts as miss for user,
|
|
171
|
+
// but we trigger adaptation.
|
|
172
|
+
self.misses += 1;
|
|
173
|
+
None
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
self.misses += 1;
|
|
178
|
+
None
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
fn set(&mut self, key: &str, value: Vec<u8>, ttl_ms: Option<u64>) -> Option<Vec<u8>> {
|
|
183
|
+
if self.capacity == 0 {
|
|
184
|
+
return None;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if let Some(&idx) = self.map.get(key) {
|
|
188
|
+
let list = self.nodes[idx].list;
|
|
189
|
+
match list {
|
|
190
|
+
ArcList::T1 | ArcList::T2 => {
|
|
191
|
+
// Cache hit. Update value.
|
|
192
|
+
let old_value = self.nodes[idx].value.replace(CacheEntry::new(value, ttl_ms)).map(|e| e.value);
|
|
193
|
+
self.detach(idx);
|
|
194
|
+
self.attach_to_head(idx, ArcList::T2);
|
|
195
|
+
old_value
|
|
196
|
+
}
|
|
197
|
+
ArcList::B1 => {
|
|
198
|
+
// Hit in B1: we need more recency
|
|
199
|
+
let b1_len = if self.b1.len == 0 { 1 } else { self.b1.len };
|
|
200
|
+
let delta = std::cmp::max(1, self.b2.len / b1_len);
|
|
201
|
+
self.p = std::cmp::min(self.p + delta, self.capacity);
|
|
202
|
+
|
|
203
|
+
self.replace(false);
|
|
204
|
+
|
|
205
|
+
self.detach(idx);
|
|
206
|
+
self.nodes[idx].value = Some(CacheEntry::new(value, ttl_ms));
|
|
207
|
+
self.attach_to_head(idx, ArcList::T2);
|
|
208
|
+
None
|
|
209
|
+
}
|
|
210
|
+
ArcList::B2 => {
|
|
211
|
+
// Hit in B2: we need more frequency
|
|
212
|
+
let b2_len = if self.b2.len == 0 { 1 } else { self.b2.len };
|
|
213
|
+
let delta = std::cmp::max(1, self.b1.len / b2_len);
|
|
214
|
+
self.p = self.p.saturating_sub(delta);
|
|
215
|
+
|
|
216
|
+
self.replace(true);
|
|
217
|
+
|
|
218
|
+
self.detach(idx);
|
|
219
|
+
self.nodes[idx].value = Some(CacheEntry::new(value, ttl_ms));
|
|
220
|
+
self.attach_to_head(idx, ArcList::T2);
|
|
221
|
+
None
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
// Cache Miss (not in cache, nor in history)
|
|
226
|
+
let l1_len = self.t1.len + self.b1.len;
|
|
227
|
+
let l2_len = self.t2.len + self.b2.len;
|
|
228
|
+
|
|
229
|
+
if l1_len == self.capacity {
|
|
230
|
+
if self.t1.len < self.capacity {
|
|
231
|
+
if let Some(b1_tail) = self.b1.tail {
|
|
232
|
+
let evicted_key = self.remove_completely(b1_tail);
|
|
233
|
+
self.map.remove(&evicted_key);
|
|
234
|
+
}
|
|
235
|
+
self.replace(false);
|
|
236
|
+
} else {
|
|
237
|
+
if let Some(t1_tail) = self.t1.tail {
|
|
238
|
+
let evicted_key = self.remove_completely(t1_tail);
|
|
239
|
+
self.map.remove(&evicted_key);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
} else if l1_len < self.capacity && l1_len + l2_len >= self.capacity {
|
|
243
|
+
if l1_len + l2_len == 2 * self.capacity {
|
|
244
|
+
if let Some(b2_tail) = self.b2.tail {
|
|
245
|
+
let evicted_key = self.remove_completely(b2_tail);
|
|
246
|
+
self.map.remove(&evicted_key);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
self.replace(false);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Create new node
|
|
253
|
+
let idx = if let Some(free_idx) = self.free_nodes.pop() {
|
|
254
|
+
self.nodes[free_idx].key = key.to_string();
|
|
255
|
+
self.nodes[free_idx].value = Some(CacheEntry::new(value, ttl_ms));
|
|
256
|
+
free_idx
|
|
257
|
+
} else {
|
|
258
|
+
let free_idx = self.nodes.len();
|
|
259
|
+
self.nodes.push(ArcNode {
|
|
260
|
+
key: key.to_string(),
|
|
261
|
+
value: Some(CacheEntry::new(value, ttl_ms)),
|
|
262
|
+
list: ArcList::T1,
|
|
263
|
+
prev: None,
|
|
264
|
+
next: None,
|
|
265
|
+
});
|
|
266
|
+
free_idx
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
self.attach_to_head(idx, ArcList::T1);
|
|
270
|
+
self.map.insert(key.to_string(), idx);
|
|
271
|
+
None
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
fn delete(&mut self, key: &str) -> bool {
|
|
276
|
+
if let Some(&idx) = self.map.get(key) {
|
|
277
|
+
self.remove_completely(idx);
|
|
278
|
+
self.map.remove(key);
|
|
279
|
+
true
|
|
280
|
+
} else {
|
|
281
|
+
false
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
fn clear(&mut self) {
|
|
286
|
+
self.map.clear();
|
|
287
|
+
self.nodes.clear();
|
|
288
|
+
self.free_nodes.clear();
|
|
289
|
+
self.t1 = ArcListMetadata::new();
|
|
290
|
+
self.b1 = ArcListMetadata::new();
|
|
291
|
+
self.t2 = ArcListMetadata::new();
|
|
292
|
+
self.b2 = ArcListMetadata::new();
|
|
293
|
+
self.p = 0;
|
|
294
|
+
self.hits = 0;
|
|
295
|
+
self.misses = 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
fn stats(&self) -> CacheStats {
|
|
299
|
+
CacheStats {
|
|
300
|
+
hits: self.hits,
|
|
301
|
+
misses: self.misses,
|
|
302
|
+
capacity: self.capacity,
|
|
303
|
+
size: self.t1.len + self.t2.len,
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
fn keys(&self) -> Vec<String> {
|
|
308
|
+
let mut result = Vec::new();
|
|
309
|
+
// Return active keys in T1
|
|
310
|
+
let mut curr = self.t1.head;
|
|
311
|
+
while let Some(idx) = curr {
|
|
312
|
+
let node = &self.nodes[idx];
|
|
313
|
+
let expired = node.value.as_ref().map_or(true, |e| e.is_expired());
|
|
314
|
+
if !expired {
|
|
315
|
+
result.push(node.key.clone());
|
|
316
|
+
}
|
|
317
|
+
curr = node.next;
|
|
318
|
+
}
|
|
319
|
+
// Return active keys in T2
|
|
320
|
+
let mut curr = self.t2.head;
|
|
321
|
+
while let Some(idx) = curr {
|
|
322
|
+
let node = &self.nodes[idx];
|
|
323
|
+
let expired = node.value.as_ref().map_or(true, |e| e.is_expired());
|
|
324
|
+
if !expired {
|
|
325
|
+
result.push(node.key.clone());
|
|
326
|
+
}
|
|
327
|
+
curr = node.next;
|
|
328
|
+
}
|
|
329
|
+
result
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
#[cfg(test)]
|
|
334
|
+
mod tests {
|
|
335
|
+
use super::*;
|
|
336
|
+
|
|
337
|
+
#[test]
|
|
338
|
+
fn test_arc_basic() {
|
|
339
|
+
let mut cache = ArcCache::new(3);
|
|
340
|
+
cache.set("a", vec![1], None);
|
|
341
|
+
cache.set("b", vec![2], None);
|
|
342
|
+
cache.set("c", vec![3], None);
|
|
343
|
+
|
|
344
|
+
// Hits to move to T2
|
|
345
|
+
assert_eq!(cache.get("a"), Some(vec![1]));
|
|
346
|
+
assert_eq!(cache.get("b"), Some(vec![2]));
|
|
347
|
+
|
|
348
|
+
// Set new key, should evict c (which was in T1 and never hit)
|
|
349
|
+
cache.set("d", vec![4], None);
|
|
350
|
+
assert_eq!(cache.get("c"), None);
|
|
351
|
+
assert_eq!(cache.get("a"), Some(vec![1]));
|
|
352
|
+
assert_eq!(cache.get("b"), Some(vec![2]));
|
|
353
|
+
assert_eq!(cache.get("d"), Some(vec![4]));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
use std::collections::HashMap;
|
|
2
|
+
use super::{CacheImpl, CacheEntry, CacheStats};
|
|
3
|
+
|
|
4
|
+
struct LruNode {
|
|
5
|
+
key: String,
|
|
6
|
+
entry: CacheEntry,
|
|
7
|
+
prev: Option<usize>,
|
|
8
|
+
next: Option<usize>,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
pub struct LruCache {
|
|
12
|
+
capacity: usize,
|
|
13
|
+
map: HashMap<String, usize>,
|
|
14
|
+
nodes: Vec<LruNode>,
|
|
15
|
+
free_nodes: Vec<usize>,
|
|
16
|
+
head: Option<usize>, // Most Recently Used
|
|
17
|
+
tail: Option<usize>, // Least Recently Used
|
|
18
|
+
hits: u64,
|
|
19
|
+
misses: u64,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
impl LruCache {
|
|
23
|
+
pub fn new(capacity: usize) -> Self {
|
|
24
|
+
Self {
|
|
25
|
+
capacity,
|
|
26
|
+
map: HashMap::with_capacity(capacity),
|
|
27
|
+
nodes: Vec::with_capacity(capacity),
|
|
28
|
+
free_nodes: Vec::new(),
|
|
29
|
+
head: None,
|
|
30
|
+
tail: None,
|
|
31
|
+
hits: 0,
|
|
32
|
+
misses: 0,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fn detach(&mut self, node_idx: usize) {
|
|
37
|
+
let prev = self.nodes[node_idx].prev;
|
|
38
|
+
let next = self.nodes[node_idx].next;
|
|
39
|
+
|
|
40
|
+
if let Some(p) = prev {
|
|
41
|
+
self.nodes[p].next = next;
|
|
42
|
+
} else {
|
|
43
|
+
self.head = next;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if let Some(n) = next {
|
|
47
|
+
self.nodes[n].prev = prev;
|
|
48
|
+
} else {
|
|
49
|
+
self.tail = prev;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
self.nodes[node_idx].prev = None;
|
|
53
|
+
self.nodes[node_idx].next = None;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fn attach_to_head(&mut self, node_idx: usize) {
|
|
57
|
+
self.nodes[node_idx].prev = None;
|
|
58
|
+
self.nodes[node_idx].next = self.head;
|
|
59
|
+
|
|
60
|
+
if let Some(h) = self.head {
|
|
61
|
+
self.nodes[h].prev = Some(node_idx);
|
|
62
|
+
} else {
|
|
63
|
+
self.tail = Some(node_idx);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
self.head = Some(node_idx);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fn move_to_head(&mut self, node_idx: usize) {
|
|
70
|
+
if self.head == Some(node_idx) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
self.detach(node_idx);
|
|
74
|
+
self.attach_to_head(node_idx);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fn remove_node(&mut self, node_idx: usize) -> (String, CacheEntry) {
|
|
78
|
+
self.detach(node_idx);
|
|
79
|
+
self.free_nodes.push(node_idx);
|
|
80
|
+
let key = std::mem::take(&mut self.nodes[node_idx].key);
|
|
81
|
+
let entry = std::mem::replace(&mut self.nodes[node_idx].entry, CacheEntry::new(Vec::new(), None));
|
|
82
|
+
(key, entry)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
impl CacheImpl for LruCache {
|
|
87
|
+
fn get(&mut self, key: &str) -> Option<Vec<u8>> {
|
|
88
|
+
if let Some(&node_idx) = self.map.get(key) {
|
|
89
|
+
if self.nodes[node_idx].entry.is_expired() {
|
|
90
|
+
self.remove_node(node_idx);
|
|
91
|
+
self.map.remove(key);
|
|
92
|
+
self.misses += 1;
|
|
93
|
+
None
|
|
94
|
+
} else {
|
|
95
|
+
self.move_to_head(node_idx);
|
|
96
|
+
self.hits += 1;
|
|
97
|
+
Some(self.nodes[node_idx].entry.value.clone())
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
self.misses += 1;
|
|
101
|
+
None
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fn set(&mut self, key: &str, value: Vec<u8>, ttl_ms: Option<u64>) -> Option<Vec<u8>> {
|
|
106
|
+
if let Some(&node_idx) = self.map.get(key) {
|
|
107
|
+
// Key exists, update it
|
|
108
|
+
let old_value = std::mem::replace(
|
|
109
|
+
&mut self.nodes[node_idx].entry,
|
|
110
|
+
CacheEntry::new(value, ttl_ms),
|
|
111
|
+
).value;
|
|
112
|
+
self.move_to_head(node_idx);
|
|
113
|
+
Some(old_value)
|
|
114
|
+
} else {
|
|
115
|
+
// If full, evict the LRU (tail)
|
|
116
|
+
if self.map.len() >= self.capacity && self.capacity > 0 {
|
|
117
|
+
if let Some(t_idx) = self.tail {
|
|
118
|
+
let (evicted_key, _) = self.remove_node(t_idx);
|
|
119
|
+
self.map.remove(&evicted_key);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Insert new entry
|
|
124
|
+
let node_idx = if let Some(idx) = self.free_nodes.pop() {
|
|
125
|
+
self.nodes[idx].key = key.to_string();
|
|
126
|
+
self.nodes[idx].entry = CacheEntry::new(value, ttl_ms);
|
|
127
|
+
idx
|
|
128
|
+
} else {
|
|
129
|
+
let idx = self.nodes.len();
|
|
130
|
+
self.nodes.push(LruNode {
|
|
131
|
+
key: key.to_string(),
|
|
132
|
+
entry: CacheEntry::new(value, ttl_ms),
|
|
133
|
+
prev: None,
|
|
134
|
+
next: None,
|
|
135
|
+
});
|
|
136
|
+
idx
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
self.attach_to_head(node_idx);
|
|
140
|
+
self.map.insert(key.to_string(), node_idx);
|
|
141
|
+
None
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
fn delete(&mut self, key: &str) -> bool {
|
|
146
|
+
if let Some(&node_idx) = self.map.get(key) {
|
|
147
|
+
self.remove_node(node_idx);
|
|
148
|
+
self.map.remove(key);
|
|
149
|
+
true
|
|
150
|
+
} else {
|
|
151
|
+
false
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
fn clear(&mut self) {
|
|
156
|
+
self.map.clear();
|
|
157
|
+
self.nodes.clear();
|
|
158
|
+
self.free_nodes.clear();
|
|
159
|
+
self.head = None;
|
|
160
|
+
self.tail = None;
|
|
161
|
+
self.hits = 0;
|
|
162
|
+
self.misses = 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
fn stats(&self) -> CacheStats {
|
|
166
|
+
CacheStats {
|
|
167
|
+
hits: self.hits,
|
|
168
|
+
misses: self.misses,
|
|
169
|
+
capacity: self.capacity,
|
|
170
|
+
size: self.map.len(),
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
fn keys(&self) -> Vec<String> {
|
|
175
|
+
let mut result = Vec::new();
|
|
176
|
+
let mut curr = self.head;
|
|
177
|
+
while let Some(idx) = curr {
|
|
178
|
+
let node = &self.nodes[idx];
|
|
179
|
+
if !node.entry.is_expired() {
|
|
180
|
+
result.push(node.key.clone());
|
|
181
|
+
}
|
|
182
|
+
curr = node.next;
|
|
183
|
+
}
|
|
184
|
+
result
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#[cfg(test)]
|
|
189
|
+
mod tests {
|
|
190
|
+
use super::*;
|
|
191
|
+
|
|
192
|
+
#[test]
|
|
193
|
+
fn test_lru_basic() {
|
|
194
|
+
let mut cache = LruCache::new(2);
|
|
195
|
+
cache.set("a", vec![1], None);
|
|
196
|
+
cache.set("b", vec![2], None);
|
|
197
|
+
assert_eq!(cache.get("a"), Some(vec![1]));
|
|
198
|
+
cache.set("c", vec![3], None); // should evict "b"
|
|
199
|
+
assert_eq!(cache.get("b"), None);
|
|
200
|
+
assert_eq!(cache.get("a"), Some(vec![1]));
|
|
201
|
+
assert_eq!(cache.get("c"), Some(vec![3]));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
#[test]
|
|
205
|
+
fn test_lru_expiry() {
|
|
206
|
+
let mut cache = LruCache::new(2);
|
|
207
|
+
cache.set("a", vec![1], Some(10)); // 10ms
|
|
208
|
+
assert_eq!(cache.get("a"), Some(vec![1]));
|
|
209
|
+
std::thread::sleep(std::time::Duration::from_millis(15));
|
|
210
|
+
assert_eq!(cache.get("a"), None);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
use std::time::{Duration, Instant};
|
|
2
|
+
|
|
3
|
+
#[derive(Debug, Clone)]
|
|
4
|
+
pub struct CacheStats {
|
|
5
|
+
pub hits: u64,
|
|
6
|
+
pub misses: u64,
|
|
7
|
+
pub capacity: usize,
|
|
8
|
+
pub size: usize,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
pub struct CacheEntry {
|
|
12
|
+
pub value: Vec<u8>,
|
|
13
|
+
pub expires_at: Option<Instant>,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
impl CacheEntry {
|
|
17
|
+
pub fn new(value: Vec<u8>, ttl_ms: Option<u64>) -> Self {
|
|
18
|
+
let expires_at = ttl_ms.map(|ms| Instant::now() + Duration::from_millis(ms));
|
|
19
|
+
Self { value, expires_at }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
pub fn is_expired(&self) -> bool {
|
|
23
|
+
if let Some(expiry) = self.expires_at {
|
|
24
|
+
Instant::now() > expiry
|
|
25
|
+
} else {
|
|
26
|
+
false
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
pub trait CacheImpl: Send + Sync {
|
|
32
|
+
fn get(&mut self, key: &str) -> Option<Vec<u8>>;
|
|
33
|
+
fn set(&mut self, key: &str, value: Vec<u8>, ttl_ms: Option<u64>) -> Option<Vec<u8>>;
|
|
34
|
+
fn delete(&mut self, key: &str) -> bool;
|
|
35
|
+
fn clear(&mut self);
|
|
36
|
+
fn stats(&self) -> CacheStats;
|
|
37
|
+
fn keys(&self) -> Vec<String>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
pub mod lru;
|
|
41
|
+
pub mod arc;
|
|
42
|
+
pub mod tinylfu;
|