Skip to content

Commit a669d3c

Browse files
committed
new cache-policy & cache middleware structure to support full page caching
1 parent 68cb269 commit a669d3c

File tree

11 files changed

+564
-244
lines changed

11 files changed

+564
-244
lines changed

src/config.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ pub struct Config {
6161
// If both are absent, don't generate the header. If only one is present,
6262
// generate just that directive. Values are in seconds.
6363
pub(crate) cache_control_stale_while_revalidate: Option<u32>,
64-
pub(crate) cache_control_max_age: Option<u32>,
6564

6665
pub(crate) cdn_backend: CdnKind,
6766

@@ -145,7 +144,6 @@ impl Config {
145144
cache_control_stale_while_revalidate: maybe_env(
146145
"CACHE_CONTROL_STALE_WHILE_REVALIDATE",
147146
)?,
148-
cache_control_max_age: maybe_env("CACHE_CONTROL_MAX_AGE")?,
149147

150148
cdn_backend: env("DOCSRS_CDN_BACKEND", CdnKind::Dummy)?,
151149

src/test/mod.rs

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@ use crate::db::{Pool, PoolClient};
66
use crate::error::Result;
77
use crate::repositories::RepositoryStatsUpdater;
88
use crate::storage::{Storage, StorageKind};
9-
use crate::web::Server;
9+
use crate::web::{cache, Server};
1010
use crate::{BuildQueue, Config, Context, Index, Metrics};
1111
use anyhow::Context as _;
1212
use fn_error_context::context;
13+
use iron::headers::CacheControl;
1314
use log::error;
1415
use once_cell::unsync::OnceCell;
1516
use postgres::Client as Connection;
1617
use reqwest::{
17-
blocking::{Client, ClientBuilder, RequestBuilder},
18+
blocking::{Client, ClientBuilder, RequestBuilder, Response},
1819
Method,
1920
};
2021
use std::{fs, net::SocketAddr, panic, sync::Arc, time::Duration};
@@ -43,21 +44,74 @@ pub(crate) fn wrapper(f: impl FnOnce(&TestEnvironment) -> Result<()>) {
4344
}
4445
}
4546

47+
/// check a request if the cache control header matches NoCache
48+
pub(crate) fn assert_no_cache(res: &Response) {
49+
assert_eq!(
50+
res.headers()
51+
.get("Cache-Control")
52+
.expect("missing cache-control header"),
53+
cache::NO_CACHE,
54+
);
55+
}
56+
57+
/// check a request if the cache control header matches the given cache config.
58+
pub(crate) fn assert_cache_control(
59+
res: &Response,
60+
cache_policy: cache::CachePolicy,
61+
config: &Config,
62+
) {
63+
assert!(config.cache_control_stale_while_revalidate.is_some());
64+
assert_eq!(
65+
res.headers()
66+
.get("Cache-Control")
67+
.expect("missing cache-control header"),
68+
&CacheControl(cache_policy.render(config)).to_string()
69+
);
70+
}
71+
4672
/// Make sure that a URL returns a status code between 200-299
4773
pub(crate) fn assert_success(path: &str, web: &TestFrontend) -> Result<()> {
4874
let status = web.get(path).send()?.status();
4975
assert!(status.is_success(), "failed to GET {}: {}", path, status);
5076
Ok(())
5177
}
5278

79+
/// Make sure that a URL returns a status code between 200-299,
80+
/// also check the cache-control headers.
81+
pub(crate) fn assert_success_cached(
82+
path: &str,
83+
web: &TestFrontend,
84+
cache_policy: cache::CachePolicy,
85+
config: &Config,
86+
) -> Result<()> {
87+
let response = web.get(path).send()?;
88+
assert_cache_control(&response, cache_policy, config);
89+
let status = response.status();
90+
assert!(status.is_success(), "failed to GET {}: {}", path, status);
91+
Ok(())
92+
}
93+
5394
/// Make sure that a URL returns a 404
5495
pub(crate) fn assert_not_found(path: &str, web: &TestFrontend) -> Result<()> {
55-
let status = web.get(path).send()?.status();
56-
assert_eq!(status, 404, "GET {} should have been a 404", path);
96+
let response = web.get(path).send()?;
97+
98+
// for now, 404s should always have `no-cache`
99+
assert_no_cache(&response);
100+
101+
assert_eq!(
102+
response.status(),
103+
404,
104+
"GET {} should have been a 404",
105+
path
106+
);
57107
Ok(())
58108
}
59109

60-
fn assert_redirect_common(path: &str, expected_target: &str, web: &TestFrontend) -> Result<()> {
110+
fn assert_redirect_common(
111+
path: &str,
112+
expected_target: &str,
113+
web: &TestFrontend,
114+
) -> Result<Response> {
61115
let response = web.get_no_redirect(path).send()?;
62116
let status = response.status();
63117
if !status.is_redirection() {
@@ -84,7 +138,7 @@ fn assert_redirect_common(path: &str, expected_target: &str, web: &TestFrontend)
84138
anyhow::bail!("got redirect to {redirect_target}");
85139
}
86140

87-
Ok(())
141+
Ok(response)
88142
}
89143

90144
/// Makes sure that a URL redirects to a specific page, but doesn't check that the target exists
@@ -94,7 +148,7 @@ pub(crate) fn assert_redirect_unchecked(
94148
expected_target: &str,
95149
web: &TestFrontend,
96150
) -> Result<()> {
97-
assert_redirect_common(path, expected_target, web)
151+
assert_redirect_common(path, expected_target, web).map(|_| ())
98152
}
99153

100154
/// Make sure that a URL redirects to a specific page, and that the target exists and is not another redirect
@@ -111,6 +165,27 @@ pub(crate) fn assert_redirect(path: &str, expected_target: &str, web: &TestFront
111165
Ok(())
112166
}
113167

168+
/// Make sure that a URL redirects to a specific page, and that the target exists and is not another redirect
169+
#[context("expected redirect from {path} to {expected_target}")]
170+
pub(crate) fn assert_redirect_cached(
171+
path: &str,
172+
expected_target: &str,
173+
cache_policy: cache::CachePolicy,
174+
web: &TestFrontend,
175+
config: &Config,
176+
) -> Result<()> {
177+
let redirect_response = assert_redirect_common(path, expected_target, web)?;
178+
assert_cache_control(&redirect_response, cache_policy, config);
179+
180+
let response = web.get_no_redirect(expected_target).send()?;
181+
let status = response.status();
182+
if !status.is_success() {
183+
anyhow::bail!("failed to GET {expected_target}: {status}");
184+
}
185+
186+
Ok(())
187+
}
188+
114189
pub(crate) struct TestEnvironment {
115190
build_queue: OnceCell<Arc<BuildQueue>>,
116191
config: OnceCell<Arc<Config>>,
@@ -188,6 +263,10 @@ impl TestEnvironment {
188263
config.local_archive_cache_path =
189264
std::env::temp_dir().join(format!("docsrs-test-index-{}", rand::random::<u64>()));
190265

266+
// set stale content serving so Cache::ForeverOnlyInCdn and Cache::ForeverInCdnAndStaleInBrowser
267+
// are actually different.
268+
config.cache_control_stale_while_revalidate = Some(86400);
269+
191270
config
192271
}
193272

src/web/builds.rs

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::{match_version, redirect_base, MatchSemver};
1+
use super::{cache::CachePolicy, match_version, redirect_base, MatchSemver};
22
use crate::{
33
db::Pool,
44
docbuilder::Limits,
@@ -7,9 +7,7 @@ use crate::{
77
};
88
use chrono::{DateTime, Utc};
99
use iron::{
10-
headers::{
11-
AccessControlAllowOrigin, CacheControl, CacheDirective, ContentType, Expires, HttpDate,
12-
},
10+
headers::{AccessControlAllowOrigin, ContentType},
1311
status, IronResult, Request, Response, Url,
1412
};
1513
use router::Router;
@@ -107,12 +105,8 @@ pub fn build_list_handler(req: &mut Request) -> IronResult<Response> {
107105
if is_json {
108106
let mut resp = Response::with((status::Ok, serde_json::to_string(&builds).unwrap()));
109107
resp.headers.set(ContentType::json());
110-
resp.headers.set(Expires(HttpDate(time::now())));
111-
resp.headers.set(CacheControl(vec![
112-
CacheDirective::NoCache,
113-
CacheDirective::NoStore,
114-
CacheDirective::MustRevalidate,
115-
]));
108+
resp.extensions
109+
.insert::<CachePolicy>(CachePolicy::NoStoreMustRevalidate);
116110
resp.headers.set(AccessControlAllowOrigin::Any);
117111

118112
Ok(resp)
@@ -131,7 +125,10 @@ pub fn build_list_handler(req: &mut Request) -> IronResult<Response> {
131125

132126
#[cfg(test)]
133127
mod tests {
134-
use crate::test::{wrapper, FakeBuild};
128+
use crate::{
129+
test::{assert_cache_control, wrapper, FakeBuild},
130+
web::cache::CachePolicy,
131+
};
135132
use chrono::{DateTime, Duration, Utc};
136133
use kuchiki::traits::TendrilSink;
137134
use reqwest::StatusCode;
@@ -156,12 +153,9 @@ mod tests {
156153
])
157154
.create()?;
158155

159-
let page = kuchiki::parse_html().one(
160-
env.frontend()
161-
.get("/crate/foo/0.1.0/builds")
162-
.send()?
163-
.text()?,
164-
);
156+
let response = env.frontend().get("/crate/foo/0.1.0/builds").send()?;
157+
assert_cache_control(&response, CachePolicy::NoCaching, &env.config());
158+
let page = kuchiki::parse_html().one(response.text()?);
165159

166160
let rows: Vec<_> = page
167161
.select("ul > li a.release")
@@ -200,12 +194,9 @@ mod tests {
200194
])
201195
.create()?;
202196

203-
let value: serde_json::Value = serde_json::from_str(
204-
&env.frontend()
205-
.get("/crate/foo/0.1.0/builds.json")
206-
.send()?
207-
.text()?,
208-
)?;
197+
let response = env.frontend().get("/crate/foo/0.1.0/builds.json").send()?;
198+
assert_cache_control(&response, CachePolicy::NoStoreMustRevalidate, &env.config());
199+
let value: serde_json::Value = serde_json::from_str(&response.text()?)?;
209200

210201
assert_eq!(value.pointer("/0/build_status"), Some(&true.into()));
211202
assert_eq!(

0 commit comments

Comments
 (0)