diff --git a/rust/src/runtime/queries.rs b/rust/src/runtime/queries.rs index b5f78e757..6c45ecb92 100644 --- a/rust/src/runtime/queries.rs +++ b/rust/src/runtime/queries.rs @@ -234,7 +234,7 @@ where "query_snapshot_exceeds_budget", )); } - let task_id = ulid::Ulid::new().to_string(); + let task_id = super::store::id(); let key = key(&task_id)?; let admitted = authority::now(); let (deadline, retained_until) = config.timing(admitted)?; diff --git a/rust/src/runtime/store.rs b/rust/src/runtime/store.rs index 5dcb683b1..6208b3123 100644 --- a/rust/src/runtime/store.rs +++ b/rust/src/runtime/store.rs @@ -27,8 +27,28 @@ pub(super) struct Store { pub(super) history_budget: super::history_budget::Config, pub(super) search_attribute_limit: i64, } +/// A ULID that sorts after every earlier one from this process, as Laravel's +/// (Symfony's) generator does. Rows created in the same millisecond, such as +/// the activity tasks of one completion, keep their creation order in +/// `ORDER BY available_at,id`. pub(super) fn id() -> String { - Ulid::new().to_string() + static GENERATOR: std::sync::Mutex = + std::sync::Mutex::new(ulid::Generator::new()); + let mut generator = GENERATOR + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + // Overflow needs 2^80 identifiers in one millisecond. + generator + .generate() + .unwrap_or_else(|_| Ulid::new()) + .to_string() +} + +#[cfg(test)] +#[test] +fn identifiers_keep_creation_order_within_a_millisecond() { + let ids: Vec = (0..10_000).map(|_| id()).collect(); + assert!(ids.windows(2).all(|pair| pair[0] < pair[1])); } pub(super) fn now() -> DateTime { DateTime::from_timestamp_millis(Utc::now().timestamp_millis()).expect("current timestamp")