Skip to content

Commit 46e3742

Browse files
drcaramelsyrupgumpt
authored andcommitted
Allow cache hit filter to run on stale meta
This changes the cache hit filter to run on both fresh and stale cache hits so implementors can choose force invalidation behavior on stale assets as well.
1 parent fda3317 commit 46e3742

File tree

5 files changed

+80
-22
lines changed

5 files changed

+80
-22
lines changed

.bleep

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2513fe1ff4219fbb218dfd1fe7ffb4bbfb96aa91
1+
ceca785a0ae9a5fe7d33d9b75ce003caa941b727

pingora-proxy/src/proxy_cache.rs

+28-21
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use pingora_core::protocols::http::conditional_filter::to_304;
2222
use pingora_core::protocols::http::v1::common::header_value_content_length;
2323
use pingora_core::ErrorType;
2424
use range_filter::RangeBodyFilter;
25+
use std::time::SystemTime;
2526

2627
impl<SV> HttpProxy<SV> {
2728
// return bool: server_session can be reused, and error if any
@@ -113,29 +114,35 @@ impl<SV> HttpProxy<SV> {
113114

114115
// hit
115116
// TODO: maybe round and/or cache now()
116-
let hit_status = if meta.is_fresh(std::time::SystemTime::now()) {
117-
// check if we should force expire or miss
118-
// vs. hard purge which forces miss)
119-
match self.inner.cache_hit_filter(session, &meta, ctx).await {
120-
Err(e) => {
121-
error!(
122-
"Failed to filter cache hit: {e}, {}",
123-
self.inner.request_summary(session, ctx)
124-
);
125-
// this return value will cause us to fetch from upstream
126-
HitStatus::FailedHitFilter
127-
}
128-
Ok(None) => HitStatus::Fresh,
129-
Ok(Some(ForcedInvalidationKind::ForceExpired)) => {
130-
// force expired asset should not be serve as stale
131-
// because force expire is usually to remove data
132-
meta.disable_serve_stale();
133-
HitStatus::ForceExpired
117+
let is_fresh = meta.is_fresh(SystemTime::now());
118+
// check if we should force expire or force miss
119+
let hit_status = match self
120+
.inner
121+
.cache_hit_filter(session, &meta, is_fresh, ctx)
122+
.await
123+
{
124+
Err(e) => {
125+
error!(
126+
"Failed to filter cache hit: {e}, {}",
127+
self.inner.request_summary(session, ctx)
128+
);
129+
// this return value will cause us to fetch from upstream
130+
HitStatus::FailedHitFilter
131+
}
132+
Ok(None) => {
133+
if is_fresh {
134+
HitStatus::Fresh
135+
} else {
136+
HitStatus::Expired
134137
}
135-
Ok(Some(ForcedInvalidationKind::ForceMiss)) => HitStatus::ForceMiss,
136138
}
137-
} else {
138-
HitStatus::Expired
139+
Ok(Some(ForcedInvalidationKind::ForceExpired)) => {
140+
// force expired asset should not be serve as stale
141+
// because force expire is usually to remove data
142+
meta.disable_serve_stale();
143+
HitStatus::ForceExpired
144+
}
145+
Ok(Some(ForcedInvalidationKind::ForceMiss)) => HitStatus::ForceMiss,
139146
};
140147

141148
hit_status_opt = Some(hit_status);

pingora-proxy/src/proxy_trait.rs

+2
Original file line numberDiff line numberDiff line change
@@ -138,13 +138,15 @@ pub trait ProxyHttp {
138138
/// cache asset is ready to be used.
139139
///
140140
/// This filter allows the user to log or force invalidate the asset.
141+
/// This also runs on stale hit assets (for which `is_fresh` is false).
141142
///
142143
/// The value returned indicates if the force invalidation should be used,
143144
/// and which kind. Returning `None` indicates no forced invalidation
144145
async fn cache_hit_filter(
145146
&self,
146147
_session: &Session,
147148
_meta: &CacheMeta,
149+
_is_fresh: bool,
148150
_ctx: &mut Self::CTX,
149151
) -> Result<Option<ForcedInvalidationKind>>
150152
where

pingora-proxy/tests/test_upstream.rs

+42
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,48 @@ mod test_cache {
477477
assert_eq!(res.text().await.unwrap(), "hello world");
478478
}
479479

480+
#[tokio::test]
481+
async fn test_force_miss_stale() {
482+
init();
483+
let url = "http://127.0.0.1:6148/unique/test_froce_miss_stale/revalidate_now";
484+
485+
let res = reqwest::get(url).await.unwrap();
486+
assert_eq!(res.status(), StatusCode::OK);
487+
let headers = res.headers();
488+
let cache_miss_epoch = headers["x-epoch"].to_str().unwrap().parse::<f64>().unwrap();
489+
assert_eq!(headers["x-cache-status"], "miss");
490+
assert_eq!(headers["x-upstream-status"], "200");
491+
assert_eq!(res.text().await.unwrap(), "hello world");
492+
493+
let res = reqwest::get(url).await.unwrap();
494+
assert_eq!(res.status(), StatusCode::OK);
495+
let headers = res.headers();
496+
let cache_hit_epoch = headers["x-epoch"].to_str().unwrap().parse::<f64>().unwrap();
497+
assert_eq!(headers["x-cache-status"], "hit");
498+
assert!(headers.get("x-upstream-status").is_none());
499+
assert_eq!(res.text().await.unwrap(), "hello world");
500+
501+
assert_eq!(cache_miss_epoch, cache_hit_epoch);
502+
503+
sleep(Duration::from_millis(1100)).await; // ttl is 1
504+
505+
// stale, but can be forced miss
506+
let res = reqwest::Client::new()
507+
.get(url)
508+
.header("x-force-miss", "1")
509+
.send()
510+
.await
511+
.unwrap();
512+
513+
assert_eq!(res.status(), StatusCode::OK);
514+
let headers = res.headers();
515+
assert_eq!(headers["x-cache-status"], "miss");
516+
assert_eq!(headers["x-upstream-status"], "200");
517+
let cache_miss_epoch2 = headers["x-epoch"].to_str().unwrap().parse::<f64>().unwrap();
518+
assert!(cache_miss_epoch != cache_miss_epoch2);
519+
assert_eq!(res.text().await.unwrap(), "hello world");
520+
}
521+
480522
#[tokio::test]
481523
async fn test_cache_downstream_revalidation_etag() {
482524
init();

pingora-proxy/tests/utils/server_utils.rs

+7
Original file line numberDiff line numberDiff line change
@@ -415,12 +415,19 @@ impl ProxyHttp for ExampleProxyCache {
415415
&self,
416416
session: &Session,
417417
_meta: &CacheMeta,
418+
is_fresh: bool,
418419
_ctx: &mut Self::CTX,
419420
) -> Result<Option<ForcedInvalidationKind>> {
420421
// allow test header to control force expiry/miss
421422
if session.get_header_bytes("x-force-miss") != b"" {
422423
return Ok(Some(ForcedInvalidationKind::ForceMiss));
423424
}
425+
426+
if !is_fresh {
427+
// already expired
428+
return Ok(None);
429+
}
430+
424431
if session.get_header_bytes("x-force-expire") != b"" {
425432
return Ok(Some(ForcedInvalidationKind::ForceExpired));
426433
}

0 commit comments

Comments
 (0)