# Migration Guide
This guide helps you migrate between Duroxide versions and handle orchestration versioning.
## Orchestration Versioning
Duroxide supports versioning to handle code evolution while maintaining compatibility with running instances.
### When to Version
You need to version your orchestration when:
1. **Adding/removing activities**: Changes the execution flow
2. **Reordering operations**: Affects correlation IDs
3. **Changing conditional logic**: Alters execution paths
4. **Modifying data structures**: Input/output format changes
You DON'T need to version when:
1. **Fixing bugs in activities**: Activities are stateless
2. **Improving activity performance**: No behavior change
3. **Adding logging**: Using `ctx.trace_*` is replay-safe
4. **Refactoring activity internals**: Interface remains the same
### Versioning Strategy
```rust
// Version 1.0.0
let orchestration_v1 = |ctx: OrchestrationContext, input: String| async move {
let result = ctx.schedule_activity("ProcessV1", input).await?;
Ok(result)
};
// Version 2.0.0 - Added validation step
let orchestration_v2 = |ctx: OrchestrationContext, input: String| async move {
// New validation step
let validated = ctx.schedule_activity("Validate", &input).await?;
let result = ctx.schedule_activity("ProcessV2", validated).await?;
Ok(result)
};
// Register both versions
let orchestrations = OrchestrationRegistry::builder()
.register_versioned("MyOrchestration", "1.0.0", orchestration_v1)
.register_versioned("MyOrchestration", "2.0.0", orchestration_v2)
.with_version_policy(VersionPolicy::Latest) // New instances use latest
.build();
```
### Version Policies
1. **Latest** (default): New instances use the latest registered version
2. **Exact**: Must specify exact version when starting
3. **Compatible**: Use semantic versioning rules
### Handling Running Instances
When you deploy a new version:
1. **Running instances continue with their version**: Pinned at start
2. **New instances use the latest version**: Based on policy
3. **ContinueAsNew can change versions**: Explicitly specify
```rust
// Migrate running instance to new version via ContinueAsNew
ctx.continue_as_new_versioned("2.0.0", new_input);
```
## Breaking Changes Between Versions
### Duroxide 0.1.0 → 0.2.0 (Hypothetical)
#### API Changes
1. **Activity Registration**:
```rust
.register("MyActivity", |ctx: ActivityContext, input: String| async move { Ok(result) })
.register("MyActivity", |ctx: ActivityContext, input: String| async move -> Result<String, ActivityError> {
Ok(result)
})
```
2. **Orchestration Context**:
```rust
ctx.new_guid()
ctx.system_new_guid().await ```
3. **Runtime Creation**:
```rust
Runtime::start(activities, orchestrations).await
Runtime::start_with_store(store, activities, orchestrations).await
```
#### Migration Steps
1. **Update Dependencies**:
```toml
[dependencies]
duroxide = "0.2"
```
2. **Update Activity Signatures**:
- Add explicit error types
- Update return types if changed
3. **Update Orchestration Code**:
- Replace deprecated methods
- Update to new async APIs
4. **Test Thoroughly**:
- Run existing tests
- Test with production-like data
- Verify determinism
## Data Migration
### Handling Input/Output Format Changes
When changing data structures:
1. **Support both formats temporarily**:
```rust
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum InputCompat {
V1(InputV1),
V2(InputV2),
}
let orchestration = |ctx: OrchestrationContext, input_json: String| async move {
let input: InputCompat = serde_json::from_str(&input_json)?;
match input {
InputCompat::V1(v1) => {
let v2 = migrate_v1_to_v2(v1);
process_v2(ctx, v2).await
}
InputCompat::V2(v2) => {
process_v2(ctx, v2).await
}
}
};
```
2. **Gradual migration**:
- Deploy version supporting both formats
- Migrate data at your pace
- Remove old format support later
### Storage Provider Migration
When switching providers:
```rust
// 1. Export from old provider
let old_store = InMemoryHistoryStore::new();
let instances = old_store.list_instances().await;
for instance in instances {
let history = old_store.read(&instance).await;
// Save history to new provider
}
// 2. Import to new provider
let new_store = SqliteProvider::new("sqlite:./data.db", None).await?;
for (instance, history) in saved_data {
// Recreate instance in new store
new_store.create_instance(&instance).await?;
new_store.append(&instance, history).await?;
}
// 3. Switch runtime to new provider
let rt = Runtime::start_with_store(Arc::new(new_store), activities, orchestrations).await;
```
## Best Practices for Versioning
1. **Semantic Versioning**: Use major.minor.patch
- Major: Breaking changes
- Minor: New features, backward compatible
- Patch: Bug fixes
2. **Deployment Strategy**:
- Deploy new version alongside old
- Monitor both versions
- Gradually migrate instances
- Remove old version when safe
3. **Testing Strategy**:
```rust
#[test]
async fn test_version_compatibility() {
let v1_result = run_with_version("1.0.0", v1_input).await;
let v2_result = run_with_version("2.0.0", v2_input).await;
let migrated = migrate_v1_to_v2(v1_result);
assert_eq!(migrated, expected);
}
```
4. **Documentation**:
- Document what changed
- Provide migration examples
- List breaking changes clearly
- Include compatibility matrix
## Rollback Strategy
If issues arise after deployment:
1. **Leave running instances**: They continue with their pinned version
2. **Revert new instances**: Change version policy or registration
3. **Monitor and fix**: Address issues without affecting running work
```rust
// Emergency rollback configuration
let orchestrations = OrchestrationRegistry::builder()
.register_versioned("MyOrchestration", "1.0.0", orchestration_v1)
.register_versioned("MyOrchestration", "2.0.0", orchestration_v2)
.with_version_policy(VersionPolicy::Exact("1.0.0")) // Force v1 for new instances
.build();
```
## Draining Stuck Orchestrations After Upgrade
If after a full upgrade some orchestrations remain pinned to an old duroxide version that no
running node supports, they will sit in the queue indefinitely. To clear them, temporarily
widen `supported_replay_versions` in `RuntimeOptions`:
```rust
RuntimeOptions {
supported_replay_versions: Some(SemverRange::new(
semver::Version::new(0, 0, 0),
semver::Version::new(99, 0, 0),
)),
max_attempts: 5,
..Default::default()
}
```
The wide range causes the provider filter to pass for all items. When the provider fetches
an item whose history contains unknown event types (from a newer duroxide version),
deserialization fails at the provider level, returning a permanent error. Each fetch cycle
increments the item's `attempt_count`. The item remains in the queue but is effectively
drained — it never reaches the runtime's replay engine because the provider cannot
deserialize its history. Compatible items whose history deserializes successfully are
processed normally. Revert to the default after draining.
See [Versioning Best Practices](versioning-best-practices.md#draining-stuck-orchestrations-version-mismatch)
for details.
## Future Compatibility
To make future migrations easier:
1. **Use typed inputs/outputs** with serde
2. **Version your APIs** from the start
3. **Keep orchestrations simple** - complex logic in activities
4. **Document assumptions** and invariants
5. **Test with multiple versions** in CI/CD
## Session Affinity Notes
Sessions are backward-compatible by design:
- Existing `schedule_activity` calls are unaffected (`session_id = None`)
- Old `ActivityScheduled` events without `session_id` deserialize with `session_id = None` via `#[serde(default)]`
- Provider schema migration: add `session_id` column to `worker_queue`, create `sessions` table
- No changes required to existing orchestration or activity code
## Getting Help
For migration assistance:
1. Review the [changelog](../CHANGELOG.md) for detailed changes
2. Check [examples](../examples/) for updated patterns
3. Run tests to verify compatibility
4. Open an issue for migration problems