diff --git a/CHANGELOG.md b/CHANGELOG.md index 71a396e..efe9995 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed + +- **`ctx.new_guid()` now returns a standard UUID v4.** The previous + implementation derived the value from `SystemTime::now()` nanoseconds plus a + thread-local counter, which produced low-entropy, structured values (the + leading groups were always zero and the rest was largely sequential). It now + uses `uuid::Uuid::new_v4()`. The value is still recorded in history, so + replays remain deterministic. +- **SQLite provider lock tokens now use a random UUID** instead of + `nanos + process id`, removing a predictable-token pattern in work-item + ownership checks. + ## [0.1.29] - 2026-05-08 **Release:** diff --git a/Cargo.toml b/Cargo.toml index 81dddba..0ca7b35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ tokio-util = "0.7" # For CancellationToken serde = { version = "1", features = ["derive"] } serde_json = "1" async-trait = "0.1" +uuid = { version = "1", features = ["v4"] } tracing = { version = "0.1", features = ["std"] } tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter", "json"] } semver = "1" diff --git a/docs/ORCHESTRATION-GUIDE.md b/docs/ORCHESTRATION-GUIDE.md index 38a0415..9a89393 100644 --- a/docs/ORCHESTRATION-GUIDE.md +++ b/docs/ORCHESTRATION-GUIDE.md @@ -325,7 +325,7 @@ async fn safe_orchestration(ctx: OrchestrationContext, count: i32) -> Result( ) -> impl Future> // Usage: -let session_id = ctx.new_guid().await?; // Deterministic, replay-safe +let session_id = ctx.new_guid().await?; // Random, replay-safe let result = ctx.schedule_activity_on_session("RunTurn", &input, &session_id).await?; ``` @@ -820,7 +820,7 @@ ctx.trace_error("Payment failed"); #### System Calls (Deterministic Non-Determinism) ```rust -// Generate deterministic GUID +// Generate a random (UUID v4) GUID, recorded in history so it is replay-stable async fn new_guid(&self) -> Result // Get deterministic UTC timestamp diff --git a/docs/durable-futures-internals.md b/docs/durable-futures-internals.md index d3542f5..12b7044 100644 --- a/docs/durable-futures-internals.md +++ b/docs/durable-futures-internals.md @@ -550,7 +550,7 @@ Some operations are provided as **built-in activities** (under reserved names) s `ActivityScheduled` + `ActivityCompleted` event flow as user activities. This ensures replay determinism without special-case logic in the replay engine. -- `ctx.new_guid()` – Generate a deterministic UUID +- `ctx.new_guid()` – Generate a random UUID (recorded in history, so replay-stable) - `ctx.utc_now()` – Get the current time (replay-safe) Example usage: diff --git a/src/lib.rs b/src/lib.rs index 6faa754..2c39f77 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2769,11 +2769,11 @@ impl OrchestrationContext { } } - /// Generate a new deterministic GUID. + /// Generate a new random GUID. /// - /// This schedules a built-in activity that generates a unique identifier. - /// The GUID is deterministic across replays (the same value is returned - /// when the orchestration replays). + /// Schedules a built-in activity that returns a random UUID. The value is + /// recorded in history, so it is stable across replays — a replay-safe + /// replacement for calling `Uuid::new_v4()` directly in an orchestration. /// /// # Example /// @@ -2871,36 +2871,12 @@ impl OrchestrationContext { } } -/// Generate a deterministic GUID for use in orchestrations. +/// Generate a random GUID for use in orchestrations. /// -/// Uses timestamp + thread-local counter for uniqueness. +/// Recorded in history by the syscall activity, so replays return the same +/// value — using a random value here does not break determinism. pub(crate) fn generate_guid() -> String { - use std::time::{SystemTime, UNIX_EPOCH}; - - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_nanos()) - .unwrap_or(0); - - // Thread-local counter for uniqueness within the same timestamp - thread_local! { - static COUNTER: std::cell::Cell = const { std::cell::Cell::new(0) }; - } - let counter = COUNTER.with(|c| { - let val = c.get(); - c.set(val.wrapping_add(1)); - val - }); - - // Format as UUID-like string - format!( - "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}", - (timestamp >> 96) as u32, - ((timestamp >> 80) & 0xFFFF) as u16, - (counter & 0xFFFF) as u16, - ((timestamp >> 64) & 0xFFFF) as u16, - (timestamp & 0xFFFFFFFFFFFF) as u64 - ) + uuid::Uuid::new_v4().to_string() } impl OrchestrationContext { diff --git a/src/providers/sqlite.rs b/src/providers/sqlite.rs index d8fe523..eda3c3a 100644 --- a/src/providers/sqlite.rs +++ b/src/providers/sqlite.rs @@ -491,11 +491,7 @@ impl SqliteProvider { /// Generate a unique lock token fn generate_lock_token() -> String { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system clock should be after UNIX epoch") - .as_nanos(); - format!("lock_{now}_{}", std::process::id()) + format!("lock_{}", uuid::Uuid::new_v4()) } /// Get current timestamp in milliseconds diff --git a/tests/system_calls_test.rs b/tests/system_calls_test.rs index c440469..3806b73 100644 --- a/tests/system_calls_test.rs +++ b/tests/system_calls_test.rs @@ -48,6 +48,13 @@ async fn test_new_guid() { let parts: Vec<&str> = output.split(',').collect(); assert_eq!(parts.len(), 2); assert_ne!(parts[0], parts[1]); + + // Guard the security-relevant contract: each value is a standard UUID v4, + // not the old predictable timestamp+counter scheme. + for guid in &parts { + let parsed = uuid::Uuid::parse_str(guid).unwrap_or_else(|e| panic!("{guid} is not a valid UUID: {e}")); + assert_eq!(parsed.get_version_num(), 4, "{guid} is not a UUID v4"); + } } else { panic!("Orchestration did not complete successfully: {status:?}"); }