1use std::{
4 collections::VecDeque,
5 sync::{
6 Arc, Mutex,
7 atomic::{AtomicU64, Ordering},
8 },
9 time::{Duration, SystemTime},
10};
11
12use serde_json::Value;
13
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub(crate) struct CacheKey {
17 tool_name: String,
19 arguments: String,
22}
23
24impl CacheKey {
25 #[cfg(test)]
27 fn new(tool_name: String, arguments: Value) -> Self {
28 Self {
29 tool_name,
30 arguments: canonical_json(&arguments),
31 }
32 }
33
34 pub(crate) fn for_generation(tool_name: String, arguments: Value, generation: u64) -> Self {
36 Self {
37 tool_name,
38 arguments: format!("{generation}:{}", canonical_json(&arguments)),
39 }
40 }
41}
42
43#[derive(Debug, Clone)]
45struct CacheEntry {
46 result: Value,
48 timestamp: SystemTime,
50 ttl: Duration,
52}
53
54impl CacheEntry {
55 fn new(result: Value, ttl: Duration) -> Self {
57 Self {
58 result,
59 timestamp: SystemTime::now(),
60 ttl,
61 }
62 }
63
64 fn is_expired(&self) -> bool {
66 match self.timestamp.elapsed() {
67 Ok(elapsed) => elapsed >= self.ttl,
68 Err(_) => true,
69 }
70 }
71}
72
73#[derive(Clone)]
79pub(crate) struct ToolCallCache {
80 state: Arc<CacheState>,
82 default_ttl: Duration,
83 max_size: usize,
84 enable_cache: bool,
85}
86
87struct CacheState {
89 entries: dashmap::DashMap<CacheKey, StoredEntry>,
90 insertion_order: Mutex<VecDeque<(CacheKey, u64)>>,
93 next_generation: AtomicU64,
94 total_hits: AtomicU64,
95 total_misses: AtomicU64,
96}
97
98struct StoredEntry {
99 value: CacheEntry,
100 generation: u64,
101}
102
103impl ToolCallCache {
104 pub(crate) fn new() -> Self {
106 Self {
107 state: Arc::new(CacheState {
108 entries: dashmap::DashMap::new(),
109 insertion_order: Mutex::new(VecDeque::new()),
110 next_generation: AtomicU64::new(0),
111 total_hits: AtomicU64::new(0),
112 total_misses: AtomicU64::new(0),
113 }),
114 default_ttl: Duration::from_secs(300),
115 max_size: 1000,
116 enable_cache: true,
117 }
118 }
119
120 pub(crate) fn with_ttl(mut self, ttl: Duration) -> Self {
122 self.default_ttl = ttl;
123 self
124 }
125
126 pub(crate) fn with_max_size(mut self, size: usize) -> Self {
128 self.max_size = size;
129 self
130 }
131
132 pub(crate) fn with_enabled(mut self, enabled: bool) -> Self {
134 self.enable_cache = enabled;
135 self
136 }
137
138 pub(crate) fn enabled(&self) -> bool {
142 self.enable_cache
143 }
144
145 pub(crate) fn get(&self, key: &CacheKey) -> Option<Value> {
148 if !self.enable_cache {
149 return None;
150 }
151
152 let expired = self
155 .state
156 .entries
157 .remove_if(key, |_key, stored| stored.value.is_expired());
158
159 if let Some((_, expired)) = expired {
160 self.state
161 .insertion_order
162 .lock()
163 .unwrap_or_else(std::sync::PoisonError::into_inner)
164 .retain(|(queued_key, generation)| {
165 queued_key != key || *generation != expired.generation
166 });
167 saturating_increment(&self.state.total_misses);
168 return None;
169 }
170
171 let Some(stored) = self.state.entries.get(key) else {
172 saturating_increment(&self.state.total_misses);
173 return None;
174 };
175 saturating_increment(&self.state.total_hits);
176 Some(stored.value.result.clone())
177 }
178
179 pub(crate) fn insert(&self, key: CacheKey, result: Value, ttl: Option<Duration>) {
182 if !self.enable_cache || self.max_size == 0 {
183 return;
184 }
185
186 let mut order = self
190 .state
191 .insertion_order
192 .lock()
193 .unwrap_or_else(std::sync::PoisonError::into_inner);
194 order.retain(|(queued_key, _)| queued_key != &key);
197 let generation = self.state.next_generation.fetch_add(1, Ordering::Relaxed);
198 let entry = CacheEntry::new(result, ttl.unwrap_or(self.default_ttl));
199 self.state.entries.insert(
200 key.clone(),
201 StoredEntry {
202 value: entry,
203 generation,
204 },
205 );
206 order.push_back((key, generation));
207
208 while self.state.entries.len() > self.max_size {
209 let Some((oldest, oldest_generation)) = order.pop_front() else {
210 break;
211 };
212 self.state.entries.remove_if(&oldest, |_key, stored| {
213 stored.generation == oldest_generation
214 });
215 }
216 }
217
218 #[cfg(test)]
220 fn insert_with_key(&self, tool_name: String, arguments: Value, result: Value) {
221 let key = CacheKey::new(tool_name, arguments);
222 self.insert(key, result, None);
223 }
224
225 pub(crate) fn clear(&self) {
227 let mut order = self
228 .state
229 .insertion_order
230 .lock()
231 .unwrap_or_else(std::sync::PoisonError::into_inner);
232 self.state.entries.clear();
233 order.clear();
234 self.state.total_hits.store(0, Ordering::Relaxed);
235 self.state.total_misses.store(0, Ordering::Relaxed);
236 }
237
238 pub(crate) fn invalidate_tool(&self, tool_name: &str) {
240 let mut order = self
241 .state
242 .insertion_order
243 .lock()
244 .unwrap_or_else(std::sync::PoisonError::into_inner);
245 self.state
246 .entries
247 .retain(|key, _| key.tool_name != tool_name);
248 order.retain(|(key, _)| key.tool_name != tool_name);
249 }
250
251 pub(crate) fn stats(&self) -> CacheStats {
254 let mut expired_count = 0u64;
255
256 for entry in self.state.entries.iter() {
257 if entry.value.is_expired() {
258 expired_count += 1;
259 }
260 }
261
262 let total_entries = self.state.entries.len();
263 let total_hits = self.state.total_hits.load(Ordering::Relaxed);
264 let total_misses = self.state.total_misses.load(Ordering::Relaxed);
265 let total_lookups = total_hits.saturating_add(total_misses);
266 CacheStats {
267 total_entries,
268 total_hits,
269 total_misses,
270 expired_count,
271 hit_rate: if total_lookups == 0 {
272 0.0
273 } else {
274 total_hits as f64 / total_lookups as f64
275 },
276 }
277 }
278}
279
280impl Default for ToolCallCache {
281 fn default() -> Self {
282 Self::new()
283 }
284}
285
286#[derive(Debug, Clone, serde::Serialize)]
288pub struct CacheStats {
289 pub total_entries: usize,
291 pub total_hits: u64,
293 pub total_misses: u64,
295 pub expired_count: u64,
297 pub hit_rate: f64,
299}
300
301fn saturating_increment(counter: &AtomicU64) {
302 let _ = counter.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
303 value.checked_add(1)
304 });
305}
306
307fn canonical_json(value: &Value) -> String {
308 value.to_string()
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn test_cache_key_new() {
320 let args = serde_json::json!({"city": "Shenzhen", "count": 5});
321 let key = CacheKey::new("test_tool".to_string(), args);
322 assert_eq!(key.tool_name, "test_tool");
323 assert!(key.arguments.contains("city"));
324 }
325
326 #[test]
327 fn cache_key_distinguishes_name_input_and_registration_generation() {
328 let first = CacheKey::for_generation("one".to_string(), serde_json::json!({"n": 1}), 7);
329 let other_name =
330 CacheKey::for_generation("two".to_string(), serde_json::json!({"n": 1}), 7);
331 let other_input =
332 CacheKey::for_generation("one".to_string(), serde_json::json!({"n": 2}), 7);
333 let other_generation =
334 CacheKey::for_generation("one".to_string(), serde_json::json!({"n": 1}), 8);
335
336 assert_ne!(first, other_name);
337 assert_ne!(first, other_input);
338 assert_ne!(first, other_generation);
339 }
340
341 #[test]
342 fn test_cache_entry_expired() {
343 let entry = CacheEntry::new(
344 serde_json::json!({"result": "success"}),
345 Duration::from_secs(1),
346 );
347 assert!(!entry.is_expired());
348
349 let mut entry_mut = entry;
350 entry_mut.timestamp = SystemTime::now() - Duration::from_secs(2);
351 assert!(entry_mut.is_expired());
352 }
353
354 #[test]
355 fn test_cache_insert_get() {
356 let cache = ToolCallCache::new();
357 let args = serde_json::json!({"input": "test"});
358 let result = serde_json::json!({"output": "success"});
359
360 cache.insert_with_key("test_tool".to_string(), args.clone(), result.clone());
361
362 let key = CacheKey::new("test_tool".to_string(), args);
363 let cached = cache.get(&key);
364 assert!(cached.is_some());
365 assert_eq!(cached.unwrap(), result);
366 }
367
368 #[test]
369 fn test_cache_expiration() {
370 let cache = ToolCallCache::new().with_ttl(Duration::from_millis(10));
372 let args = serde_json::json!({"input": "test"});
373 let result = serde_json::json!({"output": "success"});
374
375 cache.insert_with_key("test_tool".to_string(), args.clone(), result);
376
377 let key = CacheKey::new("test_tool".to_string(), args);
378
379 assert!(cache.get(&key).is_some());
381
382 std::thread::sleep(Duration::from_millis(20));
384
385 assert!(cache.get(&key).is_none());
387 }
388
389 #[test]
390 fn test_cache_stats() {
391 let cache = ToolCallCache::new();
392 let args = serde_json::json!({"input": "test"});
393
394 cache.insert_with_key("tool_a".to_string(), args.clone(), serde_json::json!({}));
395 cache.insert_with_key("tool_b".to_string(), args.clone(), serde_json::json!({}));
396
397 let key = CacheKey::new("tool_a".to_string(), args);
398 let _ = cache.get(&key);
399 let _ = cache.get(&key);
400 let _ = cache.get(&CacheKey::new("missing".to_string(), serde_json::json!({})));
401
402 let stats = cache.stats();
403 assert_eq!(stats.total_entries, 2);
404 assert_eq!(stats.total_hits, 2);
405 assert_eq!(stats.total_misses, 1);
406 assert!((stats.hit_rate - (2.0 / 3.0)).abs() < f64::EPSILON);
407 }
408
409 #[test]
410 fn test_canonical_json() {
411 let obj = serde_json::json!({
412 "CITY": "Shenzhen",
413 "count": 5,
414 "Data": {"NAME": "test"}
415 });
416
417 let normalized = canonical_json(&obj);
418 let parsed: Value = serde_json::from_str(&normalized).unwrap();
419
420 if let Some(parsed_obj) = parsed.as_object() {
422 assert!(parsed_obj.contains_key("CITY"));
423 assert!(parsed_obj.contains_key("count"));
424 assert!(parsed_obj.contains_key("Data"));
425 assert_eq!(parsed_obj.get("CITY"), Some(&serde_json::json!("Shenzhen")));
426 assert_eq!(parsed_obj.get("count"), Some(&serde_json::json!(5)));
427 }
428 }
429
430 #[test]
431 fn test_canonical_json_preserves_key_whitespace() {
432 let obj = serde_json::json!({"CityName": "Shenzhen", " UserID ": 42});
433 let normalized = canonical_json(&obj);
434 let parsed: Value = serde_json::from_str(&normalized).unwrap();
435 assert!(parsed.as_object().unwrap().contains_key("CityName"));
436 assert!(parsed.as_object().unwrap().contains_key(" UserID "));
437 }
438
439 #[test]
440 fn test_cache_concurrent_insert_and_get() {
441 use std::{sync::Arc, thread};
442
443 let cache = Arc::new(ToolCallCache::new().with_max_size(1000));
444 let mut handles = Vec::new();
445
446 for i in 0..10 {
447 let cache_clone = Arc::clone(&cache);
448 handles.push(thread::spawn(move || {
449 let key_name = format!("tool_{i}");
450 let args = serde_json::json!({"input": i});
451 let result = serde_json::json!({"output": format!("result_{}", i)});
452 cache_clone.insert_with_key(key_name.clone(), args.clone(), result);
453
454 let key = CacheKey::new(key_name, args);
456 cache_clone.get(&key)
457 }));
458 }
459
460 let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
461 let successful_gets = results.iter().filter(|r| r.is_some()).count();
462 assert_eq!(successful_gets, 10);
463
464 let stats = cache.stats();
465 assert_eq!(stats.total_entries, 10);
466 }
467
468 #[test]
469 fn test_cache_evicts_at_capacity() {
470 let cache = ToolCallCache::new()
472 .with_max_size(5)
473 .with_ttl(Duration::from_secs(300));
474
475 for i in 0..5 {
477 let args = serde_json::json!({"input": i});
478 cache.insert_with_key(format!("tool_{i}"), args, serde_json::json!({"result": i}));
479 }
480
481 assert_eq!(cache.stats().total_entries, 5);
482
483 let args = serde_json::json!({"input": "new"});
485 cache.insert_with_key(
486 "tool_new".to_string(),
487 args,
488 serde_json::json!({"result": "new"}),
489 );
490
491 let stats = cache.stats();
492 assert!(stats.total_entries <= 5);
494 let key = CacheKey::new("tool_new".to_string(), serde_json::json!({"input": "new"}));
496 assert!(cache.get(&key).is_some());
497 }
498
499 #[test]
500 fn test_cache_clone_shares_state() {
501 let cache = ToolCallCache::new();
505 let other = cache.clone();
506
507 let args = serde_json::json!({"input": "shared"});
508 cache.insert_with_key(
509 "tool".to_string(),
510 args.clone(),
511 serde_json::json!({"v": 1}),
512 );
513
514 let key = CacheKey::new("tool".to_string(), args);
515 assert!(cache.get(&key).is_some());
516 assert!(other.get(&key).is_some());
518 }
519
520 #[test]
521 fn test_cache_evicts_in_fifo_order() {
522 let cache = ToolCallCache::new().with_max_size(3);
525 for i in 0..3 {
526 cache.insert_with_key(
527 format!("t{i}"),
528 serde_json::json!({"i": i}),
529 serde_json::json!({}),
530 );
531 }
532 assert_eq!(cache.stats().total_entries, 3);
533
534 cache.insert_with_key(
535 "t3".to_string(),
536 serde_json::json!({"i": 3}),
537 serde_json::json!({}),
538 );
539
540 assert!(
541 cache
542 .get(&CacheKey::new(
543 "t0".to_string(),
544 serde_json::json!({"i": 0})
545 ))
546 .is_none(),
547 "oldest entry (t0) should have been evicted"
548 );
549 assert!(
550 cache
551 .get(&CacheKey::new(
552 "t1".to_string(),
553 serde_json::json!({"i": 1})
554 ))
555 .is_some()
556 );
557 assert!(
558 cache
559 .get(&CacheKey::new(
560 "t3".to_string(),
561 serde_json::json!({"i": 3})
562 ))
563 .is_some()
564 );
565 assert!(cache.stats().total_entries <= 3);
566 }
567
568 #[test]
569 fn test_cache_max_size_zero_stores_nothing() {
570 let cache = ToolCallCache::new().with_max_size(0);
571 let args = serde_json::json!({"input": "test"});
572 let key = CacheKey::new("tool".to_string(), args.clone());
573
574 cache.insert_with_key(
575 "tool".to_string(),
576 args,
577 serde_json::json!({"result": true}),
578 );
579
580 assert!(cache.get(&key).is_none());
581 assert_eq!(cache.stats().total_entries, 0);
582 }
583
584 #[test]
585 fn test_cache_collides_on_reordered_keys() {
586 let cache = ToolCallCache::new();
588 cache.insert_with_key(
589 "t".to_string(),
590 serde_json::json!({"a": 1, "b": 2}),
591 serde_json::json!(true),
592 );
593 let reordered = CacheKey::new("t".to_string(), serde_json::json!({"b": 2, "a": 1}));
594 assert!(
595 cache.get(&reordered).is_some(),
596 "reordered object keys must collide in the cache"
597 );
598 }
599
600 #[test]
601 fn test_cache_key_preserves_whitespace_bearing_keys() {
602 let messy = CacheKey::new("t".to_string(), serde_json::json!({" a ": 1}));
603 let clean = CacheKey::new("t".to_string(), serde_json::json!({"a": 1}));
604 assert_ne!(messy, clean);
605 }
606
607 #[test]
608 fn string_and_json_null_do_not_collide() {
609 let string = CacheKey::new("t".to_string(), Value::String("null".to_string()));
610 let null = CacheKey::new("t".to_string(), Value::Null);
611 assert_ne!(string, null);
612 }
613
614 #[test]
615 fn invalidation_removes_fifo_record_before_reinsert() {
616 let cache = ToolCallCache::new().with_max_size(2);
617 let key = CacheKey::new("same".to_string(), serde_json::json!({}));
618 cache.insert(key.clone(), serde_json::json!(1), None);
619 cache.insert(
620 CacheKey::new("older".to_string(), serde_json::json!({})),
621 serde_json::json!(0),
622 None,
623 );
624 cache.invalidate_tool("same");
625 cache.insert(key.clone(), serde_json::json!(2), None);
626 cache.insert(
627 CacheKey::new("newest".to_string(), serde_json::json!({})),
628 serde_json::json!(3),
629 None,
630 );
631 assert_eq!(cache.get(&key), Some(serde_json::json!(2)));
632 assert_eq!(
633 cache
634 .state
635 .insertion_order
636 .lock()
637 .unwrap_or_else(std::sync::PoisonError::into_inner)
638 .len(),
639 2
640 );
641 }
642}