From 25b98fec3cf69e7cf5655b9bc593ca74aa0749d3 Mon Sep 17 00:00:00 2001 From: Jakub Sztandera Date: Fri, 21 Aug 2026 13:28:54 +0200 Subject: [PATCH] Reserve run IDs atomically Signed-off-by: Jakub Sztandera --- src/main.rs | 2 +- src/run_id/mod.rs | 68 ++++++++++++++++++++++++++--------------------- 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/src/main.rs b/src/main.rs index 203931e8..b7c8489b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,7 +14,7 @@ fn main() -> Result<(), Box> { let cli = Cli::parse(); // Generate a run ID for this execution and initialize logging - let run_id = generate_run_id(); + let run_id = generate_run_id()?; init_logging(&run_id)?; // Check for poison file and attempt recovery diff --git a/src/run_id/mod.rs b/src/run_id/mod.rs index 58020231..feb3affe 100644 --- a/src/run_id/mod.rs +++ b/src/run_id/mod.rs @@ -26,28 +26,35 @@ pub const NOUNS: &[&str] = &[ "Lulu", "Bear", "Fig", "Boo", ]; -/// Generate a unique run ID for this execution. +/// Generate and atomically reserve a unique run ID for this execution. /// -/// Returns a string like "20251215T2206_ZanyPip" where: -/// - 20251215 is the date (YYYYMMDD, condensed ISO8601 format) -/// - T is the date/time separator (ISO8601) -/// - 2206 is the time (HHMM, 24-hour format, no colons for Docker compatibility) -/// - ZanyPip is the random name (adjective + noun) -/// -/// Uses condensed ISO8601 format (no dashes or colons) for Docker network name compatibility. -pub fn generate_run_id() -> String { - let now = Local::now(); - let datetime = now.format("%Y%m%dT%H%M"); - - // Implement our own random name generator to control format - let random_name = { - let rng = &mut rand::rng(); - let adjective = ADJECTIVES.choose(rng).unwrap(); - let noun = NOUNS.choose(rng).unwrap(); - format!("{}{}", adjective, noun) - }; - - format!("{}_{}", datetime, random_name) +/// The ID uses condensed ISO8601 plus a readable random name. Its run +/// directory is created atomically, so concurrent processes cannot claim the +/// same ID. +pub fn generate_run_id() -> Result> { + reserve_run_id_in(&crate::paths::foc_devnet_runs()) +} + +/// Generate a readable candidate ID without reserving it. +fn generate_candidate() -> String { + let datetime = Local::now().format("%Y%m%dT%H%M"); + let rng = &mut rand::rng(); + let adjective = ADJECTIVES.choose(rng).unwrap(); + let noun = NOUNS.choose(rng).unwrap(); + format!("{}_{}{}", datetime, adjective, noun) +} + +/// Reserve an unused run ID under the supplied runs directory. +fn reserve_run_id_in(runs_dir: &std::path::Path) -> Result> { + std::fs::create_dir_all(runs_dir)?; + loop { + let run_id = generate_candidate(); + match std::fs::create_dir(runs_dir.join(&run_id)) { + Ok(()) => return Ok(run_id), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(error) => return Err(error.into()), + } + } } /// Create a symlink to the latest run directory. @@ -110,7 +117,7 @@ mod tests { #[test] fn test_run_id_format() { - let run_id = generate_run_id(); + let run_id = generate_candidate(); // Should match pattern: YYYYMMDDTHHMM_RandomName (condensed ISO8601, no dashes/colons) let pattern = Regex::new(r"^\d{8}T\d{4}_.+$").unwrap(); @@ -122,14 +129,14 @@ mod tests { } #[test] - fn test_run_ids_are_different() { - // Generate multiple IDs in quick succession - // They should be different due to random names (time might be same) - let id1 = generate_run_id(); - let id2 = generate_run_id(); - - // At least the random name part should differ - assert_ne!(id1, id2, "Run IDs should be different"); + fn test_reserved_run_ids_are_different() { + let temp_dir = tempfile::TempDir::new().unwrap(); + let id1 = reserve_run_id_in(temp_dir.path()).unwrap(); + let id2 = reserve_run_id_in(temp_dir.path()).unwrap(); + + assert_ne!(id1, id2, "Reserved run IDs should be different"); + assert!(temp_dir.path().join(id1).is_dir()); + assert!(temp_dir.path().join(id2).is_dir()); } #[test] @@ -140,7 +147,6 @@ mod tests { use tempfile::TempDir; let temp_dir = TempDir::new().expect("Failed to create temp dir"); - let _run_id = generate_run_id(); // Mock the paths by using temp directory structure let runs_dir = temp_dir.path().join("run");