Skip to content

Commit 830baac

Browse files
committed
show pending CDN invalidations on queue page
1 parent f4828ff commit 830baac

File tree

5 files changed

+199
-23
lines changed

5 files changed

+199
-23
lines changed

src/cdn.rs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use aws_sdk_cloudfront::{
44
model::{InvalidationBatch, Paths},
55
Client, RetryConfig,
66
};
7+
use chrono::{DateTime, Utc};
8+
use serde::Serialize;
79
use std::sync::{Arc, Mutex};
810
use strum::EnumString;
911
use tokio::runtime::Runtime;
@@ -133,14 +135,51 @@ pub(crate) fn invalidate_crate(config: &Config, cdn: &CdnBackend, name: &str) ->
133135
Ok(())
134136
}
135137

138+
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
139+
pub(crate) struct CrateInvalidation {
140+
pub name: String,
141+
pub created: DateTime<Utc>,
142+
}
143+
144+
/// Return fake active cloudfront invalidations.
145+
/// CloudFront invalidations can take up to 15 minutes. Until we have
146+
/// live queries of the invalidation status we just assume it's fine
147+
/// latest 20 minutes after the build.
148+
/// TODO: should be replaced be keeping track or querying the active invalidation from CloudFront
149+
pub(crate) fn active_crate_invalidations(
150+
conn: &mut postgres::Client,
151+
) -> Result<Vec<CrateInvalidation>> {
152+
Ok(conn
153+
.query(
154+
r#"
155+
SELECT
156+
crates.name,
157+
MIN(builds.build_time) as build_time
158+
FROM crates
159+
INNER JOIN releases ON crates.id = releases.crate_id
160+
INNER JOIN builds ON releases.id = builds.rid
161+
WHERE builds.build_time >= CURRENT_TIMESTAMP - INTERVAL '20 minutes'
162+
GROUP BY crates.name
163+
ORDER BY MIN(builds.build_time)"#,
164+
&[],
165+
)?
166+
.iter()
167+
.map(|row| CrateInvalidation {
168+
name: row.get(0),
169+
created: row.get(1),
170+
})
171+
.collect())
172+
}
173+
136174
#[cfg(test)]
137175
mod tests {
138176
use super::*;
139-
use crate::test::wrapper;
177+
use crate::test::{wrapper, FakeBuild};
140178

141179
use aws_sdk_cloudfront::{Client, Config, Credentials, Region};
142180
use aws_smithy_client::{erase::DynConnector, test_connection::TestConnection};
143181
use aws_smithy_http::body::SdkBody;
182+
use chrono::{Duration, Timelike};
144183

145184
#[test]
146185
fn create_cloudfront() {
@@ -213,6 +252,56 @@ mod tests {
213252
Config::new(&cfg)
214253
}
215254

255+
#[test]
256+
fn get_active_invalidations() {
257+
wrapper(|env| {
258+
let now = Utc::now().with_nanosecond(0).unwrap();
259+
let past_deploy = now - Duration::minutes(21);
260+
let first_running_deploy = now - Duration::minutes(10);
261+
let second_running_deploy = now;
262+
263+
env.fake_release()
264+
.name("krate_2")
265+
.version("0.0.1")
266+
.builds(vec![FakeBuild::default().build_time(first_running_deploy)])
267+
.create()?;
268+
269+
env.fake_release()
270+
.name("krate_2")
271+
.version("0.0.2")
272+
.builds(vec![FakeBuild::default().build_time(second_running_deploy)])
273+
.create()?;
274+
275+
env.fake_release()
276+
.name("krate_1")
277+
.version("0.0.2")
278+
.builds(vec![FakeBuild::default().build_time(second_running_deploy)])
279+
.create()?;
280+
281+
env.fake_release()
282+
.name("krate_1")
283+
.version("0.0.3")
284+
.builds(vec![FakeBuild::default().build_time(past_deploy)])
285+
.create()?;
286+
287+
assert_eq!(
288+
active_crate_invalidations(&mut env.db().conn())?,
289+
vec![
290+
CrateInvalidation {
291+
name: "krate_2".into(),
292+
created: first_running_deploy,
293+
},
294+
CrateInvalidation {
295+
name: "krate_1".into(),
296+
created: second_running_deploy,
297+
}
298+
]
299+
);
300+
301+
Ok(())
302+
})
303+
}
304+
216305
#[tokio::test]
217306
async fn invalidate_path() {
218307
let conn = TestConnection::new(vec![(

src/test/fakes.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ pub(crate) struct FakeRelease<'a> {
3838
pub(crate) struct FakeBuild {
3939
s3_build_log: Option<String>,
4040
db_build_log: Option<String>,
41+
build_time: Option<DateTime<Utc>>,
4142
result: BuildResult,
4243
}
4344

@@ -458,6 +459,12 @@ impl FakeGithubStats {
458459
}
459460

460461
impl FakeBuild {
462+
pub(crate) fn build_time(self, build_time: impl Into<DateTime<Utc>>) -> Self {
463+
Self {
464+
build_time: Some(build_time.into()),
465+
..self
466+
}
467+
}
461468
pub(crate) fn rustc_version(self, rustc_version: impl Into<String>) -> Self {
462469
Self {
463470
result: BuildResult {
@@ -525,6 +532,13 @@ impl FakeBuild {
525532
)?;
526533
}
527534

535+
if let Some(build_time) = self.build_time.as_ref() {
536+
conn.query(
537+
"UPDATE builds SET build_time = $2 WHERE id = $1",
538+
&[&build_id, &build_time],
539+
)?;
540+
}
541+
528542
if let Some(s3_build_log) = self.s3_build_log.as_deref() {
529543
let path = format!("build-logs/{}/{}.txt", build_id, default_target);
530544
storage.store_one(path, s3_build_log)?;
@@ -539,6 +553,7 @@ impl Default for FakeBuild {
539553
Self {
540554
s3_build_log: Some("It works!".into()),
541555
db_build_log: None,
556+
build_time: None,
542557
result: BuildResult {
543558
rustc_version: "rustc 2.0.0-nightly (000000000 1970-01-01)".into(),
544559
docsrs_version: "docs.rs 1.0.0 (000000000 1970-01-01)".into(),

src/web/releases.rs

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
use crate::{
44
build_queue::QueuedCrate,
5+
cdn::{self, CrateInvalidation},
56
db::{Pool, PoolClient},
67
impl_webpage,
78
utils::report_error,
@@ -668,6 +669,7 @@ pub fn activity_handler(req: &mut Request) -> IronResult<Response> {
668669
struct BuildQueuePage {
669670
description: &'static str,
670671
queue: Vec<QueuedCrate>,
672+
active_deployments: Vec<CrateInvalidation>,
671673
}
672674

673675
impl_webpage! {
@@ -683,9 +685,12 @@ pub fn build_queue_handler(req: &mut Request) -> IronResult<Response> {
683685
krate.priority = -krate.priority;
684686
}
685687

688+
let mut conn = extension!(req, Pool).get()?;
689+
686690
BuildQueuePage {
687-
description: "List of crates scheduled to build",
691+
description: "crate documentation scheduled to build & deploy",
688692
queue,
693+
active_deployments: ctry!(req, cdn::active_crate_invalidations(&mut conn)),
689694
}
690695
.into_response(req)
691696
}
@@ -695,7 +700,8 @@ mod tests {
695700
use super::*;
696701
use crate::index::api::CrateOwner;
697702
use crate::test::{
698-
assert_redirect, assert_redirect_unchecked, assert_success, wrapper, TestFrontend,
703+
assert_redirect, assert_redirect_unchecked, assert_success, wrapper, FakeBuild,
704+
TestFrontend,
699705
};
700706
use anyhow::Error;
701707
use chrono::{Duration, TimeZone};
@@ -1326,6 +1332,40 @@ mod tests {
13261332
})
13271333
}
13281334

1335+
#[test]
1336+
fn test_deployment_queue() {
1337+
wrapper(|env| {
1338+
let web = env.frontend();
1339+
1340+
env.fake_release()
1341+
.name("krate_2")
1342+
.version("0.0.1")
1343+
.builds(vec![
1344+
FakeBuild::default().build_time(Utc::now() - Duration::minutes(10))
1345+
])
1346+
.create()?;
1347+
1348+
let empty = kuchiki::parse_html().one(web.get("/releases/queue").send()?.text()?);
1349+
assert!(empty
1350+
.select(".release > strong")
1351+
.expect("missing heading")
1352+
.any(|el| el.text_contents().contains("active CDN deployments")));
1353+
1354+
let full = kuchiki::parse_html().one(web.get("/releases/queue").send()?.text()?);
1355+
let items = full
1356+
.select(".queue-list > li")
1357+
.expect("missing list items")
1358+
.collect::<Vec<_>>();
1359+
1360+
assert_eq!(items.len(), 1);
1361+
let a = items[0].as_node().select_first("a").expect("missing link");
1362+
1363+
assert!(a.text_contents().contains("krate_2"));
1364+
1365+
Ok(())
1366+
});
1367+
}
1368+
13291369
#[test]
13301370
fn test_releases_queue() {
13311371
wrapper(|env| {
@@ -1334,10 +1374,15 @@ mod tests {
13341374

13351375
let empty = kuchiki::parse_html().one(web.get("/releases/queue").send()?.text()?);
13361376
assert!(empty
1337-
.select(".release > strong")
1377+
.select(".queue-list > strong")
13381378
.expect("missing heading")
13391379
.any(|el| el.text_contents().contains("nothing")));
13401380

1381+
assert!(!empty
1382+
.select(".release > strong")
1383+
.expect("missing heading")
1384+
.any(|el| el.text_contents().contains("active CDN deployments")));
1385+
13411386
queue.add_crate("foo", "1.0.0", 0, None)?;
13421387
queue.add_crate("bar", "0.1.0", -10, None)?;
13431388
queue.add_crate("baz", "0.0.1", 10, None)?;

templates/footer.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<div class="docs-rs-footer">
22
<a href="/about">About docs.rs</a>
33
<a href="https://foundation.rust-lang.org/policies/privacy-policy/#docs.rs">Privacy policy</a>
4-
<a href="/releases/queue">Build queue</a>
4+
<a href="/releases/queue">Queue</a>
55
</div>

templates/releases/build_queue.html

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,64 @@
11
{%- extends "base.html" -%}
22
{%- import "releases/header.html" as release_macros -%}
33

4-
{%- block title -%}Build Queue - Docs.rs{%- endblock title -%}
4+
{%- block title -%}Queue - Docs.rs{%- endblock title -%}
55

66
{%- block header -%}
7-
{{ release_macros::header(title="Build Queue", description=description, tab="queue") }}
7+
{{ release_macros::header(title="Queue", description=description, tab="queue") }}
88
{%- endblock header -%}
99

1010
{%- block body -%}
1111
<div class="container">
1212
<div class="recent-releases-container">
13+
{%- if active_deployments %}
14+
<div class="release">
15+
<strong>active CDN deployments</strong>
16+
</div>
17+
18+
<div class = "pure-g">
19+
<div class="pure-u-1-2">
20+
<ol class="queue-list">
21+
{% for invalidation in active_deployments -%}
22+
<li>
23+
<a href="https://docs.rs/{{ invalidation.name }}">
24+
{{ invalidation.name }}
25+
</a>
26+
</li>
27+
{%- endfor %}
28+
</ol>
29+
</div>
30+
<div class="pure-u-1-2">
31+
<div class="about">
32+
<p>
33+
After the build finishes it may take up to 20 minutes for all documentation
34+
pages to be up-to-date and available to everybody.
35+
</p>
36+
<p>Especially <code>/latest/</code> URLs might be affected.</p>
37+
</div>
38+
</div>
39+
</div>
40+
{%- endif %}
1341

1442
<div class="release">
15-
{% set queue_length = queue | length -%}
16-
{%- if queue_length == 0 -%}
17-
<strong>There is nothing in the queue</strong>
18-
{%- else -%}
19-
<strong>Queue</strong>
20-
{%- endif %}
43+
<strong>Build Queue</strong>
2144
</div>
2245

2346
<ol class="queue-list">
24-
{% for crate in queue -%}
25-
<li>
26-
<a href="https://crates.io/crates/{{ crate.name }}">
27-
{{ crate.name }} {{ crate.version }}
28-
</a>
47+
{%- if queue -%}
48+
{% for crate in queue -%}
49+
<li>
50+
<a href="https://crates.io/crates/{{ crate.name }}">
51+
{{ crate.name }} {{ crate.version }}
52+
</a>
2953

30-
{% if crate.priority != 0 -%}
31-
(priority: {{ crate.priority }})
32-
{%- endif %}
33-
</li>
34-
{%- endfor %}
54+
{% if crate.priority != 0 -%}
55+
(priority: {{ crate.priority }})
56+
{%- endif %}
57+
</li>
58+
{%- endfor %}
59+
{%- else %}
60+
<strong>There is nothing in the queue</strong>
61+
{%- endif %}
3562
</ol>
3663
</div>
3764
</div>

0 commit comments

Comments
 (0)