Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:** <https://crates.io/crates/duroxide/0.1.29>
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 3 additions & 3 deletions docs/ORCHESTRATION-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ async fn safe_orchestration(ctx: OrchestrationContext, count: i32) -> Result<Str
// ✅ Logging (replay-safe)
ctx.trace_info("Step completed");

// ✅ Deterministic GUIDs
// ✅ Replay-safe GUIDs
let id = ctx.new_guid().await?;

// ✅ Deterministic timestamps
Expand Down Expand Up @@ -441,7 +441,7 @@ fn schedule_activity_on_session_typed<In: Serialize, Out: DeserializeOwned>(
) -> impl Future<Output = Result<Out, String>>

// 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?;
```

Expand Down Expand Up @@ -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<String, String>

// Get deterministic UTC timestamp
Expand Down
2 changes: 1 addition & 1 deletion docs/durable-futures-internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
40 changes: 8 additions & 32 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand Down Expand Up @@ -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<u32> = 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 {
Expand Down
6 changes: 1 addition & 5 deletions src/providers/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions tests/system_calls_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:?}");
}
Expand Down
Loading