From 41d7826199cbaa6d0331782c30bd266c1b65ac0b Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Tue, 20 Oct 2026 22:08:39 +0000 Subject: [PATCH] Generate native identifiers in creation order Ulid::new() leaves identifiers from one millisecond unordered, so two activity tasks of one completion could be claimed out of authored order under ORDER BY available_at,id. Published PHP uses Laravel's (Symfony's) monotonic ULIDs. Native now draws every identifier, including query task ids, from one process-wide monotonic generator. Co-Authored-By: Claude Opus 5.5 --- rust/src/runtime/queries.rs | 2 +- rust/src/runtime/store.rs | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) 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 31014d9f0..bdd8461b0 100644 --- a/rust/src/runtime/store.rs +++ b/rust/src/runtime/store.rs @@ -26,8 +26,28 @@ pub(super) struct Store { pub(super) memo_size_limit: Option, pub(super) history_budget: super::history_budget::Config, } +/// 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")