diff --git a/.sqlx/query-13d3402d8d5bd2f1fee96566ed633ac319d1d8169e9115215775968a047e4069.json b/.sqlx/query-13d3402d8d5bd2f1fee96566ed633ac319d1d8169e9115215775968a047e4069.json new file mode 100644 index 000000000..791fc49a7 --- /dev/null +++ b/.sqlx/query-13d3402d8d5bd2f1fee96566ed633ac319d1d8169e9115215775968a047e4069.json @@ -0,0 +1,29 @@ +{ + "db_name": "PostgreSQL", + "query": "\nSELECT c.name,\n r.version AS \"version: Version\"\nFROM crates AS c\n JOIN releases AS r\n ON c.id = r.crate_id\n JOIN release_build_status AS rbs\n ON rbs.rid = r.id\n JOIN builds AS b\n ON b.rid = r.id\n AND b.build_finished = rbs.last_build_time\n AND b.rustc_nightly_date >= $1\n AND b.rustc_nightly_date < $2\n\n\n", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "version: Version", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Date", + "Date" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "13d3402d8d5bd2f1fee96566ed633ac319d1d8169e9115215775968a047e4069" +} diff --git a/src/bin/cratesfyi.rs b/src/bin/cratesfyi.rs index 0f28c637c..b8be90127 100644 --- a/src/bin/cratesfyi.rs +++ b/src/bin/cratesfyi.rs @@ -1,9 +1,10 @@ use anyhow::{Context as _, Result, anyhow}; +use chrono::NaiveDate; use clap::{Parser, Subcommand, ValueEnum}; use docs_rs::{ Config, Context, Index, PackageKind, RustwideBuilder, db::{self, CrateId, Overrides, add_path_into_database, types::version::Version}, - start_background_metrics_webserver, start_web_server, + queue_rebuilds_faulty_rustdoc, start_background_metrics_webserver, start_web_server, utils::{ ConfigName, daemon::start_background_service_metric_collector, get_config, get_crate_pattern_and_priority, list_crate_priorities, queue_builder, @@ -274,6 +275,17 @@ enum QueueSubcommand { #[arg(long, conflicts_with("reference"))] head: bool, }, + + /// Queue rebuilds for broken nightly versions of rustdoc, either for a single date (start) or a range (start inclusive, end exclusive) + RebuildBrokenNightly { + /// Start date of nightly builds to rebuild (inclusive) + #[arg(name = "START", short = 's', long = "start")] + start_nightly_date: NaiveDate, + + /// End date of nightly builds to rebuild (exclusive, optional) + #[arg(name = "END", short = 'e', long = "end")] + end_nightly_date: Option, + }, } impl QueueSubcommand { @@ -316,6 +328,15 @@ impl QueueSubcommand { } Self::DefaultPriority { subcommand } => subcommand.handle_args(ctx)?, + + Self::RebuildBrokenNightly { start_nightly_date, end_nightly_date } => { + ctx.runtime.block_on(async move { + let mut conn = ctx.pool.get_async().await?; + let queued_rebuilds_amount = queue_rebuilds_faulty_rustdoc(&mut conn, &ctx.async_build_queue, &start_nightly_date, &end_nightly_date).await?; + println!("Queued {queued_rebuilds_amount} rebuilds for broken nightly versions of rustdoc"); + Ok::<(), anyhow::Error>(()) + })? + } } Ok(()) } diff --git a/src/build_queue.rs b/src/build_queue.rs index 84c4b037b..df42d8040 100644 --- a/src/build_queue.rs +++ b/src/build_queue.rs @@ -13,6 +13,7 @@ use crate::{ utils::{ConfigName, get_config, get_crate_priority, report_error, retry, set_config}, }; use anyhow::Context as _; +use chrono::NaiveDate; use fn_error_context::context; use futures_util::{StreamExt, stream::TryStreamExt}; use opentelemetry::metrics::Counter; @@ -39,11 +40,19 @@ impl BuildQueueMetrics { } } -/// The static priority for background rebuilds. -/// Used when queueing rebuilds, and when rendering them -/// collapsed in the UI. -/// For normal build priorities we use smaller values. -pub(crate) const REBUILD_PRIORITY: i32 = 20; +pub(crate) const PRIORITY_DEFAULT: i32 = 0; +/// Used for workspaces to avoid blocking the queue (done through the cratesfyi CLI, not used in code) +#[allow(dead_code)] +pub(crate) const PRIORITY_DEPRIORITIZED: i32 = 1; +/// Rebuilds triggered from crates.io, see issue #2442 +pub(crate) const PRIORITY_MANUAL_FROM_CRATES_IO: i32 = 5; +/// Used for rebuilds queued through cratesfyi for crate versions failed due to a broken Rustdoc nightly version. +/// Note: a broken rustdoc version does not necessarily imply a failed build. +pub(crate) const PRIORITY_BROKEN_RUSTDOC: i32 = 10; +/// Used by the synchronize cratesfyi command when queueing builds that are in the crates.io index but not in the database. +pub(crate) const PRIORITY_CONSISTENCY_CHECK: i32 = 15; +/// The static priority for background rebuilds, used when queueing rebuilds, and when rendering them collapsed in the UI. +pub(crate) const PRIORITY_CONTINUOUS: i32 = 20; #[derive(Debug, Clone, Eq, PartialEq, serde::Serialize)] pub(crate) struct QueuedCrate { @@ -723,7 +732,7 @@ pub async fn queue_rebuilds( .pending_count_by_priority() .await? .iter() - .filter_map(|(priority, count)| (*priority >= REBUILD_PRIORITY).then_some(count)) + .filter_map(|(priority, count)| (*priority >= PRIORITY_CONTINUOUS).then_some(count)) .sum(); let rebuilds_to_queue = config @@ -767,7 +776,7 @@ pub async fn queue_rebuilds( { info!("queueing rebuild for {} {}...", &row.name, &row.version); build_queue - .add_crate(&row.name, &row.version, REBUILD_PRIORITY, None) + .add_crate(&row.name, &row.version, PRIORITY_CONTINUOUS, None) .await?; } } @@ -775,9 +784,71 @@ pub async fn queue_rebuilds( Ok(()) } +/// Queue rebuilds for failed crates due to a faulty version of rustdoc +/// +/// It is assumed that the version of rustdoc matches the one of rustc, which is persisted in the DB. +/// The priority of the resulting rebuild requests will be lower than previously failed builds. +/// If a crate is already queued to be rebuilt, it will not be requeued. +/// Start date is inclusive, end date is exclusive. +#[instrument(skip_all)] +pub async fn queue_rebuilds_faulty_rustdoc( + conn: &mut sqlx::PgConnection, + build_queue: &AsyncBuildQueue, + start_nightly_date: &NaiveDate, + end_nightly_date: &Option, +) -> Result { + let end_nightly_date = + end_nightly_date.unwrap_or_else(|| start_nightly_date.succ_opt().unwrap()); + let mut results = sqlx::query!( + r#" +SELECT c.name, + r.version AS "version: Version" +FROM crates AS c + JOIN releases AS r + ON c.id = r.crate_id + JOIN release_build_status AS rbs + ON rbs.rid = r.id + JOIN builds AS b + ON b.rid = r.id + AND b.build_finished = rbs.last_build_time + AND b.rustc_nightly_date >= $1 + AND b.rustc_nightly_date < $2 + + +"#, + start_nightly_date, + end_nightly_date + ) + .fetch(&mut *conn); + + let mut results_count = 0; + while let Some(row) = results.next().await { + let row = row?; + + if !build_queue + .has_build_queued(&row.name, &row.version) + .await? + { + results_count += 1; + info!( + name=%row.name, + version=%row.version, + priority=PRIORITY_BROKEN_RUSTDOC, + "queueing rebuild" + ); + build_queue + .add_crate(&row.name, &row.version, PRIORITY_BROKEN_RUSTDOC, None) + .await?; + } + } + + Ok(results_count) +} + #[cfg(test)] mod tests { use super::*; + use crate::db::types::BuildStatus; use crate::test::{FakeBuild, TestEnvironment, V1, V2}; use chrono::Utc; @@ -812,7 +883,188 @@ mod tests { assert_eq!(queue.len(), 1); assert_eq!(queue[0].name, "foo"); assert_eq!(queue[0].version, V1); - assert_eq!(queue[0].priority, REBUILD_PRIORITY); + assert_eq!(queue[0].priority, PRIORITY_CONTINUOUS); + + Ok(()) + } + + /// Verifies whether a rebuild is queued for all releases with the latest build performed with a specific nightly version of rustdoc + #[tokio::test(flavor = "multi_thread")] + async fn test_rebuild_broken_rustdoc_specific_date_simple() -> Result<()> { + let env = TestEnvironment::new().await?; + + // Matrix of test builds (crate name, nightly date, version) + let build_matrix = [ + // Should be skipped since this is not the latest build for this release + ("foo1", NaiveDate::from_ymd_opt(2020, 10, 1).unwrap(), V1), + // All those should match + ("foo1", NaiveDate::from_ymd_opt(2020, 10, 2).unwrap(), V1), + ("foo1", NaiveDate::from_ymd_opt(2020, 10, 2).unwrap(), V2), + ("foo2", NaiveDate::from_ymd_opt(2020, 10, 2).unwrap(), V1), + // Should be skipped since the nightly doesn't match + ("foo2", NaiveDate::from_ymd_opt(2020, 10, 3).unwrap(), V2), + ]; + for build in build_matrix.into_iter() { + let (crate_name, nightly, version) = build; + env.fake_release() + .await + .name(crate_name) + .version(version) + .builds(vec![ + FakeBuild::default() + .rustc_version( + format!( + "rustc 1.84.0-nightly (e7c0d2750 {})", + nightly.format("%Y-%m-%d") + ) + .as_str(), + ) + .build_status(BuildStatus::Failure), + ]) + .create() + .await?; + } + + let build_queue = env.async_build_queue(); + assert!(build_queue.queued_crates().await?.is_empty()); + + let mut conn = env.async_db().async_conn().await; + queue_rebuilds_faulty_rustdoc( + &mut conn, + build_queue, + &NaiveDate::from_ymd_opt(2020, 10, 2).unwrap(), + &None, + ) + .await?; + + let queue = build_queue.queued_crates().await?; + assert_eq!(queue.len(), 3); + assert_eq!(queue[0].name, "foo1"); + assert_eq!(queue[0].version, V1); + assert_eq!(queue[0].priority, PRIORITY_BROKEN_RUSTDOC); + assert_eq!(queue[1].name, "foo1"); + assert_eq!(queue[1].version, V2); + assert_eq!(queue[1].priority, PRIORITY_BROKEN_RUSTDOC); + assert_eq!(queue[2].name, "foo2"); + assert_eq!(queue[2].version, V1); + assert_eq!(queue[2].priority, PRIORITY_BROKEN_RUSTDOC); + + Ok(()) + } + + /// Verifies whether a rebuild is NOT queued for any crate if the nightly specified doesn't match any latest build of any release + #[tokio::test(flavor = "multi_thread")] + async fn test_rebuild_broken_rustdoc_specific_date_skipped() -> Result<()> { + let env = TestEnvironment::new().await?; + + // Matrix of test builds (crate name, nightly date, version) + let build_matrix = [ + // Should be skipped since this is not the latest build for this release even if the nightly matches + ("foo1", NaiveDate::from_ymd_opt(2020, 10, 3).unwrap(), V1), + // Should be skipped since the nightly doesn't match + ("foo1", NaiveDate::from_ymd_opt(2020, 10, 2).unwrap(), V1), + // Should be skipped since the nightly doesn't match + ("foo2", NaiveDate::from_ymd_opt(2020, 10, 4).unwrap(), V1), + ]; + for build in build_matrix.into_iter() { + let (crate_name, nightly, version) = build; + env.fake_release() + .await + .name(crate_name) + .version(version) + .builds(vec![ + FakeBuild::default() + .rustc_version( + format!( + "rustc 1.84.0-nightly (e7c0d2750 {})", + nightly.format("%Y-%m-%d") + ) + .as_str(), + ) + .build_status(BuildStatus::Failure), + ]) + .create() + .await?; + } + + let build_queue = env.async_build_queue(); + assert!(build_queue.queued_crates().await?.is_empty()); + + let mut conn = env.async_db().async_conn().await; + queue_rebuilds_faulty_rustdoc( + &mut conn, + build_queue, + &NaiveDate::from_ymd_opt(2020, 10, 3).unwrap(), + &None, + ) + .await?; + + let queue = build_queue.queued_crates().await?; + assert_eq!(queue.len(), 0); + + Ok(()) + } + + /// Verifies whether a rebuild is queued for all releases with the latest build performed with a nightly version between two dates + #[tokio::test(flavor = "multi_thread")] + async fn test_rebuild_broken_rustdoc_date_range() -> Result<()> { + let env = TestEnvironment::new().await?; + + // Matrix of test builds (crate name, nightly date, version) + let build_matrix = [ + // Should be skipped since this is not the latest build for this release + ("foo1", NaiveDate::from_ymd_opt(2020, 10, 1).unwrap(), V1), + // All those should match + ("foo1", NaiveDate::from_ymd_opt(2020, 10, 2).unwrap(), V1), + ("foo1", NaiveDate::from_ymd_opt(2020, 10, 3).unwrap(), V2), + ("foo2", NaiveDate::from_ymd_opt(2020, 10, 4).unwrap(), V1), + // Should be skipped since the nightly doesn't match (end date is exclusive) + ("foo2", NaiveDate::from_ymd_opt(2020, 10, 5).unwrap(), V2), + ]; + for build in build_matrix.into_iter() { + let (crate_name, nightly, version) = build; + env.fake_release() + .await + .name(crate_name) + .version(version) + .builds(vec![ + FakeBuild::default() + .rustc_version( + format!( + "rustc 1.84.0-nightly (e7c0d2750 {})", + nightly.format("%Y-%m-%d") + ) + .as_str(), + ) + .build_status(BuildStatus::Failure), + ]) + .create() + .await?; + } + + let build_queue = env.async_build_queue(); + assert!(build_queue.queued_crates().await?.is_empty()); + + let mut conn = env.async_db().async_conn().await; + queue_rebuilds_faulty_rustdoc( + &mut conn, + build_queue, + &NaiveDate::from_ymd_opt(2020, 10, 2).unwrap(), + &NaiveDate::from_ymd_opt(2020, 10, 5), + ) + .await?; + + let queue = build_queue.queued_crates().await?; + assert_eq!(queue.len(), 3); + assert_eq!(queue[0].name, "foo1"); + assert_eq!(queue[0].version, V1); + assert_eq!(queue[0].priority, PRIORITY_BROKEN_RUSTDOC); + assert_eq!(queue[1].name, "foo1"); + assert_eq!(queue[1].version, V2); + assert_eq!(queue[1].priority, PRIORITY_BROKEN_RUSTDOC); + assert_eq!(queue[2].name, "foo2"); + assert_eq!(queue[2].version, V1); + assert_eq!(queue[2].priority, PRIORITY_BROKEN_RUSTDOC); Ok(()) } @@ -828,10 +1080,10 @@ mod tests { let build_queue = env.async_build_queue(); build_queue - .add_crate("foo1", &V1, REBUILD_PRIORITY, None) + .add_crate("foo1", &V1, PRIORITY_CONTINUOUS, None) .await?; build_queue - .add_crate("foo2", &V1, REBUILD_PRIORITY, None) + .add_crate("foo2", &V1, PRIORITY_CONTINUOUS, None) .await?; let mut conn = env.async_db().async_conn().await; @@ -870,10 +1122,10 @@ mod tests { let build_queue = env.async_build_queue(); build_queue - .add_crate("foo1", &V1, REBUILD_PRIORITY, None) + .add_crate("foo1", &V1, PRIORITY_CONTINUOUS, None) .await?; build_queue - .add_crate("foo2", &V1, REBUILD_PRIORITY, None) + .add_crate("foo2", &V1, PRIORITY_CONTINUOUS, None) .await?; env.fake_release() diff --git a/src/lib.rs b/src/lib.rs index fc9d1423f..e45e42b16 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,9 @@ //! documentation of crates for the Rust Programming Language. #![allow(clippy::cognitive_complexity)] -pub use self::build_queue::{AsyncBuildQueue, BuildQueue, queue_rebuilds}; +pub use self::build_queue::{ + AsyncBuildQueue, BuildQueue, queue_rebuilds, queue_rebuilds_faulty_rustdoc, +}; pub use self::config::Config; pub use self::context::Context; pub use self::docbuilder::PackageKind; diff --git a/src/utils/consistency/mod.rs b/src/utils/consistency/mod.rs index d1c977448..81695f246 100644 --- a/src/utils/consistency/mod.rs +++ b/src/utils/consistency/mod.rs @@ -1,3 +1,4 @@ +use crate::build_queue::PRIORITY_CONSISTENCY_CHECK; use crate::{Context, db::delete}; use anyhow::{Context as _, Result}; use itertools::Itertools; @@ -8,8 +9,6 @@ mod db; mod diff; mod index; -const BUILD_PRIORITY: i32 = 15; - /// consistency check /// /// will compare our database with the local crates.io index and @@ -101,7 +100,7 @@ where if !dry_run && let Err(err) = ctx .async_build_queue - .add_crate(name, version, BUILD_PRIORITY, None) + .add_crate(name, version, PRIORITY_CONSISTENCY_CHECK, None) .await { warn!("{:?}", err); @@ -128,7 +127,7 @@ where if !dry_run && let Err(err) = ctx .async_build_queue - .add_crate(name, version, BUILD_PRIORITY, None) + .add_crate(name, version, PRIORITY_CONSISTENCY_CHECK, None) .await { warn!("{:?}", err); diff --git a/src/utils/queue.rs b/src/utils/queue.rs index 7df91672a..92d697ed6 100644 --- a/src/utils/queue.rs +++ b/src/utils/queue.rs @@ -1,9 +1,8 @@ //! Utilities for interacting with the build queue +use crate::build_queue::PRIORITY_DEFAULT; use crate::error::Result; use futures_util::stream::TryStreamExt; -const DEFAULT_PRIORITY: i32 = 0; - /// Get the build queue priority for a crate, returns the matching pattern too pub async fn list_crate_priorities(conn: &mut sqlx::PgConnection) -> Result> { Ok( @@ -34,7 +33,7 @@ pub async fn get_crate_pattern_and_priority( pub async fn get_crate_priority(conn: &mut sqlx::PgConnection, name: &str) -> Result { Ok(get_crate_pattern_and_priority(conn, name) .await? - .map_or(DEFAULT_PRIORITY, |(_, priority)| priority)) + .map_or(PRIORITY_DEFAULT, |(_, priority)| priority)) } /// Set all crates that match [`pattern`] to have a certain priority @@ -96,22 +95,22 @@ mod tests { ); assert_eq!( get_crate_priority(&mut conn, "docsrs").await?, - DEFAULT_PRIORITY + PRIORITY_DEFAULT ); set_crate_priority(&mut conn, "_c_", 100).await?; assert_eq!(get_crate_priority(&mut conn, "rcc").await?, 100); - assert_eq!(get_crate_priority(&mut conn, "rc").await?, DEFAULT_PRIORITY); + assert_eq!(get_crate_priority(&mut conn, "rc").await?, PRIORITY_DEFAULT); set_crate_priority(&mut conn, "hexponent", 10).await?; assert_eq!(get_crate_priority(&mut conn, "hexponent").await?, 10); assert_eq!( get_crate_priority(&mut conn, "hexponents").await?, - DEFAULT_PRIORITY + PRIORITY_DEFAULT ); assert_eq!( get_crate_priority(&mut conn, "floathexponent").await?, - DEFAULT_PRIORITY + PRIORITY_DEFAULT ); Ok(()) @@ -133,7 +132,7 @@ mod tests { ); assert_eq!( get_crate_priority(&mut conn, "docsrs-").await?, - DEFAULT_PRIORITY + PRIORITY_DEFAULT ); Ok(()) @@ -160,7 +159,7 @@ mod tests { ); assert_eq!( get_crate_priority(&mut conn, "unrelated").await?, - DEFAULT_PRIORITY + PRIORITY_DEFAULT ); Ok(()) @@ -175,23 +174,23 @@ mod tests { assert_eq!( get_crate_priority(&mut conn, "docsrs").await?, - DEFAULT_PRIORITY + PRIORITY_DEFAULT ); assert_eq!( get_crate_priority(&mut conn, "rcc").await?, - DEFAULT_PRIORITY + PRIORITY_DEFAULT ); assert_eq!( get_crate_priority(&mut conn, "lasso").await?, - DEFAULT_PRIORITY + PRIORITY_DEFAULT ); assert_eq!( get_crate_priority(&mut conn, "hexponent").await?, - DEFAULT_PRIORITY + PRIORITY_DEFAULT ); assert_eq!( get_crate_priority(&mut conn, "rust4lyfe").await?, - DEFAULT_PRIORITY + PRIORITY_DEFAULT ); Ok(()) diff --git a/src/web/builds.rs b/src/web/builds.rs index 65660a2dc..b400aca3a 100644 --- a/src/web/builds.rs +++ b/src/web/builds.rs @@ -1,3 +1,4 @@ +use crate::build_queue::PRIORITY_MANUAL_FROM_CRATES_IO; use crate::{ AsyncBuildQueue, Config, db::{ @@ -139,10 +140,6 @@ async fn build_trigger_check( Ok(()) } -// Priority according to issue #2442; positive here as it's inverted. -// FUTURE: move to a crate-global enum with all special priorities? -const TRIGGERED_REBUILD_PRIORITY: i32 = 5; - pub(crate) async fn build_trigger_rebuild_handler( Path((name, version)): Path<(String, Version)>, mut conn: DbConnection, @@ -176,7 +173,7 @@ pub(crate) async fn build_trigger_rebuild_handler( .add_crate( &name, &version, - TRIGGERED_REBUILD_PRIORITY, + PRIORITY_MANUAL_FROM_CRATES_IO, None, /* because crates.io is the only service that calls this endpoint */ ) .await diff --git a/src/web/releases.rs b/src/web/releases.rs index 84d997ada..a1eed2485 100644 --- a/src/web/releases.rs +++ b/src/web/releases.rs @@ -1,8 +1,10 @@ //! Releases web handlersrelease +use super::cache::CachePolicy; +use crate::build_queue::PRIORITY_CONTINUOUS; use crate::{ AsyncBuildQueue, Config, InstanceMetrics, RegistryApi, - build_queue::{QueuedCrate, REBUILD_PRIORITY}, + build_queue::QueuedCrate, cdn, db::types::version::Version, impl_axum_webpage, @@ -37,8 +39,6 @@ use std::{ use tracing::{trace, warn}; use url::form_urlencoded; -use super::cache::CachePolicy; - /// Number of release in home page const RELEASES_IN_HOME: i64 = 15; /// Releases in /releases page @@ -780,7 +780,7 @@ pub(crate) async fn build_queue_handler( .collect_vec(); queue.retain_mut(|krate| { - if krate.priority >= REBUILD_PRIORITY { + if krate.priority >= PRIORITY_CONTINUOUS { rebuild_queue.push(krate.clone()); false } else { @@ -1989,12 +1989,14 @@ mod tests { async_wrapper(|env| async move { let web = env.web_app().await; let queue = env.async_build_queue(); - queue.add_crate("foo", &V1, REBUILD_PRIORITY, None).await?; queue - .add_crate("bar", &V2, REBUILD_PRIORITY + 1, None) + .add_crate("foo", &V1, PRIORITY_CONTINUOUS, None) + .await?; + queue + .add_crate("bar", &V2, PRIORITY_CONTINUOUS + 1, None) .await?; queue - .add_crate("baz", &V3, REBUILD_PRIORITY - 1, None) + .add_crate("baz", &V3, PRIORITY_CONTINUOUS - 1, None) .await?; let full = kuchikiki::parse_html().one(web.get("/releases/queue").await?.text().await?);