diff --git a/src/build_queue.rs b/src/build_queue.rs index a9d2c7020..48ea3e4f4 100644 --- a/src/build_queue.rs +++ b/src/build_queue.rs @@ -176,13 +176,41 @@ impl BuildQueue { // available one. let to_process = match transaction .query_opt( - "SELECT id, name, version, priority, registry - FROM queue - WHERE attempt < $1 - ORDER BY priority ASC, attempt ASC, id ASC + "SELECT + id, + name, + version, + priority, + registry + FROM + queue + WHERE + attempt < $1 AND + ( + SELECT + CASE + WHEN MAX(b.build_time) IS NULL then TRUE + WHEN MAX(b.build_time) < (NOW() - make_interval(secs => $2)) then TRUE + ELSE FALSE + END + FROM + crates AS c + LEFT OUTER JOIN releases AS r ON c.id = r.crate_id + LEFT OUTER JOIN builds AS b on b.rid = r.id + WHERE + c.name = queue.name AND + r.version = queue.version + ) = TRUE + ORDER BY + priority ASC, + attempt ASC, + id ASC LIMIT 1 - FOR UPDATE SKIP LOCKED", - &[&self.max_attempts], + FOR UPDATE OF queue SKIP LOCKED", + &[ + &self.max_attempts, + &self.config.delay_between_build_attempts.as_secs_f64(), + ], )? .map(|row| QueuedCrate { id: row.get("id"), @@ -484,6 +512,8 @@ impl BuildQueue { #[cfg(test)] mod tests { use super::*; + use crate::test::FakeBuild; + use chrono::Utc; #[test] fn test_add_duplicate_doesnt_fail_last_priority_wins() { @@ -703,6 +733,63 @@ mod tests { }); } + #[test] + fn test_time_between_builds_attempts_ok_with_delay() { + crate::test::wrapper(|env| { + env.override_config(|config| { + config.delay_between_build_attempts = std::time::Duration::from_secs(120); + }); + + let queue = env.build_queue(); + + queue.add_crate("foo", "0.1.0", 0, None)?; + + env.fake_release() + .name("foo") + .version("0.1.0") + .builds(vec![ + FakeBuild::default().build_time(Utc::now() - chrono::Duration::seconds(600)) + ]) + .create()?; + assert_eq!(queue.prioritized_count()?, 1); + queue.process_next_crate(|krate| { + assert_eq!("foo", krate.name); + Ok(()) + })?; + assert_eq!(queue.prioritized_count()?, 0); + + Ok(()) + }); + } + + #[test] + fn test_time_between_builds_attempts_too_short_for_delay() { + crate::test::wrapper(|env| { + env.override_config(|config| { + config.delay_between_build_attempts = std::time::Duration::from_secs(120); + }); + + let queue = env.build_queue(); + + queue.add_crate("foo", "0.1.0", 0, None)?; + + env.fake_release() + .name("foo") + .version("0.1.0") + .builds(vec![ + FakeBuild::default().build_time(Utc::now() - chrono::Duration::seconds(60)) + ]) + .create()?; + assert_eq!(queue.prioritized_count()?, 1); + queue.process_next_crate(|_| { + unreachable!(); + })?; + assert_eq!(queue.prioritized_count()?, 1); + + Ok(()) + }); + } + #[test] fn test_prioritized_count() { crate::test::wrapper(|env| { diff --git a/src/config.rs b/src/config.rs index e44dae942..f96783e35 100644 --- a/src/config.rs +++ b/src/config.rs @@ -103,6 +103,7 @@ pub struct Config { pub(crate) build_default_memory_limit: Option, pub(crate) include_default_targets: bool, pub(crate) disable_memory_limit: bool, + pub(crate) delay_between_build_attempts: Duration, } impl Config { @@ -204,6 +205,10 @@ impl Config { build_default_memory_limit: maybe_env("DOCSRS_BUILD_DEFAULT_MEMORY_LIMIT")?, include_default_targets: env("DOCSRS_INCLUDE_DEFAULT_TARGETS", true)?, disable_memory_limit: env("DOCSRS_DISABLE_MEMORY_LIMIT", false)?, + delay_between_build_attempts: Duration::from_secs(env( + "DOCSRS_DELAY_BETWEEN_BUILD_ATTEMPTS", + 120, + )?), }) } } diff --git a/src/test/fakes.rs b/src/test/fakes.rs index 299a2e8da..ef4f8dedd 100644 --- a/src/test/fakes.rs +++ b/src/test/fakes.rs @@ -40,6 +40,7 @@ pub(crate) struct FakeRelease<'a> { pub(crate) struct FakeBuild { s3_build_log: Option, db_build_log: Option, + build_time: Option>, result: BuildResult, } @@ -512,6 +513,13 @@ impl FakeBuild { } } + pub(crate) fn build_time(self, build_time: DateTime) -> Self { + Self { + build_time: Some(build_time), + ..self + } + } + pub(crate) fn db_build_log(self, build_log: impl Into) -> Self { Self { db_build_log: Some(build_log.into()), @@ -545,6 +553,13 @@ impl FakeBuild { ) -> Result<()> { let build_id = crate::db::add_build_into_database(conn, release_id, &self.result)?; + if let Some(build_time) = self.build_time { + conn.query( + "UPDATE builds SET build_time = $2 WHERE id = $1", + &[&build_id, &build_time], + )?; + } + if let Some(db_build_log) = self.db_build_log.as_deref() { conn.query( "UPDATE builds SET output = $2 WHERE id = $1", @@ -566,6 +581,7 @@ impl Default for FakeBuild { Self { s3_build_log: Some("It works!".into()), db_build_log: None, + build_time: None, result: BuildResult { rustc_version: "rustc 2.0.0-nightly (000000000 1970-01-01)".into(), docsrs_version: "docs.rs 1.0.0 (000000000 1970-01-01)".into(),