Skip to content

Commit 0122517

Browse files
authored
Improve ttl backing (#11)
* Improve TTL backing * Improve test matrix * Improve tests (test all features)
1 parent d2c5ce3 commit 0122517

File tree

6 files changed

+282
-89
lines changed

6 files changed

+282
-89
lines changed

.github/workflows/rust.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,11 @@ jobs:
2323
- uses: actions/checkout@v2
2424
- name: Build
2525
run: cargo build
26-
- name: Run tests
27-
run: cargo test --features "ttl-cache","lru-cache"
26+
- name: Run tests no-features
27+
run: cargo test
28+
- name: Run tests lru-feature
29+
run: cargo test --features lru-cache
30+
- name: Run tests ttl-feature
31+
run: cargo test --features ttl-cache
32+
- name: Run tests all-features
33+
run: cargo test --features lru-cache,ttl-cache

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ thiserror = "1.0"
1919
# Optional feature based dependencies
2020
lru = { version = "0.6.5", optional = true }
2121

22+
[dev-dependencies]
23+
cache_loader_async_macros = { path = "./cache-loader-async-macros" }
24+
2225
[features]
2326
default = []
2427
lru-cache = ["lru"]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "cache_loader_async_macros"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[lib]
9+
proc-macro = true
10+
11+
[dependencies]
12+
syn = { version = "1.0", features = ["full"] }
13+
quote = "1.0"
14+
proc-macro2 = "1.0"
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
extern crate proc_macro;
2+
3+
use quote::quote;
4+
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
5+
use proc_macro2::token_stream::IntoIter;
6+
7+
fn collect_diamond_idents(stream: &mut IntoIter) -> Vec<Ident> {
8+
let mut idents = Vec::new();
9+
if let TokenTree::Punct(punct) = stream.next().expect("Missing next element") {
10+
if !punct.as_char().eq(&'<') {
11+
panic!("Invalid diamond start");
12+
}
13+
} else {
14+
panic!("Invalid diamond start");
15+
}
16+
let mut expect_ident = true;
17+
loop {
18+
match stream.next().expect("Missing next element") {
19+
TokenTree::Ident(ident) => {
20+
if expect_ident {
21+
expect_ident = false;
22+
idents.push(ident);
23+
} else {
24+
panic!("Invalid diamond format! (Didn't expect ident)");
25+
}
26+
},
27+
TokenTree::Punct(punct) => {
28+
if !expect_ident {
29+
if punct.as_char().eq(&',') {
30+
expect_ident = true;
31+
} else if punct.as_char().eq(&'>') {
32+
break;
33+
} else {
34+
panic!("Invalid diamond format! (Invalid punct)");
35+
}
36+
} else {
37+
panic!("Invalid diamond format! (Didn't expect punct)");
38+
}
39+
}
40+
_ => panic!("Invalid type"),
41+
}
42+
}
43+
idents
44+
}
45+
46+
#[proc_macro]
47+
pub fn test_with_features(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
48+
let mut stream = TokenStream::from(item).into_iter();
49+
let fn_ident = if let TokenTree::Ident(ident) = stream.next().expect("First token mandatory") {
50+
ident
51+
} else {
52+
panic!("First token must be an ident!");
53+
};
54+
let ident = if let TokenTree::Ident(ident) = stream.next().expect("Second token mandatory") {
55+
ident
56+
} else {
57+
panic!("Second token must be an ident!");
58+
};
59+
let types = collect_diamond_idents(&mut stream);
60+
let loader = if let TokenTree::Group(group) = stream.next().expect("Missing group token") {
61+
group
62+
} else {
63+
panic!("Group token not present");
64+
};
65+
66+
let mut fn_body = quote! {};
67+
while let Some(token) = stream.next() {
68+
fn_body = quote! {
69+
#fn_body #token
70+
}
71+
}
72+
73+
let key_type = types.get(0).unwrap();
74+
let value_type = types.get(1).unwrap();
75+
let error_type = types.get(2).unwrap();
76+
77+
let fn_ident_default = syn::Ident::new(&format!("test_default_{}", fn_ident), Span::call_site());
78+
let fn_ident_lru = syn::Ident::new(&format!("test_lru_{}", fn_ident), Span::call_site());
79+
let fn_ident_ttl = syn::Ident::new(&format!("test_ttl_{}", fn_ident), Span::call_site());
80+
81+
let result = quote! {
82+
#[tokio::test]
83+
async fn #fn_ident_default() {
84+
let #ident: LoadingCache<#key_type, #value_type, #error_type> = LoadingCache::new(move |key: #key_type| {
85+
async move #loader
86+
});
87+
88+
#fn_body
89+
}
90+
91+
#[cfg(feature = "lru-cache")]
92+
#[tokio::test]
93+
async fn #fn_ident_lru() {
94+
let #ident: LoadingCache<#key_type, #value_type, #error_type> = LoadingCache::with_backing(LruCacheBacking::new(100), move |key: #key_type| {
95+
async move #loader
96+
});
97+
98+
#fn_body
99+
}
100+
101+
#[cfg(feature = "ttl-cache")]
102+
#[tokio::test]
103+
async fn #fn_ident_ttl() {
104+
let #ident: LoadingCache<#key_type, #value_type, #error_type> = LoadingCache::with_backing(TtlCacheBacking::new(Duration::from_secs(3)), move |key: #key_type| {
105+
async move #loader
106+
});
107+
108+
#fn_body
109+
}
110+
};
111+
112+
return result.into();
113+
}

