Skip to content
Open
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
3 changes: 2 additions & 1 deletion docs/replay-engine-test-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,8 @@ Tests for deterministic child instance ID.

| Test | Baseline | Handler | Expected |
|------|----------|---------|----------|
| `auto_instance_id` | `[Started]` | `schedule_sub_orchestration("Child", input)` | Instance = `sub::{event_id}` |
| `auto_instance_id` | `[Started]` | `schedule_sub_orchestration("Child", input)` | Instance = `sub::{event_id}` (execution 1) |
| `auto_instance_id_includes_execution_id_for_later_generations` | `[Started(execution=2)]` | `schedule_sub_orchestration("Child", input)` | Instance = `sub::2.{event_id}` |
| `explicit_instance_preserved` | `[Started]` | `schedule_sub_orchestration_with_instance("my-id", ...)` | Instance = "my-id" |
| `replay_uses_history_instance` | `[Started, SubOrchScheduled{instance:"sub::2"}]` | Same schedule | Binds to same instance |

Expand Down
5 changes: 2 additions & 3 deletions docs/sub-orchestrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ There are two modes for child instance ID assignment:

**Auto-generated (default):**
- Use `schedule_sub_orchestration()` - no instance ID parameter
- Runtime generates `instance = "sub::{event_id}"` and prefixes with parent: `"{parent_instance}::sub::{id}"`
- This guarantees the same child id on replays and prevents collisions across parents
- Runtime generates `instance = "sub::{event_id}"` for execution 1, and `instance = "sub::{execution_id}.{event_id}"` for later continue-as-new executions; then prefixes with parent.
- This guarantees the same child id on replays and prevents collisions across parents and continue-as-new generations.

**Explicit:**
- Use `schedule_sub_orchestration_with_id("Child", "my-instance-id", input)`
Expand Down Expand Up @@ -162,4 +162,3 @@ let root = |ctx: OrchestrationContext, input: String| async move {
- Provider-backed durable completion routing (enqueue `SubOrchCompleted/Failed` items), leasing, DLQ.
- Visualization: render sub-orchestration edges in Mermaid diagrams under each parent.


4 changes: 3 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -862,7 +862,9 @@ pub const INITIAL_EVENT_ID: u64 = 1;
pub const SUB_ORCH_AUTO_PREFIX: &str = "sub::";

/// Prefix for placeholder instance IDs before event ID assignment.
/// These are replaced with `sub::{event_id}` during action processing.
/// These are replaced during action processing:
/// - execution 1: `sub::{event_id}`
/// - execution N>1: `sub::{execution_id}.{event_id}`
pub(crate) const SUB_ORCH_PENDING_PREFIX: &str = "sub::pending_";

/// Determine if a sub-orchestration instance ID is auto-generated (needs parent prefix).
Expand Down
10 changes: 7 additions & 3 deletions src/runtime/replay_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1082,7 +1082,7 @@ impl ReplayEngine {

ctx.bind_token(token, event_id);

let updated_action = update_action_event_id(action, event_id);
let updated_action = update_action_event_id(action, event_id, self.execution_id);

if let crate::Action::StartSubOrchestration { instance, .. } = &updated_action {
ctx.bind_sub_orchestration_instance(token, instance.clone());
Expand Down Expand Up @@ -1750,7 +1750,7 @@ fn action_to_event(action: &Action, instance: &str, execution_id: u64, event_id:
/// Update an action's scheduling_event_id to the correct event_id.
/// Also generates the actual sub-orchestration instance ID from the event_id
/// (unless an explicit instance ID was provided, indicated by not starting with SUB_ORCH_PENDING_PREFIX).
fn update_action_event_id(action: Action, event_id: u64) -> Action {
fn update_action_event_id(action: Action, event_id: u64, execution_id: u64) -> Action {
match action {
Action::CallActivity {
name,
Expand Down Expand Up @@ -1793,7 +1793,11 @@ fn update_action_event_id(action: Action, event_id: u64) -> Action {
// If instance starts with the pending prefix, it's a placeholder that needs to be replaced.
// Otherwise, it's an explicit instance ID provided by the user.
let final_instance = if instance.starts_with(crate::SUB_ORCH_PENDING_PREFIX) {
format!("{}{event_id}", crate::SUB_ORCH_AUTO_PREFIX)
if execution_id == crate::INITIAL_EXECUTION_ID {
format!("{}{event_id}", crate::SUB_ORCH_AUTO_PREFIX)
} else {
format!("{}{execution_id}.{event_id}", crate::SUB_ORCH_AUTO_PREFIX)
}
} else {
instance
};
Expand Down
25 changes: 25 additions & 0 deletions tests/replay_engine/sub_orchestration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use super::helpers::*;
use async_trait::async_trait;
use duroxide::{OrchestrationContext, OrchestrationHandler};
use duroxide::runtime::replay_engine::ReplayEngine;
use std::sync::Arc;

/// Auto-generated instance ID should be sub::{event_id}.
Expand Down Expand Up @@ -43,6 +44,30 @@ fn auto_instance_id() {
}
}

/// Auto-generated instance ID includes execution_id after continue-as-new generations.
#[test]
fn auto_instance_id_includes_execution_id_for_later_generations() {
let mut started = started_event(1); // OrchestrationStarted
started.execution_id = 2;
let history = vec![started];
let mut engine = ReplayEngine::new(TEST_INSTANCE.to_string(), 2, history);
let result = execute(&mut engine, SubOrchHandler::new("Child", "child-input"));

assert_continue(&result);

assert_eq!(engine.pending_actions().len(), 1);
match &engine.pending_actions()[0] {
duroxide::Action::StartSubOrchestration {
instance,
scheduling_event_id,
..
} => {
assert_eq!(instance, &format!("sub::2.{scheduling_event_id}"));
}
_ => panic!("Expected StartSubOrchestration action"),
}
}

/// Replay uses history's instance ID.
///
/// Orchestration code:
Expand Down
Loading