diff --git a/src/ids.rs b/src/ids.rs index 17aa032..05c74a5 100644 --- a/src/ids.rs +++ b/src/ids.rs @@ -1,5 +1,9 @@ use crate::model::ChangeGroup; use chrono::Utc; +use std::collections::hash_map::RandomState; +use std::hash::{BuildHasher, Hasher}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::OnceLock; use std::time::{SystemTime, UNIX_EPOCH}; /// Expand CLI repo selector flags. Each value may be one id or a comma-separated @@ -74,17 +78,56 @@ pub fn node_id(prefix: &str) -> String { object_id(prefix) } +/// Ids from one process start at a random suffix and count up from it, so two +/// ids requested in the same clock tick (parallel revert clones, for example) +/// never repeat. fn object_id(prefix: &str) -> String { + static START: OnceLock = OnceLock::new(); + static NEXT: AtomicU64 = AtomicU64::new(0); + let start = *START.get_or_init(|| { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_nanos()) + .unwrap_or_default(); + let mut hasher = RandomState::new().build_hasher(); + hasher.write_u128(nanos); + hasher.write_u32(std::process::id()); + hasher.finish() + }); let date = Utc::now().format("%Y%m%d"); - let nanos = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_nanos()) - .unwrap_or_default(); - let mixed = nanos ^ u128::from(std::process::id()); - let suffix = format!("{:06x}", mixed & 0xFF_FFFF); - format!("{prefix}_{date}_{suffix}") + let suffix = start.wrapping_add(NEXT.fetch_add(1, Ordering::Relaxed)) & 0xFF_FFFF; + format!("{prefix}_{date}_{suffix:06x}") } pub fn short_sha(sha: &str) -> String { sha.trim().chars().take(7).collect() } + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + + #[test] + fn ids_requested_at_once_from_many_threads_never_repeat() { + let ids: Vec = std::thread::scope(|scope| { + let workers: Vec<_> = (0..8) + .map(|_| scope.spawn(|| (0..2_000).map(|_| revert_group_id()).collect::>())) + .collect(); + workers + .into_iter() + .flat_map(|worker| worker.join().unwrap()) + .collect() + }); + let unique: HashSet<_> = ids.iter().collect(); + assert_eq!(unique.len(), ids.len()); + assert!(ids.iter().all(|id| { + let parts: Vec<_> = id.split('_').collect(); + parts.len() == 3 + && parts[0] == "rv" + && parts[1].len() == 8 + && parts[2].len() == 6 + && parts[2].chars().all(|c| c.is_ascii_hexdigit()) + })); + } +}