src/backing.rs

Lines changed: 99 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ pub trait CacheBacking<K, V>
2323

2424
#[cfg(feature = "lru-cache")]
2525
pub struct LruCacheBacking<K, V> {
26-
lru: LruCache<K, V>
26+
lru: LruCache<K, V>,
2727
}
2828

2929
#[cfg(feature = "lru-cache")]
@@ -93,10 +93,27 @@ impl<
9393
#[cfg(feature = "ttl-cache")]
9494
pub struct TtlCacheBacking<K, V> {
9595
ttl: Duration,
96-
expiry_queue: VecDeque<(K, Instant)>,
96+
expiry_queue: VecDeque<TTlEntry<K>>,
9797
map: HashMap<K, (V, Instant)>,
9898
}
9999

100+
#[cfg(feature = "ttl-cache")]
101+
struct TTlEntry<K> {
102+
key: K,
103+
expiry: Instant,
104+
105+
}
106+
107+
#[cfg(feature = "ttl-cache")]
108+
impl<K> From<(K, Instant)> for TTlEntry<K> {
109+
fn from(tuple: (K, Instant)) -> Self {
110+
Self {
111+
key: tuple.0,
112+
expiry: tuple.1,
113+
}
114+
}
115+
}
116+
100117
#[cfg(feature = "ttl-cache")]
101118
impl<
102119
K: Eq + Hash + Sized + Clone + Send,
@@ -117,21 +134,21 @@ impl<
117134
fn set(&mut self, key: K, value: V) -> Option<V> {
118135
self.remove_old();
119136
let expiry = Instant::now().add(self.ttl);
120-
let option = self.map.insert(key.clone(), (value, expiry));
121-
if option.is_some() {
122-
self.expiry_queue.retain(|(vec_key, _)| vec_key.ne(&key));
137+
let result = self.replace(key.clone(), value, expiry);
138+
match self.expiry_queue.binary_search_by_key(&expiry, |entry| entry.expiry) {
139+
Ok(found) => {
140+
self.expiry_queue.insert(found + 1, (key, expiry).into());
141+
}
142+
Err(idx) => {
143+
self.expiry_queue.insert(idx, (key, expiry).into());
144+
}
123145
}
124-
self.expiry_queue.push_back((key, expiry));
125-
option.map(|(value, _)| value)
146+
result
126147
}
127148

128149
fn remove(&mut self, key: &K) -> Option<V> {
129150
self.remove_old();
130-
let option = self.map.remove(key);
131-
if option.is_some() {
132-
self.expiry_queue.retain(|(vec_key, _)| vec_key.ne(&key));
133-
}
134-
option.map(|(value, _)| value)
151+
self.remove_key(key)
135152
}
136153

137154
fn contains_key(&self, key: &K) -> bool {
@@ -154,7 +171,8 @@ impl<
154171
.collect::<Vec<K>>();
155172
for key in keys.into_iter() {
156173
self.map.remove(&key);
157-
self.expiry_queue.retain(|(expiry_key, _)| expiry_key.ne(&key))
174+
// optimize looping through expiry_queue multiple times?
175+
self.expiry_queue.retain(|entry| entry.key.ne(&key))
158176
}
159177
}
160178

@@ -165,7 +183,7 @@ impl<
165183
}
166184

