Skip to content

Commit c02b5ee

Browse files
committed
optionally set caching headers for /latest/, invalidate caches after build
1 parent 8e71b4e commit c02b5ee

File tree

12 files changed

+271
-29
lines changed

12 files changed

+271
-29
lines changed

Cargo.lock

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,10 @@ tokio = { version = "1.0", features = ["rt-multi-thread"] }
7373
futures-util = "0.3.5"
7474
aws-config = "0.48.0"
7575
aws-sdk-s3 = "0.18.0"
76+
aws-sdk-cloudfront = "0.18.0"
7677
aws-smithy-types-convert = { version = "0.48.0", features = ["convert-chrono"] }
7778
http = "0.2.6"
79+
uuid = "1.1.2"
7880

7981
# Data serialization and deserialization
8082
serde = { version = "1.0", features = ["derive"] }

src/bin/cratesfyi.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use once_cell::sync::OnceCell;
1616
use sentry_log::SentryLogger;
1717
use structopt::StructOpt;
1818
use strum::VariantNames;
19+
use tokio::runtime::Runtime;
1920

2021
fn main() {
2122
let _sentry_guard = if let Ok(sentry_dsn) = env::var("SENTRY_DSN") {
@@ -563,6 +564,7 @@ struct BinContext {
563564
metrics: OnceCell<Arc<Metrics>>,
564565
index: OnceCell<Arc<Index>>,
565566
repository_stats_updater: OnceCell<Arc<RepositoryStatsUpdater>>,
567+
runtime: OnceCell<Arc<Runtime>>,
566568
}
567569

568570
impl BinContext {
@@ -575,6 +577,7 @@ impl BinContext {
575577
metrics: OnceCell::new(),
576578
index: OnceCell::new(),
577579
repository_stats_updater: OnceCell::new(),
580+
runtime: OnceCell::new(),
578581
}
579582
}
580583

@@ -606,9 +609,11 @@ impl Context for BinContext {
606609
self.pool()?,
607610
self.metrics()?,
608611
self.config()?,
612+
self.runtime()?,
609613
)?;
610614
fn config(self) -> Config = Config::from_env()?;
611615
fn metrics(self) -> Metrics = Metrics::new()?;
616+
fn runtime(self) -> Runtime = Runtime::new()?;
612617
fn index(self) -> Index = {
613618
let config = self.config()?;
614619
let path = config.registry_index_path.clone();

src/cache.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
use anyhow::Error;
2+
use aws_sdk_cloudfront::{
3+
model::{InvalidationBatch, Paths},
4+
Client, RetryConfig,
5+
};
6+
use tokio::runtime::Runtime;
7+
use uuid::Uuid;
8+
9+
/// create a CloudFront invalidation request for a list of path patterns.
10+
/// patterns can be
11+
/// * `/filename.ext` (a specific path)
12+
/// * `/directory-path/file-name.*` (delete these files, all extensions)
13+
/// * `/directory-path/*` (invalidate all of the files in a directory, without subdirectories)
14+
/// * `/directory-path*` (recursive directory delete, including subdirectories)
15+
/// see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Invalidation.html#invalidation-specifying-objects
16+
///
17+
/// Returns the caller reference that can be used to query the status of this
18+
/// invalidation request.
19+
pub(crate) fn create_cloudfront_invalidation(
20+
runtime: &Runtime,
21+
distribution_id: &str,
22+
path_patterns: &[&str],
23+
) -> Result<Uuid, Error> {
24+
let shared_config = runtime.block_on(aws_config::load_from_env());
25+
let config_builder = aws_sdk_cloudfront::config::Builder::from(&shared_config)
26+
.retry_config(RetryConfig::new().with_max_attempts(3));
27+
28+
let client = Client::from_conf(config_builder.build());
29+
let path_patterns: Vec<_> = path_patterns.iter().cloned().map(String::from).collect();
30+
31+
let caller_reference = Uuid::new_v4();
32+
33+
runtime.block_on(async {
34+
client
35+
.create_invalidation()
36+
.distribution_id(distribution_id)
37+
.invalidation_batch(
38+
InvalidationBatch::builder()
39+
.paths(
40+
Paths::builder()
41+
.quantity(path_patterns.len().try_into().unwrap())
42+
.set_items(Some(path_patterns))
43+
.build(),
44+
)
45+
.caller_reference(format!("{}", caller_reference))
46+
.build(),
47+
)
48+
.send()
49+
.await
50+
})?;
51+
52+
Ok(caller_reference)
53+
}

src/config.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,26 @@ pub struct Config {
5757
// Content Security Policy
5858
pub(crate) csp_report_only: bool,
5959

60-
// Cache-Control header
60+
// Cache-Control header, for versioned URLs.
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>,
6464
pub(crate) cache_control_max_age: Option<u32>,
6565

66+
// Cache-Control header, for /latest/ URLs.
67+
// Same conditions as above apply.
68+
pub(crate) cache_control_stale_while_revalidate_latest: Option<u32>,
69+
pub(crate) cache_control_max_age_latest: Option<u32>,
70+
71+
// CloudFront distribution ID for the web server.
72+
// Will be used for invalidation-requests.
73+
pub(crate) cloudfront_distribution_id_web: Option<String>,
74+
75+
// when invalidating, either
76+
// * invalidate the whole crate path (`/krate/*`)
77+
// * invalidate only the `/latest/` url
78+
pub(crate) cloudfront_only_invalidate_latest: bool,
79+
6680
// Build params
6781
pub(crate) build_attempts: u16,
6882
pub(crate) rustwide_workspace: PathBuf,
@@ -141,6 +155,15 @@ impl Config {
141155
)?,
142156
cache_control_max_age: maybe_env("CACHE_CONTROL_MAX_AGE")?,
143157

158+
cache_control_stale_while_revalidate_latest: maybe_env(
159+
"CACHE_CONTROL_STALE_WHILE_REVALIDATE_LATEST",
160+
)?,
161+
cache_control_max_age_latest: maybe_env("CACHE_CONTROL_MAX_AGE_LATEST")?,
162+
163+
cloudfront_distribution_id_web: maybe_env("CLOUDFRONT_DISTRIBUTION_ID_WEB")?,
164+
165+
cloudfront_only_invalidate_latest: env("CLOUDFRONT_ONLY_INVALIDATE_LATEST", false)?,
166+
144167
local_archive_cache_path: env(
145168
"DOCSRS_ARCHIVE_INDEX_CACHE_PATH",
146169
prefix.join("archive_cache"),

src/context.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::error::Result;
33
use crate::repositories::RepositoryStatsUpdater;
44
use crate::{BuildQueue, Config, Index, Metrics, Storage};
55
use std::sync::Arc;
6+
use tokio::runtime::Runtime;
67

78
pub trait Context {
89
fn config(&self) -> Result<Arc<Config>>;
@@ -12,4 +13,5 @@ pub trait Context {
1213
fn metrics(&self) -> Result<Arc<Metrics>>;
1314
fn index(&self) -> Result<Arc<Index>>;
1415
fn repository_stats_updater(&self) -> Result<Arc<RepositoryStatsUpdater>>;
16+
fn runtime(&self) -> Result<Arc<Runtime>>;
1517
}

src/docbuilder/rustwide_builder.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::cache;
12
use crate::db::file::add_path_into_database;
23
use crate::db::{
34
add_build_into_database, add_doc_coverage, add_package_into_database,
@@ -25,6 +26,7 @@ use rustwide::{AlternativeRegistry, Build, Crate, Toolchain, Workspace, Workspac
2526
use std::collections::{HashMap, HashSet};
2627
use std::path::Path;
2728
use std::sync::Arc;
29+
use tokio::runtime::Runtime;
2830

2931
const USER_AGENT: &str = "docs.rs builder (https://github.com/rust-lang/docs.rs)";
3032
const DUMMY_CRATE_NAME: &str = "empty-library";
@@ -43,6 +45,7 @@ pub struct RustwideBuilder {
4345
db: Pool,
4446
storage: Arc<Storage>,
4547
metrics: Arc<Metrics>,
48+
runtime: Arc<Runtime>,
4649
index: Arc<Index>,
4750
rustc_version: String,
4851
repository_stats_updater: Arc<RepositoryStatsUpdater>,
@@ -80,6 +83,7 @@ impl RustwideBuilder {
8083
config,
8184
db: context.pool()?,
8285
storage: context.storage()?,
86+
runtime: context.runtime()?,
8387
metrics: context.metrics()?,
8488
index: context.index()?,
8589
rustc_version: String::new(),
@@ -500,6 +504,20 @@ impl RustwideBuilder {
500504
.purge_from_cache(&self.workspace)
501505
.map_err(FailureError::compat)?;
502506
local_storage.close()?;
507+
if let Some(distribution_id) = self.config.cloudfront_distribution_id_web.as_ref() {
508+
if successful {
509+
let invalidate_path = if self.config.cloudfront_only_invalidate_latest {
510+
format!("/{}/latest*", name)
511+
} else {
512+
format!("/{}*", name)
513+
};
514+
cache::create_cloudfront_invalidation(
515+
&self.runtime,
516+
distribution_id,
517+
&[&invalidate_path],
518+
)?;
519+
}
520+
}
503521
Ok(successful)
504522
}
505523

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub use self::storage::Storage;
1313
pub use self::web::Server;
1414

1515
mod build_queue;
16+
pub mod cache;
1617
mod config;
1718
mod context;
1819
pub mod db;

src/storage/mod.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use std::{
2121
path::{Path, PathBuf},
2222
sync::Arc,
2323
};
24+
use tokio::runtime::Runtime;
2425

2526
const MAX_CONCURRENT_UPLOADS: usize = 1000;
2627

@@ -113,14 +114,21 @@ pub struct Storage {
113114
}
114115

115116
impl Storage {
116-
pub fn new(pool: Pool, metrics: Arc<Metrics>, config: Arc<Config>) -> Result<Self> {
117+
pub fn new(
118+
pool: Pool,
119+
metrics: Arc<Metrics>,
120+
config: Arc<Config>,
121+
runtime: Arc<Runtime>,
122+
) -> Result<Self> {
117123
Ok(Storage {
118124
config: config.clone(),
119125
backend: match config.storage_backend {
120126
StorageKind::Database => {
121127
StorageBackend::Database(DatabaseBackend::new(pool, metrics))
122128
}
123-
StorageKind::S3 => StorageBackend::S3(Box::new(S3Backend::new(metrics, &config)?)),
129+
StorageKind::S3 => {
130+
StorageBackend::S3(Box::new(S3Backend::new(metrics, &config, runtime)?))
131+
}
124132
},
125133
})
126134
}

src/storage/s3.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,19 @@ use tokio::runtime::Runtime;
1818

1919
pub(super) struct S3Backend {
2020
client: Client,
21-
runtime: Runtime,
21+
runtime: Arc<Runtime>,
2222
bucket: String,
2323
metrics: Arc<Metrics>,
2424
#[cfg(test)]
2525
temporary: bool,
2626
}
2727

2828
impl S3Backend {
29-
pub(super) fn new(metrics: Arc<Metrics>, config: &Config) -> Result<Self, Error> {
30-
let runtime = Runtime::new()?;
31-
29+
pub(super) fn new(
30+
metrics: Arc<Metrics>,
31+
config: &Config,
32+
runtime: Arc<Runtime>,
33+
) -> Result<Self, Error> {
3234
let shared_config = runtime.block_on(aws_config::load_from_env());
3335
let mut config_builder = aws_sdk_s3::config::Builder::from(&shared_config)
3436
.retry_config(RetryConfig::new().with_max_attempts(3))

src/test/mod.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use reqwest::{
1717
Method,
1818
};
1919
use std::{fs, net::SocketAddr, panic, sync::Arc, time::Duration};
20+
use tokio::runtime::Runtime;
2021

2122
pub(crate) fn wrapper(f: impl FnOnce(&TestEnvironment) -> Result<()>) {
2223
let env = TestEnvironment::new();
@@ -114,6 +115,7 @@ pub(crate) struct TestEnvironment {
114115
db: OnceCell<TestDatabase>,
115116
storage: OnceCell<Arc<Storage>>,
116117
index: OnceCell<Arc<Index>>,
118+
runtime: OnceCell<Arc<Runtime>>,
117119
metrics: OnceCell<Arc<Metrics>>,
118120
frontend: OnceCell<TestFrontend>,
119121
repository_stats_updater: OnceCell<Arc<RepositoryStatsUpdater>>,
@@ -140,6 +142,7 @@ impl TestEnvironment {
140142
index: OnceCell::new(),
141143
metrics: OnceCell::new(),
142144
frontend: OnceCell::new(),
145+
runtime: OnceCell::new(),
143146
repository_stats_updater: OnceCell::new(),
144147
}
145148
}
@@ -216,8 +219,13 @@ impl TestEnvironment {
216219
self.storage
217220
.get_or_init(|| {
218221
Arc::new(
219-
Storage::new(self.db().pool(), self.metrics(), self.config())
220-
.expect("failed to initialize the storage"),
222+
Storage::new(
223+
self.db().pool(),
224+
self.metrics(),
225+
self.config(),
226+
self.runtime(),
227+
)
228+
.expect("failed to initialize the storage"),
221229
)
222230
})
223231
.clone()
@@ -228,6 +236,11 @@ impl TestEnvironment {
228236
.get_or_init(|| Arc::new(Metrics::new().expect("failed to initialize the metrics")))
229237
.clone()
230238
}
239+
pub(crate) fn runtime(&self) -> Arc<Runtime> {
240+
self.runtime
241+
.get_or_init(|| Arc::new(Runtime::new().expect("failed to initialize runtime")))
242+
.clone()
243+
}
231244

232245
pub(crate) fn index(&self) -> Arc<Index> {
233246
self.index
@@ -303,6 +316,10 @@ impl Context for TestEnvironment {
303316
fn repository_stats_updater(&self) -> Result<Arc<RepositoryStatsUpdater>> {
304317
Ok(self.repository_stats_updater())
305318
}
319+
320+
fn runtime(&self) -> Result<Arc<Runtime>> {
321+
Ok(self.runtime())
322+
}
306323
}
307324

308325
pub(crate) struct TestDatabase {

0 commit comments

Comments
 (0)