1use std::{
14 collections::HashMap,
15 sync::{Arc, Mutex},
16 time::Duration,
17};
18
19use axum::{
20 body::Body,
21 extract::Request,
22 http::{Method, StatusCode},
23 middleware::Next,
24 response::Response,
25};
26
27use crate::cache::LruCache;
28
29const MAX_CACHE_BODY_BYTES: usize = 8 * 1024 * 1024;
33
34const DEDUP_WAIT_TIMEOUT: Duration = Duration::from_secs(120);
37
38type CachedBody = (u16, Option<String>, Vec<u8>);
41
42enum CollectedBody {
44 Fitted(bytes::Bytes),
46 Oversized(futures_util::stream::BoxStream<'static, Result<bytes::Bytes, axum::Error>>),
50}
51
52struct BodyStream(Body);
57
58impl futures_util::Stream for BodyStream {
59 type Item = Result<bytes::Bytes, axum::Error>;
60
61 fn poll_next(
62 mut self: std::pin::Pin<&mut Self>,
63 cx: &mut std::task::Context<'_>,
64 ) -> std::task::Poll<Option<Self::Item>> {
65 use http_body::Body as _;
66 match std::pin::Pin::new(&mut self.0).poll_frame(cx) {
67 std::task::Poll::Pending => std::task::Poll::Pending,
68 std::task::Poll::Ready(None) => std::task::Poll::Ready(None),
69 std::task::Poll::Ready(Some(Err(err))) => {
70 std::task::Poll::Ready(Some(Err(axum::Error::new(err))))
71 },
72 std::task::Poll::Ready(Some(Ok(frame))) => match frame.into_data() {
73 Ok(bytes) => std::task::Poll::Ready(Some(Ok(bytes))),
74 Err(_trailers) => std::task::Poll::Ready(None),
77 },
78 }
79 }
80}
81
82async fn collect_for_cache(body: Body, limit: usize) -> Result<CollectedBody, axum::Error> {
91 use futures_util::{Stream, StreamExt};
92 let mut buf: Vec<u8> = Vec::new();
93 let mut frames = BodyStream(body);
94 loop {
95 let item = std::future::poll_fn(|cx| std::pin::Pin::new(&mut frames).poll_next(cx)).await;
96 let chunk = item.transpose().map_err(axum::Error::new)?;
97 let Some(mut chunk) = chunk else {
98 return Ok(CollectedBody::Fitted(bytes::Bytes::from(buf)));
99 };
100 let room = limit.saturating_sub(buf.len());
101 if chunk.len() <= room {
102 buf.extend_from_slice(&chunk);
103 continue;
104 }
105 let head = chunk.split_to(room);
108 buf.extend_from_slice(&head);
109 let tail = chunk;
110 let prefix = bytes::Bytes::from(buf);
111 let full: futures_util::stream::BoxStream<'static, Result<bytes::Bytes, axum::Error>> =
112 Box::pin(futures_util::stream::iter([Ok(prefix), Ok(tail)]).chain(frames));
113 return Ok(CollectedBody::Oversized(full));
114 }
115}
116
117struct InFlight {
119 done: Arc<tokio::sync::Notify>,
120 response: Mutex<Option<CachedBody>>,
121}
122
123struct InFlightGuard {
140 cache: Arc<CacheState>,
141 key: String,
142 entry: Arc<InFlight>,
143}
144
145impl Drop for InFlightGuard {
146 fn drop(&mut self) {
147 let mut map = self
148 .cache
149 .in_flight
150 .lock()
151 .expect("in_flight mutex poisoned");
152 match map.get(&self.key) {
153 Some(current) if Arc::ptr_eq(current, &self.entry) => {
154 map.remove(&self.key);
155 },
156 _ => {},
157 }
158 self.entry.done.notify_waiters();
161 }
162}
163
164#[derive(Clone, Copy, PartialEq, Eq, Debug)]
171enum Bucket {
172 Genai,
173 Stats,
174 Sessions,
175 Metrics,
176 Names,
177 Keys,
178}
179
180impl Bucket {
181 fn ttl(&self) -> Duration {
182 match self {
183 Bucket::Genai => Duration::from_secs(20),
184 Bucket::Stats => Duration::from_secs(60),
185 Bucket::Sessions => Duration::from_secs(10),
186 Bucket::Metrics => Duration::from_secs(60),
187 Bucket::Names => Duration::from_secs(600),
188 Bucket::Keys => Duration::from_secs(300),
189 }
190 }
191}
192
193pub struct CacheState {
195 genai: LruCache<String, String>,
196 stats: LruCache<String, String>,
197 sessions: LruCache<String, String>,
198 metrics: LruCache<String, String>,
199 names: LruCache<String, String>,
200 keys: LruCache<String, String>,
201 in_flight: Mutex<HashMap<String, Arc<InFlight>>>,
202}
203
204impl CacheState {
205 pub fn new() -> Arc<Self> {
206 Arc::new(Self {
207 genai: LruCache::new(500, Bucket::Genai.ttl()),
208 stats: LruCache::new(50, Bucket::Stats.ttl()),
209 sessions: LruCache::new(200, Bucket::Sessions.ttl()),
210 metrics: LruCache::new(200, Bucket::Metrics.ttl()),
211 names: LruCache::new(50, Bucket::Names.ttl()),
212 keys: LruCache::new(30, Bucket::Keys.ttl()),
213 in_flight: Mutex::new(HashMap::new()),
214 })
215 }
216
217 fn policy(path: &str) -> Option<Bucket> {
219 if path == "/api/stats" {
220 Some(Bucket::Stats)
221 } else if path == "/api/sessions" {
222 Some(Bucket::Sessions)
223 } else if path == "/api/metrics/names" {
224 Some(Bucket::Names)
225 } else if path == "/api/metrics/export" {
226 None
230 } else if path == "/api/metrics" || path.starts_with("/api/metrics/") {
231 Some(Bucket::Metrics)
232 } else if path == "/api/resource-keys" {
233 Some(Bucket::Keys)
234 } else if path.starts_with("/api/genai/") {
235 Some(Bucket::Genai)
236 } else {
237 None
238 }
239 }
240
241 fn get(&self, key: &str, bucket: Bucket) -> Option<String> {
242 let k = key.to_string();
243 match bucket {
244 Bucket::Genai => self.genai.get(&k),
245 Bucket::Stats => self.stats.get(&k),
246 Bucket::Sessions => self.sessions.get(&k),
247 Bucket::Metrics => self.metrics.get(&k),
248 Bucket::Names => self.names.get(&k),
249 Bucket::Keys => self.keys.get(&k),
250 }
251 }
252
253 fn insert(&self, key: &str, value: String, bucket: Bucket) {
254 match bucket {
255 Bucket::Genai => self.genai.insert(key.to_string(), value),
256 Bucket::Stats => self.stats.insert(key.to_string(), value),
257 Bucket::Sessions => self.sessions.insert(key.to_string(), value),
258 Bucket::Metrics => self.metrics.insert(key.to_string(), value),
259 Bucket::Names => self.names.insert(key.to_string(), value),
260 Bucket::Keys => self.keys.insert(key.to_string(), value),
261 }
262 }
263
264 pub fn clear_all(&self) {
266 self.genai.clear();
267 self.stats.clear();
268 self.sessions.clear();
269 self.metrics.clear();
270 self.names.clear();
271 self.keys.clear();
272 }
273}
274
275pub async fn cache_handler(
278 axum::extract::State(cache): axum::extract::State<Arc<CacheState>>,
279 req: Request,
280 next: Next,
281) -> Response {
282 let path = req.uri().path().to_string();
283 let bucket = match req.method() == Method::GET {
284 true => match CacheState::policy(&path) {
285 Some(b) => b,
286 None => return next.run(req).await,
287 },
288 false => return next.run(req).await,
289 };
290
291 let key = match req.uri().query() {
292 Some(q) => format!("{}?{}", path, q),
293 None => path,
294 };
295
296 if let Some(cached) = cache.get(&key, bucket) {
298 return json_response(&cached);
299 }
300
301 let (entry, is_owner) = {
303 let mut map = cache.in_flight.lock().expect("in_flight mutex poisoned");
304 match map.get(&key) {
305 Some(existing) => (Arc::clone(existing), false),
306 None => {
307 let entry = Arc::new(InFlight {
308 done: Arc::new(tokio::sync::Notify::new()),
309 response: Mutex::new(None),
310 });
311 map.insert(key.clone(), Arc::clone(&entry));
312 (entry, true)
313 },
314 }
315 };
316
317 if !is_owner {
318 let deadline = tokio::time::Instant::now() + DEDUP_WAIT_TIMEOUT;
323 loop {
324 let notified = entry.done.notified();
325 tokio::pin!(notified);
326 if let Some((status, content_type, body)) =
327 entry.response.lock().expect("poisoned").clone()
328 {
329 return rebuild_response(status, content_type, body);
330 }
331 if tokio::time::Instant::now() >= deadline {
332 break;
333 }
334 let still_present = cache
339 .in_flight
340 .lock()
341 .expect("in_flight mutex poisoned")
342 .get(&key)
343 .is_some_and(|current| Arc::ptr_eq(current, &entry));
344 if !still_present {
345 break;
346 }
347 let _ = tokio::time::timeout(Duration::from_secs(1), notified).await;
348 }
349 {
358 let mut map = cache.in_flight.lock().expect("in_flight mutex poisoned");
359 match map.get(&key) {
360 Some(current) if Arc::ptr_eq(current, &entry) => {
361 map.remove(&key);
362 },
363 _ => {},
364 }
365 }
366 entry.done.notify_waiters();
367 return next.run(req).await;
368 }
369
370 let _guard = InFlightGuard {
375 cache: Arc::clone(&cache),
376 key: key.clone(),
377 entry: Arc::clone(&entry),
378 };
379
380 let response = next.run(req).await;
382 let status = response.status();
383 let response = if status.is_success() {
384 let (parts, body) = response.into_parts();
385 match collect_for_cache(body, MAX_CACHE_BODY_BYTES).await {
386 Ok(CollectedBody::Fitted(bytes)) => {
387 let text = String::from_utf8_lossy(&bytes).into_owned();
388 if serde_json::from_str::<serde_json::Value>(&text).is_ok() {
390 cache.insert(&key, text, bucket);
391 }
392 let content_type = parts
393 .headers
394 .get(axum::http::header::CONTENT_TYPE)
395 .and_then(|v| v.to_str().ok())
396 .map(|s| s.to_string());
397 *entry.response.lock().expect("poisoned") =
398 Some((status.as_u16(), content_type, bytes.to_vec()));
399 Response::from_parts(parts, Body::from(bytes))
400 },
401 Ok(CollectedBody::Oversized(stream)) => {
402 tracing::debug!(
407 "Serving {} response uncached (body > {} bytes)",
408 key,
409 MAX_CACHE_BODY_BYTES
410 );
411 *entry.response.lock().expect("poisoned") = None;
412 Response::from_parts(parts, Body::from_stream(stream))
413 },
414 Err(err) => {
415 *entry.response.lock().expect("poisoned") = None;
419 tracing::debug!("Not caching {} response: {}", key, err);
420 Response::builder()
421 .status(StatusCode::INTERNAL_SERVER_ERROR)
422 .header(axum::http::header::CONTENT_TYPE, "application/json")
423 .body(Body::from(format!(
424 r#"{{"error":"failed to read response body: {}"}}"#,
425 err
426 )))
427 .expect("valid error response")
428 },
429 }
430 } else {
431 *entry.response.lock().expect("poisoned") = None;
433 response
434 };
435
436 response
439}
440
441fn json_response(body: &str) -> Response {
443 Response::builder()
444 .status(StatusCode::OK)
445 .header(axum::http::header::CONTENT_TYPE, "application/json")
446 .header("x-cache", "hit")
447 .body(Body::from(body.to_string()))
448 .expect("valid cached response")
449}
450
451fn rebuild_response(status: u16, content_type: Option<String>, body: Vec<u8>) -> Response {
453 let mut builder =
454 Response::builder().status(StatusCode::from_u16(status).unwrap_or(StatusCode::OK));
455 if let Some(ct) = content_type {
456 builder = builder.header(axum::http::header::CONTENT_TYPE, ct);
457 }
458 builder
459 .header("x-cache", "dedup")
460 .body(Body::from(body))
461 .expect("valid rebuilt response")
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 #[test]
469 fn test_policy_whitelist() {
470 assert_eq!(CacheState::policy("/api/genai/usage"), Some(Bucket::Genai));
471 assert_eq!(CacheState::policy("/api/stats"), Some(Bucket::Stats));
472 assert_eq!(CacheState::policy("/api/sessions"), Some(Bucket::Sessions));
473 assert_eq!(CacheState::policy("/api/metrics"), Some(Bucket::Metrics));
474 assert_eq!(
475 CacheState::policy("/api/metrics/names"),
476 Some(Bucket::Names)
477 );
478 assert_eq!(
479 CacheState::policy("/api/metrics/timeseries/whatever"),
480 Some(Bucket::Metrics)
481 );
482 assert_eq!(CacheState::policy("/api/resource-keys"), Some(Bucket::Keys));
483 assert_eq!(CacheState::policy("/api/traces"), None);
484 assert_eq!(CacheState::policy("/api/health"), None);
485 assert_eq!(CacheState::policy("/api/admin/purge"), None);
486 assert_eq!(
488 CacheState::policy("/api/genai/other/thing"),
489 Some(Bucket::Genai)
490 );
491 assert_eq!(CacheState::policy("/api/genai2/usage"), None);
492 assert_eq!(CacheState::policy("/api/metricstore"), None);
493 }
494
495 #[test]
496 fn test_policy_excludes_metrics_export() {
497 assert_eq!(CacheState::policy("/api/metrics/export"), None);
500 assert_eq!(CacheState::policy("/api/metrics"), Some(Bucket::Metrics));
502 assert_eq!(
503 CacheState::policy("/api/metrics/timeseries/whatever"),
504 Some(Bucket::Metrics)
505 );
506 }
507
508 #[test]
509 fn test_bucket_ttls() {
510 assert_eq!(Bucket::Genai.ttl(), Duration::from_secs(20));
511 assert_eq!(Bucket::Stats.ttl(), Duration::from_secs(60));
512 assert_eq!(Bucket::Sessions.ttl(), Duration::from_secs(10));
513 assert_eq!(Bucket::Metrics.ttl(), Duration::from_secs(60));
514 assert_eq!(Bucket::Names.ttl(), Duration::from_secs(600));
515 assert_eq!(Bucket::Keys.ttl(), Duration::from_secs(300));
516 }
517
518 #[test]
519 fn test_cache_clear_all() {
520 let state = CacheState::new();
521 state.insert("k1", "v1".to_string(), Bucket::Genai);
522 state.insert("k2", "v2".to_string(), Bucket::Stats);
523 state.insert("k3", "v3".to_string(), Bucket::Sessions);
524 state.insert("k4", "v4".to_string(), Bucket::Metrics);
525 state.insert("k5", "v5".to_string(), Bucket::Names);
526 state.insert("k6", "v6".to_string(), Bucket::Keys);
527 assert!(state.get("k1", Bucket::Genai).is_some());
528 assert!(state.get("k2", Bucket::Stats).is_some());
529 assert!(state.get("k3", Bucket::Sessions).is_some());
530 assert!(state.get("k4", Bucket::Metrics).is_some());
531 assert!(state.get("k5", Bucket::Names).is_some());
532 assert!(state.get("k6", Bucket::Keys).is_some());
533 state.clear_all();
534 assert!(state.get("k1", Bucket::Genai).is_none());
535 assert!(state.get("k2", Bucket::Stats).is_none());
536 assert!(state.get("k3", Bucket::Sessions).is_none());
537 assert!(state.get("k4", Bucket::Metrics).is_none());
538 assert!(state.get("k5", Bucket::Names).is_none());
539 assert!(state.get("k6", Bucket::Keys).is_none());
540 }
541
542 use axum::{routing::get, Json, Router};
545 use std::sync::atomic::{AtomicUsize, Ordering};
546 use tower::ServiceExt;
547
548 fn get_request(uri: &str) -> Request {
549 Request::builder()
550 .uri(uri)
551 .body(Body::empty())
552 .expect("valid request")
553 }
554
555 async fn body_text(response: Response) -> String {
556 let bytes = http_body_util::BodyExt::collect(response.into_body())
557 .await
558 .expect("body readable")
559 .to_bytes();
560 String::from_utf8(bytes.to_vec()).expect("utf-8 body")
561 }
562
563 async fn counting_handler(count: Arc<AtomicUsize>) -> Json<serde_json::Value> {
565 let n = count.fetch_add(1, Ordering::SeqCst) + 1;
566 Json(serde_json::json!({ "handler_runs": n }))
567 }
568
569 #[tokio::test]
570 async fn test_ttl_cache_hit_skips_handler() {
571 let count = Arc::new(AtomicUsize::new(0));
572 let c = Arc::clone(&count);
573 let app = Router::new()
574 .route(
575 "/api/genai/usage",
576 get(move |_req: axum::extract::Request| async move { counting_handler(c).await }),
577 )
578 .layer(axum::middleware::from_fn_with_state(
579 CacheState::new(),
580 cache_handler,
581 ));
582
583 let first = app
584 .clone()
585 .oneshot(get_request("/api/genai/usage?start=1&end=2"))
586 .await
587 .unwrap();
588 assert_eq!(first.status(), StatusCode::OK);
589 assert!(first.headers().get("x-cache").is_none());
590
591 let second = app
594 .clone()
595 .oneshot(get_request("/api/genai/usage?start=1&end=2"))
596 .await
597 .unwrap();
598 assert_eq!(second.status(), StatusCode::OK);
599 assert_eq!(
600 second
601 .headers()
602 .get("x-cache")
603 .and_then(|v| v.to_str().ok()),
604 Some("hit")
605 );
606 assert_eq!(count.load(Ordering::SeqCst), 1);
607 assert_eq!(body_text(second).await, r#"{"handler_runs":1}"#);
608
609 let third = app
611 .clone()
612 .oneshot(get_request("/api/genai/usage?start=1&end=3"))
613 .await
614 .unwrap();
615 assert_eq!(third.status(), StatusCode::OK);
616 assert_eq!(count.load(Ordering::SeqCst), 2);
617 }
618
619 #[tokio::test]
620 async fn test_in_flight_dedup_collapses_concurrent_requests() {
621 let count = Arc::new(AtomicUsize::new(0));
622 let c = Arc::clone(&count);
623 let app = Router::new()
624 .route(
625 "/api/genai/usage",
626 get(move |_req: axum::extract::Request| async move {
627 tokio::time::sleep(Duration::from_millis(200)).await;
629 counting_handler(c).await
630 }),
631 )
632 .layer(axum::middleware::from_fn_with_state(
633 CacheState::new(),
634 cache_handler,
635 ));
636
637 let uri = "/api/genai/usage?start=9&end=9";
638 let (first, second) = tokio::join!(
639 app.clone().oneshot(get_request(uri)),
640 app.clone().oneshot(get_request(uri)),
641 );
642 let first = first.unwrap();
643 let second = second.unwrap();
644 assert_eq!(first.status(), StatusCode::OK);
645 assert_eq!(second.status(), StatusCode::OK);
646 assert_eq!(count.load(Ordering::SeqCst), 1);
648 assert_eq!(body_text(first).await, body_text(second).await);
649 }
650
651 #[tokio::test]
652 async fn test_non_whitelisted_path_passes_through() {
653 let count = Arc::new(AtomicUsize::new(0));
654 let c = Arc::clone(&count);
655 let app = Router::new()
656 .route(
657 "/api/traces",
658 get(move |_req: axum::extract::Request| async move { counting_handler(c).await }),
659 )
660 .layer(axum::middleware::from_fn_with_state(
661 CacheState::new(),
662 cache_handler,
663 ));
664
665 let _ = app
666 .clone()
667 .oneshot(get_request("/api/traces"))
668 .await
669 .unwrap();
670 let second = app
671 .clone()
672 .oneshot(get_request("/api/traces"))
673 .await
674 .unwrap();
675 assert_eq!(count.load(Ordering::SeqCst), 2);
677 assert!(second.headers().get("x-cache").is_none());
678 }
679
680 #[tokio::test]
686 async fn test_oversize_response_served_uncached_not_500() {
687 let count = Arc::new(AtomicUsize::new(0));
691 let c = Arc::clone(&count);
692 let app = Router::new()
693 .route(
694 "/api/genai/usage",
695 get(move |_req: axum::extract::Request| async move {
696 let payload = "x".repeat(MAX_CACHE_BODY_BYTES + 1024);
697 let n = c.fetch_add(1, Ordering::SeqCst) + 1;
698 Json(serde_json::json!({ "handler_runs": n, "data": payload }))
699 }),
700 )
701 .layer(axum::middleware::from_fn_with_state(
702 CacheState::new(),
703 cache_handler,
704 ));
705
706 let first = app
707 .clone()
708 .oneshot(get_request("/api/genai/usage?start=1&end=2"))
709 .await
710 .unwrap();
711 assert_eq!(first.status(), StatusCode::OK, "oversize must not 500");
712 let body = body_text(first).await;
713 assert!(
714 body.len() > MAX_CACHE_BODY_BYTES,
715 "full body must be served: {} bytes",
716 body.len()
717 );
718 assert!(body.contains(&"x".repeat(MAX_CACHE_BODY_BYTES + 1024)));
719
720 let second = app
722 .clone()
723 .oneshot(get_request("/api/genai/usage?start=1&end=2"))
724 .await
725 .unwrap();
726 assert_eq!(second.status(), StatusCode::OK);
727 assert_eq!(count.load(Ordering::SeqCst), 2);
728 }
729
730 #[tokio::test]
731 async fn test_cancelled_owner_cleans_up_in_flight_entry() {
732 let count = Arc::new(AtomicUsize::new(0));
733 let state = CacheState::new();
734 let c = Arc::clone(&count);
735 let app = Router::new()
736 .route(
737 "/api/genai/usage",
738 get(move |_req: axum::extract::Request| async move {
739 tokio::time::sleep(Duration::from_millis(500)).await;
741 counting_handler(c).await
742 }),
743 )
744 .layer(axum::middleware::from_fn_with_state(
745 Arc::clone(&state),
746 cache_handler,
747 ));
748
749 let uri = "/api/genai/usage?start=7&end=7";
750
751 let owner_app = app.clone();
754 let owner = tokio::spawn(async move { owner_app.oneshot(get_request(uri)).await });
755 tokio::time::sleep(Duration::from_millis(150)).await;
756 eprintln!(
757 "DBG after owner sleep: count={} inflight={}",
758 count.load(Ordering::SeqCst),
759 state.in_flight.lock().expect("poisoned").len()
760 );
761
762 let waiter_app = app.clone();
764 let waiter = tokio::spawn(async move { waiter_app.oneshot(get_request(uri)).await });
765 tokio::time::sleep(Duration::from_millis(50)).await;
766 eprintln!(
767 "DBG after waiter sleep: count={} inflight={}",
768 count.load(Ordering::SeqCst),
769 state.in_flight.lock().expect("poisoned").len()
770 );
771
772 owner.abort();
774
775 let started = tokio::time::Instant::now();
776 let waiter_response = waiter
777 .await
778 .expect("waiter task must not abort")
779 .expect("waiter request completes");
780 let waiter_elapsed = started.elapsed();
781
782 let waiter_status = waiter_response.status();
788 let waiter_body = body_text(waiter_response).await;
789 assert_eq!(waiter_status, StatusCode::OK);
790 let runs = count.load(Ordering::SeqCst);
791 assert!(
792 (1..=2).contains(&runs),
793 "expected the owner's and/or waiter's handler run, got {runs}"
794 );
795 let parsed: serde_json::Value =
796 serde_json::from_str(&waiter_body).expect("waiter got valid JSON");
797 assert!(parsed.get("handler_runs").is_some());
798 assert!(
799 waiter_elapsed < Duration::from_secs(5),
800 "waiter waited {waiter_elapsed:?} after the owner's cancellation; a cleaned-up entry lets it run its own copy within ~1 s"
801 );
802
803 assert!(state.in_flight.lock().expect("poisoned").is_empty());
807 let follower = app
808 .clone()
809 .oneshot(get_request(uri))
810 .await
811 .expect("follower request completes");
812 assert_eq!(follower.status(), StatusCode::OK);
813 assert!(state.in_flight.lock().expect("poisoned").is_empty());
814 }
815
816 #[test]
820 fn test_stale_in_flight_guard_keeps_new_owner() {
821 let state = CacheState::new();
822 let key = "/api/stats".to_string();
823
824 let old_entry = Arc::new(InFlight {
825 done: Arc::new(tokio::sync::Notify::new()),
826 response: Mutex::new(None),
827 });
828 state
829 .in_flight
830 .lock()
831 .expect("poisoned")
832 .insert(key.clone(), Arc::clone(&old_entry));
833
834 let new_entry = Arc::new(InFlight {
837 done: Arc::new(tokio::sync::Notify::new()),
838 response: Mutex::new(None),
839 });
840 state
841 .in_flight
842 .lock()
843 .expect("poisoned")
844 .insert(key.clone(), Arc::clone(&new_entry));
845
846 let stale_guard = InFlightGuard {
847 cache: Arc::clone(&state),
848 key,
849 entry: old_entry,
850 };
851 drop(stale_guard);
852
853 let map = state.in_flight.lock().expect("poisoned");
854 assert!(
855 map.get("/api/stats")
856 .is_some_and(|current| Arc::ptr_eq(current, &new_entry)),
857 "the stale guard must not evict the new owner's entry"
858 );
859 }
860}