Vary examples

This commit is contained in:
Gustav Davidsson 2024-04-08 09:55:07 -07:00 committed by Kevin Guthrie
parent 7b838b8118
commit 2631da74b0
4 changed files with 308 additions and 30 deletions

2
.bleep
View file

@ -1 +1 @@
fa95ba9cdfb9a347913e0cffbb84ca003833ccaa
9d19d79b4015f2730c6213b9c20d9787e7ed896a

View file

@ -18,7 +18,7 @@ use utils::server_utils::init;
use utils::websocket::WS_ECHO;
use futures::{SinkExt, StreamExt};
use reqwest::header::HeaderValue;
use reqwest::header::{HeaderName, HeaderValue};
use reqwest::StatusCode;
use std::time::Duration;
use tokio_tungstenite::tungstenite::{client::IntoClientRequest, Message};
@ -135,6 +135,7 @@ async fn test_ws_server_ends_conn() {
mod test_cache {
use super::*;
use std::str::FromStr;
use tokio::time::sleep;
#[tokio::test]
@ -1578,28 +1579,60 @@ mod test_cache {
assert_eq!(res.text().await.unwrap(), "hello world!");
}
async fn send_vary_req(url: &str, vary: &str) -> reqwest::Response {
reqwest::Client::new()
async fn send_vary_req_with_headers_with_dups(
url: &str,
vary_field: &str,
headers: Vec<(&str, &str)>,
dup_headers: Vec<(&str, &str)>,
) -> reqwest::Response {
let req_headers = headers
.iter()
.map(|(name, value)| {
(
HeaderName::from_str(name).unwrap(),
HeaderValue::from_str(value).unwrap(),
)
})
.collect();
let mut req_builder = reqwest::Client::new()
.get(url)
.header("x-vary-me", vary)
.send()
.await
.unwrap()
.headers(req_headers)
.header("set-vary", vary_field);
// Apply any duplicate headers
for (key, value) in dup_headers {
req_builder = req_builder.header(key, value);
}
req_builder.send().await.unwrap()
}
async fn send_vary_req_with_headers(
url: &str,
vary_field: &str,
headers: Vec<(&str, &str)>,
) -> reqwest::Response {
send_vary_req_with_headers_with_dups(url, vary_field, headers, vec![]).await
}
async fn send_vary_req(url: &str, vary_field: &str, value: &str) -> reqwest::Response {
send_vary_req_with_headers(url, vary_field, vec![(vary_field, value)]).await
}
#[tokio::test]
async fn test_vary_caching() {
init();
let url = "http://127.0.0.1:6148/unique/test_vary_caching/now";
let url = "http://127.0.0.1:6148/unique/test_vary_caching/vary";
let vary_field = "Accept";
let res = send_vary_req(url, "a").await;
let res = send_vary_req(url, vary_field, "image/png").await;
assert_eq!(res.status(), StatusCode::OK);
let headers = res.headers();
let cache_a_miss_epoch = headers["x-epoch"].to_str().unwrap().parse::<f64>().unwrap();
assert_eq!(headers["x-cache-status"], "miss");
assert_eq!(res.text().await.unwrap(), "hello world");
let res = send_vary_req(url, "a").await;
let res = send_vary_req(url, vary_field, "image/png").await;
assert_eq!(res.status(), StatusCode::OK);
let headers = res.headers();
let cache_hit_epoch = headers["x-epoch"].to_str().unwrap().parse::<f64>().unwrap();
@ -1608,14 +1641,14 @@ mod test_cache {
assert_eq!(cache_a_miss_epoch, cache_hit_epoch);
let res = send_vary_req(url, "b").await;
let res = send_vary_req(url, vary_field, "image/jpeg").await;
assert_eq!(res.status(), StatusCode::OK);
let headers = res.headers();
let cache_b_miss_epoch = headers["x-epoch"].to_str().unwrap().parse::<f64>().unwrap();
assert_eq!(headers["x-cache-status"], "miss");
assert_eq!(res.text().await.unwrap(), "hello world");
let res = send_vary_req(url, "b").await;
let res = send_vary_req(url, vary_field, "image/jpeg").await;
assert_eq!(res.status(), StatusCode::OK);
let headers = res.headers();
let cache_hit_epoch = headers["x-epoch"].to_str().unwrap().parse::<f64>().unwrap();
@ -1626,19 +1659,235 @@ mod test_cache {
assert!(cache_a_miss_epoch != cache_b_miss_epoch);
}
#[tokio::test]
async fn test_vary_caching_ignored_vary_header() {
init();
let url = "http://127.0.0.1:6148/unique/test_vary_caching_ignored_vary_header/vary";
let vary_field = "Some-Ignored-Vary-Header";
// Asset into cache (png)
let res = send_vary_req(url, vary_field, "image/png").await;
assert_eq!(res.status(), StatusCode::OK);
let headers = res.headers();
let epoch = headers["x-epoch"].to_str().unwrap().parse::<f64>().unwrap();
assert_eq!(headers["x-cache-status"], "miss");
assert_eq!(res.text().await.unwrap(), "hello world");
// HIT on png
let res = send_vary_req(url, vary_field, "image/png").await;
assert_eq!(res.status(), StatusCode::OK);
let headers = res.headers();
assert_eq!(
epoch,
headers["x-epoch"].to_str().unwrap().parse::<f64>().unwrap()
);
assert_eq!(headers["x-cache-status"], "hit");
assert_eq!(res.text().await.unwrap(), "hello world");
// Vary header ignored -> get png, not jpeg
let res = send_vary_req(url, vary_field, "image/jpeg").await;
assert_eq!(res.status(), StatusCode::OK);
let headers = res.headers();
assert_eq!(
epoch,
headers["x-epoch"].to_str().unwrap().parse::<f64>().unwrap()
);
assert_eq!(headers["x-cache-status"], "hit");
}
#[tokio::test]
async fn test_vary_some_ignored() {
init();
let url = "http://127.0.0.1:6148/unique/test_vary_some_ignored/vary";
let vary_header = "Accept, SomeIgnoredVaryHeader";
// Make a request where we vary on some headers, and provide values for those.
let res = send_vary_req_with_headers(
url,
vary_header,
vec![
("SomeIgnoredVaryHeader", "image/webp"),
("Accept", "image/webp"),
],
)
.await;
assert_eq!(res.status(), StatusCode::OK);
let headers = res.headers();
assert_eq!(headers["x-cache-status"], "miss");
let epoch1 = headers["x-epoch"].to_str().unwrap().parse::<f64>().unwrap();
assert_eq!(res.text().await.unwrap(), "hello world");
// Identical request should yield a HIT.
let res = send_vary_req_with_headers(
url,
vary_header,
vec![
("SomeIgnoredVaryHeader", "image/webp"),
("Accept", "image/webp"),
],
)
.await;
assert_eq!(res.status(), StatusCode::OK);
let headers = res.headers();
assert_eq!(headers["x-cache-status"], "hit");
assert_eq!(
epoch1,
headers["x-epoch"].to_str().unwrap().parse::<f64>().unwrap()
);
assert_eq!(res.text().await.unwrap(), "hello world");
// Hit when changing a header we don't vary on.
let res = send_vary_req_with_headers(
url,
vary_header,
vec![
("SomeIgnoredVaryHeader", "definitely-not-webp"),
("Accept", "image/webp"),
],
)
.await;
assert_eq!(res.status(), StatusCode::OK);
let headers = res.headers();
assert_eq!(headers["x-cache-status"], "hit");
assert_eq!(
epoch1,
headers["x-epoch"].to_str().unwrap().parse::<f64>().unwrap()
);
assert_eq!(res.text().await.unwrap(), "hello world");
// Get a secondary variant by changing Accept.
let res = send_vary_req_with_headers(
url,
vary_header,
vec![
("SomeIgnoredVaryHeader", "definitely-not-webp"),
("Accept", "image/jpeg"),
],
)
.await;
assert_eq!(res.status(), StatusCode::OK);
let headers = res.headers();
assert_eq!(headers["x-cache-status"], "miss");
let epoch2 = headers["x-epoch"].to_str().unwrap().parse::<f64>().unwrap();
assert_ne!(epoch1, epoch2);
assert_eq!(res.text().await.unwrap(), "hello world");
// Cache hit on secondary variant.
let res = send_vary_req_with_headers(
url,
vary_header,
vec![
("SomeIgnoredVaryHeader", "definitely-not-webp"),
("Accept", "image/jpeg"),
],
)
.await;
assert_eq!(res.status(), StatusCode::OK);
let headers = res.headers();
assert_eq!(headers["x-cache-status"], "hit");
assert_eq!(
epoch2,
headers["x-epoch"].to_str().unwrap().parse::<f64>().unwrap()
);
// Cache hit on primary variant.
let res = send_vary_req_with_headers(
url,
vary_header,
vec![
("SomeIgnoredVaryHeader", "definitely-not-webp"),
("Accept", "image/webp"),
],
)
.await;
assert_eq!(res.status(), StatusCode::OK);
let headers = res.headers();
assert_eq!(headers["x-cache-status"], "hit");
assert_eq!(
epoch1,
headers["x-epoch"].to_str().unwrap().parse::<f64>().unwrap()
);
}
#[tokio::test]
async fn test_vary_dup_header_some_ignored() {
init();
let url = "http://127.0.0.1:6148/unique/test_vary_dup_header_some_ignored/vary";
let first_vary_header = "SomeIgnoredVaryHeader";
let dup_vary_header = "Accept";
// Make a request where we vary on some headers, and provide values for those.
let res = send_vary_req_with_headers_with_dups(
url,
first_vary_header,
vec![
("SomeIgnoredVaryHeader", "image/webp"),
("Accept", "image/webp"),
],
vec![("set-vary", dup_vary_header)],
)
.await;
assert_eq!(res.status(), StatusCode::OK);
let headers = res.headers();
assert_eq!(headers["x-cache-status"], "miss");
let epoch1 = headers["x-epoch"].to_str().unwrap().parse::<f64>().unwrap();
assert_eq!(res.text().await.unwrap(), "hello world");
// Identical request should yield a HIT.
let res = send_vary_req_with_headers_with_dups(
url,
first_vary_header,
vec![
("SomeIgnoredVaryHeader", "image/webp"),
("Accept", "image/webp"),
],
vec![("set-vary", dup_vary_header)],
)
.await;
assert_eq!(res.status(), StatusCode::OK);
let headers = res.headers();
assert_eq!(headers["x-cache-status"], "hit");
assert_eq!(
epoch1,
headers["x-epoch"].to_str().unwrap().parse::<f64>().unwrap()
);
assert_eq!(res.text().await.unwrap(), "hello world");
// Get a secondary variant by changing Accept.
let res = send_vary_req_with_headers_with_dups(
url,
first_vary_header,
vec![
("SomeIgnoredVaryHeader", "image/webp"),
("Accept", "image/jpeg"),
],
vec![("set-vary", dup_vary_header)],
)
.await;
assert_eq!(res.status(), StatusCode::OK);
let headers = res.headers();
assert_eq!(headers["x-cache-status"], "miss");
let epoch2 = headers["x-epoch"].to_str().unwrap().parse::<f64>().unwrap();
assert_ne!(epoch1, epoch2);
assert_eq!(res.text().await.unwrap(), "hello world");
}
#[tokio::test]
async fn test_vary_purge() {
init();
let url = "http://127.0.0.1:6148/unique/test_vary_purge/now";
let url = "http://127.0.0.1:6148/unique/test_vary_purge/vary";
let vary_field = "Accept";
let opt1 = "image/png";
let opt2 = "image/jpeg";
send_vary_req(url, "a").await;
let res = send_vary_req(url, "a").await;
send_vary_req(url, vary_field, opt1).await;
let res = send_vary_req(url, vary_field, opt1).await;
assert_eq!(res.status(), StatusCode::OK);
let headers = res.headers();
assert_eq!(headers["x-cache-status"], "hit");
send_vary_req(url, "b").await;
let res = send_vary_req(url, "b").await;
send_vary_req(url, vary_field, opt2).await;
let res = send_vary_req(url, vary_field, opt2).await;
assert_eq!(res.status(), StatusCode::OK);
let headers = res.headers();
assert_eq!(headers["x-cache-status"], "hit");
@ -1657,12 +1906,12 @@ mod test_cache {
//both should be miss
let res = send_vary_req(url, "a").await;
let res = send_vary_req(url, vary_field, opt1).await;
assert_eq!(res.status(), StatusCode::OK);
let headers = res.headers();
assert_eq!(headers["x-cache-status"], "miss");
let res = send_vary_req(url, "b").await;
let res = send_vary_req(url, vary_field, opt2).await;
assert_eq!(res.status(), StatusCode::OK);
let headers = res.headers();
assert_eq!(headers["x-cache-status"], "miss");

View file

@ -218,7 +218,7 @@ http {
return 200 "hello world";
}
location /revalidate_vary {
location /vary {
header_filter_by_lua_block {
ngx.header["Last-Modified"] = "Tue, 03 May 2022 01:04:39 GMT"
ngx.header["Etag"] = '"abcd"'
@ -226,10 +226,7 @@ http {
if h["set-vary"] then
ngx.header["Vary"] = h["set-vary"]
end
if h["set-no-vary"] then
-- expects proxy to force return no variance with this
ngx.header["Vary"] = "x-no-vary"
end
ngx.header["x-epoch"] = ngx.now()
if not h["x-no-revalidate"] and (h["if-modified-since"] or h["if-none-match"]) then
-- just assume they match
return ngx.exit(304)

View file

@ -14,6 +14,8 @@
use super::cert;
use async_trait::async_trait;
use http::header::VARY;
use http::HeaderValue;
use once_cell::sync::Lazy;
use pingora_cache::cache_control::CacheControl;
use pingora_cache::key::HashBinary;
@ -31,6 +33,7 @@ use pingora_core::utils::CertKey;
use pingora_error::{Error, ErrorSource, Result};
use pingora_http::{RequestHeader, ResponseHeader};
use pingora_proxy::{ProxyHttp, Session};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::thread;
use structopt::StructOpt;
@ -269,6 +272,9 @@ static CACHE_PREDICTOR: Lazy<Predictor<32>> = Lazy::new(|| Predictor::new(5, Non
static EVICTION_MANAGER: Lazy<Manager> = Lazy::new(|| Manager::new(8192)); // 8192 bytes
static CACHE_LOCK: Lazy<CacheLock> =
Lazy::new(|| CacheLock::new(std::time::Duration::from_secs(2)));
// Example of how one might restrict which fields can be varied on.
static CACHE_VARY_ALLOWED_HEADERS: Lazy<Option<HashSet<&str>>> =
Lazy::new(|| Some(vec!["accept", "accept-encoding"].into_iter().collect()));
// #[allow(clippy::upper_case_acronyms)]
pub struct CacheCTX {
@ -342,15 +348,41 @@ impl ProxyHttp for ExampleProxyCache {
fn cache_vary_filter(
&self,
_meta: &CacheMeta,
meta: &CacheMeta,
_ctx: &mut Self::CTX,
req: &RequestHeader,
) -> Option<HashBinary> {
// Here the response is always vary on request header "x-vary-me" if it exists
// in the real world, this callback should check Vary response header to decide
let vary_me = req.headers.get("x-vary-me")?;
let mut key = VarianceBuilder::new();
key.add_value("headers.x-vary-me", vary_me);
// Vary per header from origin. Target headers are de-duplicated by key logic.
let vary_headers_lowercased: Vec<String> = meta
.headers()
.get_all(VARY)
.iter()
// Filter out any unparseable vary headers.
.flat_map(|vary_header| vary_header.to_str().ok())
.flat_map(|vary_header| vary_header.split(','))
.map(|s| s.trim().to_lowercase())
.filter(|header_name| {
// Filter only for allowed headers, if restricted.
CACHE_VARY_ALLOWED_HEADERS
.as_ref()
.map(|al| al.contains(header_name.as_str()))
.unwrap_or(true)
})
.collect();
vary_headers_lowercased.iter().for_each(|header_name| {
// Add this header and value to be considered in the variance key.
key.add_value(
header_name,
req.headers
.get(header_name)
.map(|v| v.as_bytes())
.unwrap_or(&[]),
);
});
key.finalize()
}