167185
#[cfg(feature = "ttl-cache")]
168-
impl<K: Hash + Sized + PartialEq + Eq, V> TtlCacheBacking<K, V> {
186+
impl<K: Eq + Hash + Sized + Clone + Send, V: Sized + Clone + Send> TtlCacheBacking<K, V> {
169187
pub fn new(ttl: Duration) -> TtlCacheBacking<K, V> {
170188
TtlCacheBacking {
171189
ttl,
@@ -176,18 +194,80 @@ impl<K: Hash + Sized + PartialEq + Eq, V> TtlCacheBacking<K, V> {
176194

177195
fn remove_old(&mut self) {
178196
let now = Instant::now();
179-
while let Some((key, expiry)) = self.expiry_queue.pop_front() {
180-
if now.lt(&expiry) {
181-
self.expiry_queue.push_front((key, expiry));
197+
while let Some(entry) = self.expiry_queue.pop_front() {
198+
if now.lt(&entry.expiry) {
199+
self.expiry_queue.push_front(entry);
182200
break;
183201
}
184-
self.map.remove(&key);
202+
self.map.remove(&entry.key);
203+
}
204+
}
205+
206+
fn replace(&mut self, key: K, value: V, expiry: Instant) -> Option<V> {
207+
let entry = self.map.insert(key.clone(), (value, expiry));
208+
self.cleanup_expiry(entry, &key)
209+
}
210+
211+
fn remove_key(&mut self, key: &K) -> Option<V> {
212+
let entry = self.map.remove(key);
213+
self.cleanup_expiry(entry, key)
214+
}
215+
216+
fn cleanup_expiry(&mut self, entry: Option<(V, Instant)>, key: &K) -> Option<V> {
217+
if let Some((value, old_expiry)) = entry {
218+
match self.expiry_queue.binary_search_by_key(&old_expiry, |entry| entry.expiry) {
219+
Ok(found) => {
220+
let index = self.expiry_index_on_key_eq(found, &old_expiry, key);
221+
if let Some(index) = index {
222+
self.expiry_queue.remove(index);
223+
} else {
224+
// expiry not found (key)???
225+
}
226+
}
227+
Err(_) => {
228+
// expiry not found???
229+
}
230+
}
231+
Some(value)
232+
} else {
233+
None
234+
}
235+
}
236+
237+
fn expiry_index_on_key_eq(&self, idx: usize, expiry: &Instant, key: &K) -> Option<usize> {
238+
let entry = self.expiry_queue.get(idx).unwrap();
239+
if entry.key.eq(key) {
240+
return Some(idx);
241+
}
242+
243+
let mut offset = 0;
244+
while idx - offset > 0 {
245+
offset += 1;
246+
let entry = self.expiry_queue.get(idx - offset).unwrap();
247+
if !entry.expiry.eq(expiry) {
248+
break;
249+
}
250+
if entry.key.eq(key) {
251+
return Some(idx - offset);
252+
}
253+
}
254+
offset = 0;
255+
while idx + offset < self.expiry_queue.len() {
256+
offset += 1;
257+
let entry = self.expiry_queue.get(idx + offset).unwrap();
258+
if !entry.expiry.eq(expiry) {
259+
break;
260+
}
261+
if entry.key.eq(key) {
262+
return Some(idx + offset);
263+
}
185264
}
265+
None
186266
}
187267
}
188268

189269
pub struct HashMapBacking<K, V> {
190-
map: HashMap<K, V>
270+
map: HashMap<K, V>,
191271
}
192272

193273
impl<

0 commit comments

Comments
 (0)