From 746614cccb5d24f27ac67ec9dc9b98241c434b0d Mon Sep 17 00:00:00 2001 From: Mola-maker <229602071+Mola-maker@users.noreply.github.com> Date: Sun, 1 Nov 2026 04:15:36 +0000 Subject: [PATCH] fix(layers/foyer): skip cached entries for conditional reads A cached full-object entry carries no precondition, so serving it to a conditional read would never validate the condition against the origin. On a cache hit, fall back to the origin, which observes the original read args. On a cache miss the fill below still validates the condition at the origin, preserving the args-preserving fill from #7718. This addresses the remaining part of #7686: the fill path was fixed by #7718, but the hit path still ignored preconditions. --- core/layers/foyer/src/full.rs | 19 +++++++++++++++++ core/layers/foyer/src/lib.rs | 39 +++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/core/layers/foyer/src/full.rs b/core/layers/foyer/src/full.rs index 023e35992c45..1f1f1fd4a15b 100644 --- a/core/layers/foyer/src/full.rs +++ b/core/layers/foyer/src/full.rs @@ -135,6 +135,25 @@ impl FullReader { return Ok(None); } + // A cached entry carries no precondition, so a conditional read must + // not be served from it: fall back to the origin, which observes the + // original args. On a miss the fill below still validates the + // condition at the origin, so only the hit path is skipped. + // `version` is not listed here because it is part of the cache key. + if self.args.if_match().is_some() + || self.args.if_none_match().is_some() + || self.args.if_version_match().is_some() + || self.args.if_version_not_match().is_some() + || self.args.if_modified_since().is_some() + || self.args.if_unmodified_since().is_some() + { + match self.inner.cache.get(&key).await { + Ok(Some(_)) => return Ok(None), + Ok(None) => {} + Err(e) => return Err(extract_err(e)), + } + } + let result = self .inner .cache diff --git a/core/layers/foyer/src/lib.rs b/core/layers/foyer/src/lib.rs index 80b26a77b49b..45624c12fa3d 100644 --- a/core/layers/foyer/src/lib.rs +++ b/core/layers/foyer/src/lib.rs @@ -714,6 +714,45 @@ mod tests { assert_eq!(read_args.if_match(), Some("etag-1")); } + #[tokio::test] + async fn test_conditional_read_bypasses_full_object_cache() { + let cache = memory_cache().await; + let source = Arc::new(MockReadService::new("0123456789")); + let state = source.state.clone(); + let service = FoyerLayer::new(cache) + .with_size_limit(0..100) + .apply_service(source); + let ctx = service_context(&service); + + // Fill the cache with an unconditional read. + let reader = service.read(&ctx, "test", OpRead::default()).unwrap(); + let (_, mut stream) = reader.open(BytesRange::from(0_u64..2)).await.unwrap(); + let buffer = stream.read_all().await.unwrap(); + assert_eq!(buffer.to_vec(), b"01"); + assert_eq!(state.stat_calls.load(Ordering::Relaxed), 1); + assert_eq!(state.open_calls.load(Ordering::Relaxed), 1); + + // A conditional read must not be served from the cache: the cached + // entry carries no condition, so only the origin can observe it. + let (_, args, _) = options::ReadOptions { + if_match: Some("etag-1".to_owned()), + ..Default::default() + } + .into(); + let reader = service.read(&ctx, "test", args).unwrap(); + let (_, mut stream) = reader.open(BytesRange::from(0_u64..2)).await.unwrap(); + let buffer = stream.read_all().await.unwrap(); + assert_eq!(buffer.to_vec(), b"01"); + assert_eq!(state.open_calls.load(Ordering::Relaxed), 2); + + // Unconditional reads still hit the cache. + let reader = service.read(&ctx, "test", OpRead::default()).unwrap(); + let (_, mut stream) = reader.open(BytesRange::from(0_u64..2)).await.unwrap(); + let buffer = stream.read_all().await.unwrap(); + assert_eq!(buffer.to_vec(), b"01"); + assert_eq!(state.open_calls.load(Ordering::Relaxed), 2); + } + #[tokio::test] async fn test() { let dir = tempfile::tempdir().unwrap();