duroxide/providers/mod.rs
1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4use crate::Event;
5use std::any::Any;
6use std::collections::HashSet;
7use std::time::Duration;
8
9pub mod error;
10pub use error::ProviderError;
11
12/// Maximum number of tags a worker can subscribe to.
13pub use crate::runtime::limits::MAX_WORKER_TAGS;
14
15/// Filter for which activity tags a worker will process.
16///
17/// Activity tags route work items to specialized workers. A tag is an
18/// opaque string label set at schedule time via `.with_tag()`. Workers
19/// declare which tags they accept via [`crate::RuntimeOptions`]`.worker_tag_filter`.
20///
21/// # Variants
22///
23/// - `DefaultOnly` (default) — process only activities with no tag.
24/// - `Tags(["gpu"])` — process only activities tagged `"gpu"`.
25/// - `DefaultAnd(["gpu"])` — process activities with no tag OR tagged `"gpu"`.
26/// - `Any` — process all activities regardless of tag.
27/// - `None` — don't process any activities (orchestrator-only mode).
28#[derive(Debug, Clone, PartialEq, Eq, Default)]
29pub enum TagFilter {
30 /// Process only activities with no tag (default).
31 #[default]
32 DefaultOnly,
33
34 /// Process only activities with the specified tags (NOT default).
35 /// Limited to [`MAX_WORKER_TAGS`] (5) tags.
36 Tags(HashSet<String>),
37
38 /// Process activities with no tag AND the specified tags.
39 /// Limited to [`MAX_WORKER_TAGS`] (5) tags.
40 DefaultAnd(HashSet<String>),
41
42 /// Process all activities regardless of tag (generalist worker).
43 Any,
44
45 /// Don't process any activities (orchestrator-only mode).
46 None,
47}
48
49impl TagFilter {
50 /// Create a filter for specific tags only.
51 ///
52 /// # Panics
53 /// Panics if more than [`MAX_WORKER_TAGS`] (5) tags are provided.
54 pub fn tags<I, S>(tags: I) -> Self
55 where
56 I: IntoIterator<Item = S>,
57 S: Into<String>,
58 {
59 let set: HashSet<String> = tags.into_iter().map(Into::into).collect();
60 assert!(
61 !set.is_empty(),
62 "TagFilter::tags() requires at least one tag; use TagFilter::None for no activities"
63 );
64 assert!(
65 set.len() <= MAX_WORKER_TAGS,
66 "Worker can subscribe to at most {} tags, got {}",
67 MAX_WORKER_TAGS,
68 set.len()
69 );
70 TagFilter::Tags(set)
71 }
72
73 /// Create a filter for default plus specific tags.
74 ///
75 /// # Panics
76 /// Panics if more than [`MAX_WORKER_TAGS`] (5) tags are provided.
77 pub fn default_and<I, S>(tags: I) -> Self
78 where
79 I: IntoIterator<Item = S>,
80 S: Into<String>,
81 {
82 let set: HashSet<String> = tags.into_iter().map(Into::into).collect();
83 assert!(
84 !set.is_empty(),
85 "TagFilter::default_and() requires at least one tag; use TagFilter::DefaultOnly for untagged only"
86 );
87 assert!(
88 set.len() <= MAX_WORKER_TAGS,
89 "Worker can subscribe to at most {} tags, got {}",
90 MAX_WORKER_TAGS,
91 set.len()
92 );
93 TagFilter::DefaultAnd(set)
94 }
95
96 /// Create a filter for only untagged activities.
97 pub fn default_only() -> Self {
98 TagFilter::DefaultOnly
99 }
100
101 /// Create a filter that processes no activities.
102 pub fn none() -> Self {
103 TagFilter::None
104 }
105
106 /// Create a filter that processes all activities regardless of tag.
107 pub fn any() -> Self {
108 TagFilter::Any
109 }
110
111 /// Check if an activity with the given tag matches this filter.
112 pub fn matches(&self, tag: Option<&str>) -> bool {
113 match (self, tag) {
114 (TagFilter::Any, _) => true,
115 (TagFilter::None, _) => false,
116 (TagFilter::DefaultOnly, Option::None) => true,
117 (TagFilter::DefaultOnly, Some(_)) => false,
118 (TagFilter::Tags(_), Option::None) => false,
119 (TagFilter::Tags(set), Some(t)) => set.contains(t),
120 (TagFilter::DefaultAnd(_), Option::None) => true,
121 (TagFilter::DefaultAnd(set), Some(t)) => set.contains(t),
122 }
123 }
124}
125
126/// Returns the current build version of the duroxide crate.
127pub fn current_build_version() -> semver::Version {
128 semver::Version::parse(env!("CARGO_PKG_VERSION")).expect("CARGO_PKG_VERSION must be valid semver")
129}
130
131/// An inclusive version range: [min, max].
132///
133/// Both bounds are inclusive. For example, `SemverRange { min: (0,0,0), max: (1,5,0) }`
134/// matches any version `v` where `0.0.0 <= v <= 1.5.0`.
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct SemverRange {
137 pub min: semver::Version,
138 pub max: semver::Version,
139}
140
141impl SemverRange {
142 pub fn new(min: semver::Version, max: semver::Version) -> Self {
143 Self { min, max }
144 }
145
146 /// Check if a version falls within this range (inclusive on both ends).
147 pub fn contains(&self, version: &semver::Version) -> bool {
148 version >= &self.min && version <= &self.max
149 }
150
151 /// Default range: `>=0.0.0, <=CURRENT_BUILD_VERSION`.
152 ///
153 /// This means the runtime can replay any execution pinned at or below its own
154 /// build version. Replay engines are backward-compatible.
155 pub fn default_for_current_build() -> Self {
156 Self {
157 min: semver::Version::new(0, 0, 0),
158 max: current_build_version(),
159 }
160 }
161}
162
163/// Capability filter passed by the orchestration dispatcher to the provider.
164///
165/// The provider uses this to return only orchestration items whose pinned
166/// `duroxide_version` falls within one of the supported ranges.
167///
168/// If no filter is supplied (`None`), the provider returns any available item
169/// (legacy behavior, useful for admin tooling).
170#[derive(Debug, Clone)]
171pub struct DispatcherCapabilityFilter {
172 /// Supported duroxide version ranges. An execution is compatible if its
173 /// pinned duroxide version falls within ANY of these ranges.
174 ///
175 /// Phase 1 typically has a single range: `[>=0.0.0, <=CURRENT_BUILD_VERSION]`.
176 pub supported_duroxide_versions: Vec<SemverRange>,
177}
178
179impl DispatcherCapabilityFilter {
180 /// Check if a pinned version is compatible with this filter.
181 pub fn is_compatible(&self, version: &semver::Version) -> bool {
182 self.supported_duroxide_versions.iter().any(|r| r.contains(version))
183 }
184
185 /// Build the default filter for the current build version.
186 pub fn default_for_current_build() -> Self {
187 Self {
188 supported_duroxide_versions: vec![SemverRange::default_for_current_build()],
189 }
190 }
191}
192
193/// Identity of an activity for cancellation purposes.
194///
195/// Used by the runtime to specify which activities should be cancelled
196/// during an orchestration turn (e.g., select losers, orchestration termination).
197#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct ScheduledActivityIdentifier {
199 /// Instance ID of the orchestration
200 pub instance: String,
201 /// Execution ID within the instance
202 pub execution_id: u64,
203 /// Activity ID (the event_id of the ActivityScheduled event)
204 pub activity_id: u64,
205}
206
207/// Configuration for session-aware work item fetching.
208///
209/// When passed to [`Provider::fetch_work_item`], enables session routing:
210/// - Non-session items (`session_id IS NULL`) are always eligible
211/// - Items for sessions already owned by `owner_id` are eligible
212/// - Items for unowned (claimable) sessions are eligible
213///
214/// When `fetch_work_item` is called with `session: None`, only non-session
215/// items are returned.
216#[derive(Debug, Clone)]
217pub struct SessionFetchConfig {
218 /// Identity tag for session ownership. All worker slots sharing this
219 /// value are treated as a single owner for session affinity purposes.
220 /// Typically set to `RuntimeOptions::worker_node_id` (process-level)
221 /// or a per-slot UUID (ephemeral).
222 pub owner_id: String,
223 /// How long to hold the session lock when claiming a new session.
224 pub lock_timeout: Duration,
225}
226
227/// Orchestration item containing all data needed to process an instance atomically.
228///
229/// This represents a locked batch of work for a single orchestration instance.
230/// The provider must guarantee that no other process can modify this instance
231/// until `ack_orchestration_item()` or `abandon_orchestration_item()` is called.
232///
233/// # Fields
234///
235/// * `instance` - Unique identifier for the orchestration instance (e.g., "order-123")
236/// * `orchestration_name` - Name of the orchestration being executed (e.g., "ProcessOrder")
237/// * `execution_id` - Current execution ID (starts at 1, increments with ContinueAsNew)
238/// * `version` - Orchestration version string (e.g., "1.0.0")
239/// * `history` - Complete event history for the current execution (ordered by event_id)
240/// * `messages` - Batch of WorkItems to process (may include Start, completions, external events)
241/// * `lock_token` - Unique token that must be used to ack or abandon this batch
242/// * `history_error` - If set, history deserialization failed; `history` may be empty or partial
243///
244/// # Implementation Notes
245///
246/// - The provider must ensure `history` contains ALL events for `execution_id` in order
247/// - For multi-execution instances (ContinueAsNew), only return the LATEST execution's history
248/// - The `lock_token` must be unique and prevent concurrent processing of the same instance
249/// - All messages in the batch should belong to the same instance
250/// - The lock should expire after a timeout (e.g., 30s) to handle worker crashes
251/// - If history deserialization fails, set `history_error` with the error message and return
252/// `Ok(Some(...))` instead of `Err(...)`. This allows the runtime to reach the poison check
253/// and terminate the orchestration after `max_attempts`.
254///
255/// # Example from SQLite Provider
256///
257/// ```text
258/// // 1. Find available instance (check instance_locks for active locks)
259/// SELECT q.instance_id FROM orchestrator_queue q
260/// LEFT JOIN instance_locks il ON q.instance_id = il.instance_id
261/// WHERE q.visible_at <= now()
262/// AND (il.instance_id IS NULL OR il.locked_until <= now())
263/// ORDER BY q.id LIMIT 1
264///
265/// // 2. Atomically acquire instance-level lock
266/// INSERT INTO instance_locks (instance_id, lock_token, locked_until, locked_at)
267/// VALUES (?, ?, ?, ?)
268/// ON CONFLICT(instance_id) DO UPDATE
269/// SET lock_token = excluded.lock_token, locked_until = excluded.locked_until
270/// WHERE locked_until <= excluded.locked_at
271///
272/// // 3. Lock all visible messages for that instance
273/// UPDATE orchestrator_queue SET lock_token = ?, locked_until = ?
274/// WHERE instance_id = ? AND visible_at <= now()
275///
276/// // 4. Load instance metadata
277/// SELECT orchestration_name, orchestration_version, current_execution_id
278/// FROM instances WHERE instance_id = ?
279///
280/// // 5. Load history for current execution
281/// SELECT event_data FROM history
282/// WHERE instance_id = ? AND execution_id = ?
283/// ORDER BY event_id
284///
285/// // 6. Return OrchestrationItem with lock_token
286/// ```
287#[derive(Debug, Clone)]
288pub struct OrchestrationItem {
289 pub instance: String,
290 pub orchestration_name: String,
291 pub execution_id: u64,
292 pub version: String,
293 pub history: Vec<Event>,
294 pub messages: Vec<WorkItem>,
295 /// If set, history deserialization failed. `history` may be empty or partial.
296 ///
297 /// The runtime treats this as a terminal error: it abandons the item with backoff,
298 /// and once `attempt_count > max_attempts`, poisons the orchestration.
299 ///
300 /// Providers SHOULD return `Ok(Some(...))` with this field set instead of
301 /// `Err(ProviderError::permanent(...))` when deserialization fails after acquiring
302 /// a lock. This ensures the lock lifecycle stays clean (Ok = lock held, Err = no lock).
303 pub history_error: Option<String>,
304
305 /// KV snapshot loaded from the materialized `kv_store` table at fetch time.
306 /// Seeds the orchestration's in-memory `kv_state` and `kv_metadata` before the turn executes.
307 pub kv_snapshot: std::collections::HashMap<String, KvEntry>,
308}
309
310/// A single entry in the KV store snapshot.
311#[derive(Debug, Clone, PartialEq)]
312pub struct KvEntry {
313 /// The stored value.
314 pub value: String,
315 /// Timestamp (ms since epoch) when this key was last written.
316 /// Set by the runtime at `set_kv_value()` time, persisted by the provider.
317 pub last_updated_at_ms: u64,
318}
319
320/// Execution metadata computed by the runtime to be persisted by the provider.
321///
322/// The runtime inspects the `history_delta` and `orchestrator_items` to compute this metadata.
323/// **Providers must NOT inspect event contents themselves** - they should blindly store this metadata.
324///
325/// This design ensures the provider remains a pure storage abstraction without orchestration knowledge.
326///
327/// # Fields
328///
329/// * `status` - New execution status: `Some("Completed")`, `Some("Failed")`, `Some("ContinuedAsNew")`, or `None`
330/// - `None` means the execution is still running (no status update needed)
331/// - Provider should update the stored execution status when `Some(...)`
332///
333/// * `output` - The terminal value to store (depends on status):
334/// - `Completed`: The orchestration's successful result
335/// - `Failed`: The error message
336/// - `ContinuedAsNew`: The input that was passed to continue_as_new()
337/// - `None`: No output (execution still running)
338///
339/// # Example Usage in Provider
340///
341/// ```text
342/// async fn ack_orchestration_item(..., metadata: ExecutionMetadata) {
343/// // Store metadata without understanding what it means
344/// if let Some(status) = &metadata.status {
345/// UPDATE executions SET status = ?, output = ? WHERE instance_id = ? AND execution_id = ?
346/// }
347/// }
348/// ```
349///
350/// # ContinueAsNew Handling
351///
352/// ContinueAsNew is handled entirely by the runtime. Providers must NOT try to
353/// synthesize new executions in `fetch_orchestration_item`.
354///
355/// Runtime behavior:
356/// - When an orchestration calls `continue_as_new(input)`, the runtime stamps
357/// `OrchestrationContinuedAsNew` into the current execution's history and enqueues
358/// a `WorkItem::ContinueAsNew`.
359/// - When processing that work item, the runtime starts a fresh execution with
360/// `execution_id = current + 1`, passes an empty `existing_history`, and stamps an
361/// `OrchestrationStarted { event_id: 1, .. }` event for the new execution.
362/// - The runtime then calls `ack_orchestration_item(lock_token, execution_id, ...)` with
363/// the explicit execution id to persist history and queue operations.
364///
365/// Provider responsibilities:
366/// - Use the explicit `execution_id` given to `ack_orchestration_item`.
367/// - Idempotently create the execution record/entry if it doesn't already exist.
368/// - Update the instance's “current execution” pointer to be at least `execution_id`.
369/// - Append all `history_delta` events to the specified `execution_id`.
370/// - Update `executions.status, executions.output` from `ExecutionMetadata` when provided.
371#[derive(Debug, Clone, Default)]
372pub struct ExecutionMetadata {
373 /// New status for the execution ('Completed', 'Failed', 'ContinuedAsNew', or None to keep current)
374 pub status: Option<String>,
375 /// Output/error/input to store (for Completed/Failed/ContinuedAsNew)
376 pub output: Option<String>,
377 /// Orchestration name (for new instances or updates)
378 pub orchestration_name: Option<String>,
379 /// Orchestration version (for new instances or updates)
380 pub orchestration_version: Option<String>,
381 /// Parent instance ID (for sub-orchestrations, used for cascading delete)
382 pub parent_instance_id: Option<String>,
383 /// Pinned duroxide version for this execution (set from OrchestrationStarted event).
384 ///
385 /// The provider stores this alongside the execution record for efficient
386 /// capability filtering.
387 ///
388 /// - `Some(v)`: Store `v` as the execution's pinned version. The provider should
389 /// update the stored version unconditionally when provided.
390 /// - `None`: No version update requested. The provider should not modify the
391 /// existing stored value.
392 ///
393 /// The **runtime** guarantees this is only set on the first turn of a new execution
394 /// (when `OrchestrationStarted` is in the history delta), enforced by a `debug_assert`
395 /// in the orchestration dispatcher. The provider does not need to enforce write-once
396 /// semantics — it simply stores what it's told.
397 pub pinned_duroxide_version: Option<semver::Version>,
398}
399
400/// Provider-backed work queue items the runtime consumes continually.
401///
402/// WorkItems represent messages that flow through provider-managed queues.
403/// They are serialized/deserialized using serde_json for storage.
404///
405/// # Queue Routing
406///
407/// Different WorkItem types go to different queues:
408/// - `StartOrchestration, ContinueAsNew, ActivityCompleted/Failed, TimerFired, ExternalRaised, SubOrchCompleted/Failed, CancelInstance` → **Orchestrator queue**
409/// - TimerFired items use `visible_at = fire_at_ms` for delayed visibility
410/// - `ActivityExecute` → **Worker queue**
411///
412/// # Instance ID Extraction
413///
414/// Most WorkItems have an `instance` field. Sub-orchestration completions use `parent_instance`.
415/// Providers need to extract the instance ID for routing. SQLite example:
416///
417/// ```ignore
418/// let instance = match &item {
419/// WorkItem::StartOrchestration { instance, .. } |
420/// WorkItem::ActivityCompleted { instance, .. } |
421/// WorkItem::CancelInstance { instance, .. } => instance,
422/// WorkItem::SubOrchCompleted { parent_instance, .. } => parent_instance,
423/// _ => return Err("unexpected item type"),
424/// };
425/// ```
426///
427/// # Execution ID Tracking
428///
429/// WorkItems for activities, timers, and sub-orchestrations include `execution_id`.
430/// This allows providers to route completions to the correct execution when ContinueAsNew creates multiple executions.
431///
432/// **Critical:** Completions with mismatched execution_id should still be enqueued (the runtime filters them).
433#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
434pub enum WorkItem {
435 /// Start a new orchestration instance
436 /// - Instance metadata is created by runtime via ack_orchestration_item metadata (not on enqueue)
437 /// - `execution_id`: The execution ID for this start (usually INITIAL_EXECUTION_ID=1)
438 /// - `version`: None means runtime will resolve from registry
439 StartOrchestration {
440 instance: String,
441 orchestration: String,
442 input: String,
443 version: Option<String>,
444 parent_instance: Option<String>,
445 parent_id: Option<u64>,
446 /// Execution id of the parent that scheduled this sub-orchestration, stamped at
447 /// schedule time so completion/failure notifications route back to the exact
448 /// parent execution that awaited the child (rather than the parent's current
449 /// execution at completion time). `None` for top-level starts and for work items
450 /// produced by older runtimes; routing then falls back to a durable provider read.
451 #[serde(default, skip_serializing_if = "Option::is_none")]
452 parent_execution_id: Option<u64>,
453 execution_id: u64,
454 },
455
456 /// Execute an activity (goes to worker queue)
457 /// - `id`: event_id from ActivityScheduled (for correlation)
458 /// - Worker will enqueue ActivityCompleted or ActivityFailed
459 ActivityExecute {
460 instance: String,
461 execution_id: u64,
462 id: u64, // scheduling_event_id from ActivityScheduled
463 name: String,
464 input: String,
465 #[serde(skip_serializing_if = "Option::is_none")]
466 #[serde(default)]
467 session_id: Option<String>,
468 /// Routing tag for directing this activity to specific workers.
469 /// `None` means default (untagged) queue.
470 #[serde(skip_serializing_if = "Option::is_none")]
471 #[serde(default)]
472 tag: Option<String>,
473 },
474
475 /// Activity completed successfully (goes to orchestrator queue)
476 /// - `id`: source_event_id referencing the ActivityScheduled event
477 /// - Triggers next orchestration turn
478 ActivityCompleted {
479 instance: String,
480 execution_id: u64,
481 id: u64, // source_event_id referencing ActivityScheduled
482 result: String,
483 },
484
485 /// Activity failed with error (goes to orchestrator queue)
486 /// - `id`: source_event_id referencing the ActivityScheduled event
487 /// - Triggers next orchestration turn
488 ActivityFailed {
489 instance: String,
490 execution_id: u64,
491 id: u64, // source_event_id referencing ActivityScheduled
492 details: crate::ErrorDetails,
493 },
494
495 /// Timer fired (goes to orchestrator queue with delayed visibility)
496 /// - Created directly by runtime when timer is scheduled
497 /// - Enqueued to orchestrator queue with `visible_at = fire_at_ms`
498 /// - Orchestrator dispatcher processes when `visible_at <= now()`
499 TimerFired {
500 instance: String,
501 execution_id: u64,
502 id: u64, // source_event_id referencing TimerCreated
503 fire_at_ms: u64,
504 },
505
506 /// External event raised (goes to orchestrator queue)
507 /// - Matched by `name` to ExternalSubscribed events
508 /// - `data`: JSON payload from external system
509 ExternalRaised {
510 instance: String,
511 name: String,
512 data: String,
513 },
514
515 /// Sub-orchestration completed (goes to parent's orchestrator queue)
516 /// - Routes to `parent_instance`, not the child
517 /// - `parent_id`: event_id from parent's SubOrchestrationScheduled event
518 SubOrchCompleted {
519 parent_instance: String,
520 parent_execution_id: u64,
521 parent_id: u64, // source_event_id referencing SubOrchestrationScheduled
522 result: String,
523 },
524
525 /// Sub-orchestration failed (goes to parent's orchestration queue)
526 /// - Routes to `parent_instance`, not the child
527 /// - `parent_id`: event_id from parent's SubOrchestrationScheduled event
528 SubOrchFailed {
529 parent_instance: String,
530 parent_execution_id: u64,
531 parent_id: u64, // source_event_id referencing SubOrchestrationScheduled
532 details: crate::ErrorDetails,
533 },
534
535 /// Request orchestration cancellation (goes to orchestrator queue)
536 /// - Runtime will append OrchestrationCancelRequested event
537 /// - Eventually results in OrchestrationFailed with "canceled: {reason}"
538 CancelInstance { instance: String, reason: String },
539
540 /// Continue orchestration as new execution (goes to orchestrator queue)
541 /// - Signals the end of current execution and start of next
542 /// - Runtime will create Event::OrchestrationStarted for next execution
543 /// - Provider should create new execution (see ExecutionMetadata.create_next_execution)
544 ContinueAsNew {
545 instance: String,
546 orchestration: String,
547 input: String,
548 version: Option<String>,
549 /// Parent instance ID, preserved across continue-as-new so a logical
550 /// sub-orchestration can still notify its parent when a later execution
551 /// completes or fails.
552 ///
553 /// Absent on messages enqueued by older runtimes, in which case the parent
554 /// link is not preserved for that execution.
555 #[serde(skip_serializing_if = "Option::is_none")]
556 #[serde(default)]
557 parent_instance: Option<String>,
558 /// Parent's `SubOrchestrationScheduled` event id, preserved across
559 /// continue-as-new alongside `parent_instance`. Absent on messages enqueued
560 /// by older runtimes, with the same consequence described above.
561 #[serde(skip_serializing_if = "Option::is_none")]
562 #[serde(default)]
563 parent_id: Option<u64>,
564 /// Parent's execution id, preserved across continue-as-new so completion
565 /// notifications route to the exact parent execution that scheduled this
566 /// child. Absent on messages enqueued by older runtimes, with the same
567 /// consequence described above.
568 #[serde(skip_serializing_if = "Option::is_none")]
569 #[serde(default)]
570 parent_execution_id: Option<u64>,
571 /// Persistent events carried forward from the previous execution.
572 /// These are seeded into the new execution's history before any new
573 /// externally-raised events, preserving FIFO order across CAN boundaries.
574 #[serde(default)]
575 carry_forward_events: Vec<(String, String)>,
576 /// Custom status accumulated from the previous execution, if any.
577 /// Carried forward so `get_custom_status()` works immediately in the new execution.
578 #[serde(skip_serializing_if = "Option::is_none")]
579 #[serde(default)]
580 initial_custom_status: Option<String>,
581 },
582
583 /// Persistent external event raised (goes to orchestrator queue).
584 /// Matched by `name` using FIFO mailbox semantics — events stick around until consumed.
585 QueueMessage {
586 instance: String,
587 name: String,
588 data: String,
589 },
590
591 /// V2: External event with topic-based pub/sub matching (goes to orchestrator queue).
592 ///
593 /// Matched by `name` AND `topic` to ExternalSubscribed2 events.
594 /// Feature-gated for replay engine extensibility verification.
595 #[cfg(feature = "replay-version-test")]
596 ExternalRaised2 {
597 instance: String,
598 name: String,
599 topic: String,
600 data: String,
601 },
602}
603
604/// Provider abstraction for durable orchestration execution (persistence + queues).
605///
606/// # Overview
607///
608/// A Provider is responsible for:
609/// 1. **Persistence**: Storing orchestration history (append-only event log)
610/// 2. **Queueing**: Managing two work queues (orchestrator, worker)
611/// 3. **Locking**: Implementing peek-lock semantics to prevent concurrent processing
612/// 4. **Atomicity**: Ensuring transactional consistency across operations
613///
614/// # Architecture: Two-Queue Model
615///
616/// ```text
617/// ┌─────────────────────────────────────────────────────────────┐
618/// │ RUNTIME (2 Dispatchers) │
619/// ├─────────────────────────────────────────────────────────────┤
620/// │ │
621/// │ [Orchestration Dispatcher] ←─── fetch_orchestration_item │
622/// │ ↓ │
623/// │ Process Turn │
624/// │ ↓ │
625/// │ ack_orchestration_item ────┬──► Orchestrator Queue │
626/// │ │ (TimerFired with delayed │
627/// │ │ visibility for timers) │
628/// │ └──► Worker Queue │
629/// │ │
630/// │ [Worker Dispatcher] ←────────── fetch_work_item │
631/// │ Execute Activity │
632/// │ ↓ │
633/// │ Completion ────────────────────► Orchestrator Queue │
634/// │ │
635/// └─────────────────────────────────────────────────────────────┘
636/// ↕
637/// ┌────────────────────────┐
638/// │ PROVIDER (Storage) │
639/// │ ┌──────────────────┐ │
640/// │ │ History (Events) │ │
641/// │ ├──────────────────┤ │
642/// │ │ Orch Queue │ │
643/// │ │ Worker Queue │ │
644/// │ └──────────────────┘ │
645/// └────────────────────────┘
646/// ```
647///
648/// # Design Principles
649///
650/// **Providers should be storage abstractions, NOT orchestration engines:**
651/// - ✅ Store and retrieve events as opaque data (don't inspect Event contents)
652/// - ✅ Manage queues and locks (generic queue operations)
653/// - ✅ Provide ACID guarantees where possible
654/// - ❌ DON'T interpret orchestration semantics (use ExecutionMetadata from runtime)
655/// - ❌ DON'T create events (runtime creates all events)
656/// - ❌ DON'T make orchestration decisions (runtime decides control flow)
657///
658/// # Multi-Execution Support (ContinueAsNew)
659///
660/// Orchestration instances can have multiple executions (execution_id 1, 2, 3, ...):
661/// - Each execution has its own event history
662/// - `read()` should return the LATEST execution's history
663/// - Provider tracks current_execution_id to know which execution is active
664/// - When metadata.create_next_execution=true, create a new execution record/entry
665///
666/// **Example:**
667/// ```text
668/// Instance "order-123":
669/// Execution 1: [OrchestrationStarted, ActivityScheduled, OrchestrationContinuedAsNew]
670/// Execution 2: [OrchestrationStarted, ActivityScheduled, OrchestrationCompleted]
671///
672/// read("order-123") → Returns Execution 2's events (latest)
673/// read_with_execution("order-123", 1) → Returns Execution 1's events
674/// latest_execution_id("order-123") → Returns Some(2)
675/// ```
676///
677/// # Concurrency Model
678///
679/// The runtime runs 3 background dispatchers polling your queues:
680/// 1. **Orchestration Dispatcher**: Polls fetch_orchestration_item() continuously
681/// 2. **Work Dispatcher**: Polls fetch_work_item() continuously
682/// 3. **Timer Dispatcher**: Polls dequeue_timer_peek_lock() continuously
683///
684/// **Your implementation must be thread-safe** and support concurrent access from multiple dispatchers.
685///
686/// # Peek-Lock Pattern (Critical)
687///
688/// All dequeue operations use peek-lock semantics:
689/// 1. **Peek**: Select and lock a message (message stays in queue)
690/// 2. **Process**: Runtime processes the locked message
691/// 3. **Ack**: Delete message from queue (success) OR
692/// 4. **Abandon**: Release lock for retry (failure)
693///
694/// Benefits:
695/// - At-least-once delivery (messages survive crashes)
696/// - Automatic retry on worker failure (lock expires)
697/// - Prevents duplicate processing (locked messages invisible to others)
698///
699/// # Transactional Guarantees
700///
701/// **`ack_orchestration_item()` is the atomic boundary:**
702/// - ALL operations in ack must succeed or fail together
703/// - History append + queue enqueues + lock release = atomic
704/// - If commit fails, entire turn is retried
705/// - This ensures exactly-once semantics for orchestration turns
706///
707/// # Queue Message Flow
708///
709/// **Orchestrator Queue:**
710/// - Inputs: StartOrchestration, ActivityCompleted/Failed, TimerFired, ExternalRaised, SubOrchCompleted/Failed, CancelInstance, ContinueAsNew
711/// - Output: Processed by orchestration dispatcher → ack_orchestration_item
712/// - Batching: All messages for an instance processed together
713/// - Timers: TimerFired items are enqueued with `visible_at = fire_at_ms` for delayed visibility
714///
715/// **Worker Queue:**
716/// - Inputs: ActivityExecute (from ack_orchestration_item)
717/// - Output: Processed by work dispatcher → ActivityCompleted/Failed to orch queue
718/// - Batching: One message at a time (activities executed independently)
719///
720/// **Note:** There is no separate timer queue. Timers are handled by enqueuing TimerFired items
721/// directly to the orchestrator queue with delayed visibility (`visible_at` set to `fire_at_ms`).
722/// The orchestrator dispatcher processes them when `visible_at <= now()`.
723///
724/// # Instance Metadata Management
725///
726/// Providers typically maintain metadata about instances:
727/// - instance_id (primary key)
728/// - orchestration_name, orchestration_version
729/// - current_execution_id (for multi-execution support)
730/// - status, output (optional, for quick queries)
731/// - created_at, updated_at timestamps
732///
733/// This metadata is updated via:
734/// - `enqueue_for_orchestrator()` with StartOrchestration
735/// - `ack_orchestration_item()` with history changes
736/// - `ExecutionMetadata` for status/output updates
737///
738/// # Required vs Optional Methods
739///
740/// **REQUIRED** (must implement):
741/// - fetch_orchestration_item, ack_orchestration_item, abandon_orchestration_item
742/// - read, append_with_execution
743/// - enqueue_for_worker, fetch_work_item, ack_work_item
744/// - enqueue_for_orchestrator
745///
746/// **OPTIONAL** (has defaults):
747/// - latest_execution_id, read_with_execution
748/// - list_instances, list_executions
749///
750/// # Recommended Database Schema (SQL Example)
751///
752/// ```sql
753/// -- Instance metadata
754/// CREATE TABLE instances (
755/// instance_id TEXT PRIMARY KEY,
756/// orchestration_name TEXT NOT NULL,
757/// orchestration_version TEXT, -- NULLable, set by runtime via metadata
758/// current_execution_id INTEGER DEFAULT 1,
759/// created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
760/// );
761///
762/// -- Execution tracking
763/// CREATE TABLE executions (
764/// instance_id TEXT NOT NULL,
765/// execution_id INTEGER NOT NULL,
766/// status TEXT DEFAULT 'Running',
767/// output TEXT,
768/// started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
769/// completed_at TIMESTAMP,
770/// PRIMARY KEY (instance_id, execution_id)
771/// );
772///
773/// -- Event history (append-only)
774/// CREATE TABLE history (
775/// instance_id TEXT NOT NULL,
776/// execution_id INTEGER NOT NULL,
777/// event_id INTEGER NOT NULL,
778/// event_type TEXT NOT NULL,
779/// event_data TEXT NOT NULL, -- JSON serialized Event
780/// created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
781/// PRIMARY KEY (instance_id, execution_id, event_id)
782/// );
783///
784/// -- Orchestrator queue (peek-lock)
785/// CREATE TABLE orchestrator_queue (
786/// id INTEGER PRIMARY KEY AUTOINCREMENT,
787/// instance_id TEXT NOT NULL,
788/// work_item TEXT NOT NULL, -- JSON serialized WorkItem
789/// visible_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
790/// lock_token TEXT,
791/// locked_until TIMESTAMP,
792/// created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
793/// );
794///
795/// -- Worker queue (peek-lock with visibility control)
796/// CREATE TABLE worker_queue (
797/// id INTEGER PRIMARY KEY AUTOINCREMENT,
798/// work_item TEXT NOT NULL,
799/// visible_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
800/// lock_token TEXT,
801/// locked_until TIMESTAMP,
802/// attempt_count INTEGER NOT NULL DEFAULT 0,
803/// created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
804/// );
805///
806/// -- Instance-level locks (CRITICAL: Prevents concurrent processing)
807/// CREATE TABLE instance_locks (
808/// instance_id TEXT PRIMARY KEY,
809/// lock_token TEXT NOT NULL,
810/// locked_until INTEGER NOT NULL, -- Unix timestamp (milliseconds)
811/// locked_at INTEGER NOT NULL -- Unix timestamp (milliseconds)
812/// );
813/// ```
814///
815/// # Implementing a New Provider: Checklist
816///
817/// 1. ✅ **Storage Layer**: Choose backing store (PostgreSQL, Redis, DynamoDB, etc.)
818/// 2. ✅ **Serialization**: Use serde_json for Event and WorkItem (or compatible format)
819/// 3. ✅ **Instance Locking**: Implement instance-level locks to prevent concurrent processing
820/// 4. ✅ **Locking**: Implement peek-lock with unique tokens and expiration
821/// 5. ✅ **Transactions**: Ensure `ack_orchestration_item` is atomic
822/// 6. ✅ **Indexes**: Add indexes on `instance_id`, `lock_token`, `visible_at` for orchestrator queue
823/// 7. ✅ **Delayed Visibility**: Support `visible_at` timestamps for TimerFired items in orchestrator queue
824/// 8. ✅ **Testing**: Use `tests/sqlite_provider_validations.rs` as a template, or see `docs/provider-implementation-guide.md`
825/// 9. ✅ **Multi-execution**: Support execution_id partitioning for ContinueAsNew
826/// 10. ⚠️ **DO NOT**: Inspect event contents (use ExecutionMetadata)
827/// 11. ⚠️ **DO NOT**: Create events (runtime owns event creation)
828/// 12. ⚠️ **DO NOT**: Make orchestration decisions (runtime owns logic)
829///
830/// # Example: Minimal Redis Provider Sketch
831///
832/// ```ignore
833/// struct RedisProvider {
834/// client: redis::Client,
835/// lock_timeout: Duration,
836/// }
837///
838/// impl Provider for RedisProvider {
839/// async fn fetch_orchestration_item(&self) -> Option<OrchestrationItem> {
840/// // 1. RPOPLPUSH from "orch_queue" to "orch_processing"
841/// let instance = self.client.rpoplpush("orch_queue", "orch_processing")?;
842///
843/// // 2. Load history from "history:{instance}:{exec_id}" (sorted set)
844/// let exec_id = self.client.get(&format!("instance:{instance}:exec_id")).unwrap_or(1);
845/// let history = self.client.zrange(&format!("history:{instance}:{exec_id}"), 0, -1);
846///
847/// // 3. Generate lock token and set expiration
848/// let lock_token = uuid::Uuid::new_v4().to_string();
849/// self.client.setex(&format!("lock:{lock_token}"), self.lock_timeout.as_secs() as usize, &instance);
850///
851/// Some(OrchestrationItem { instance, history, lock_token, ... })
852/// }
853///
854/// async fn ack_orchestration_item(..., metadata: ExecutionMetadata) -> Result<(), String> {
855/// let mut pipe = redis::pipe();
856/// pipe.atomic(); // Use Redis transaction
857///
858/// // Append history (ZADD to sorted set with event_id as score)
859/// for event in history_delta {
860/// pipe.zadd(&format!("history:{instance}:{exec_id}"), event_json, event.event_id());
861/// }
862///
863/// // Update metadata (HSET)
864/// if let Some(status) = &metadata.status {
865/// pipe.hset(&format!("exec:{instance}:{exec_id}"), "status", status);
866/// pipe.hset(&format!("exec:{instance}:{exec_id}"), "output", &metadata.output);
867/// }
868///
869/// // Create next execution if needed
870/// if metadata.create_next_execution {
871/// pipe.incr(&format!("instance:{instance}:exec_id"), 1);
872/// }
873///
874/// // Enqueue worker/orchestrator items
875/// for item in worker_items { pipe.lpush("worker_queue", serialize(item)); }
876/// for item in orch_items {
877/// let visible_at = match &item {
878/// WorkItem::TimerFired { fire_at_ms, .. } => *fire_at_ms,
879/// _ => now()
880/// };
881/// pipe.zadd("orch_queue", serialize(item), visible_at);
882/// }
883///
884/// // Release lock
885/// pipe.del(&format!("lock:{lock_token}"));
886/// pipe.lrem("orch_processing", 1, instance);
887///
888/// pipe.execute()?;
889/// Ok(())
890/// }
891/// }
892/// ```
893///
894/// # Design Principles
895///
896/// **Providers should be storage abstractions, NOT orchestration engines:**
897/// - ✅ Store and retrieve events as opaque data (don't inspect Event contents)
898/// - ✅ Manage queues and locks (generic queue operations)
899/// - ✅ Provide ACID guarantees where possible
900/// - ❌ DON'T interpret orchestration semantics (use ExecutionMetadata from runtime)
901/// - ❌ DON'T create events (runtime creates all events)
902/// - ❌ DON'T make orchestration decisions (runtime decides control flow)
903///
904/// # Multi-Execution Support (ContinueAsNew)
905///
906/// Orchestration instances can have multiple executions (execution_id 1, 2, 3, ...):
907/// - Each execution has its own event history
908/// - `read()` should return the LATEST execution's history
909/// - Provider tracks current_execution_id to know which execution is active
910/// - When metadata.create_next_execution=true, create a new execution record/entry
911///
912/// **Example:**
913/// ```text
914/// Instance "order-123":
915/// Execution 1: [OrchestrationStarted, ActivityScheduled, OrchestrationContinuedAsNew]
916/// Execution 2: [OrchestrationStarted, ActivityScheduled, OrchestrationCompleted]
917///
918/// read("order-123") → Returns Execution 2's events (latest)
919/// read_with_execution("order-123", 1) → Returns Execution 1's events
920/// latest_execution_id("order-123") → Returns Some(2)
921/// list_executions("order-123") → Returns vec!\[1, 2\]
922/// ```
923///
924/// # Concurrency Model
925///
926/// The runtime runs 3 background dispatchers polling your queues:
927/// 1. **Orchestration Dispatcher**: Polls fetch_orchestration_item() continuously
928/// 2. **Work Dispatcher**: Polls fetch_work_item() continuously
929/// 3. **Timer Dispatcher**: Polls dequeue_timer_peek_lock() continuously
930///
931/// **Your implementation must be thread-safe** and support concurrent access from multiple dispatchers.
932///
933/// # Peek-Lock Pattern (Critical)
934///
935/// All dequeue operations use peek-lock semantics:
936/// 1. **Peek**: Select and lock a message (message stays in queue)
937/// 2. **Process**: Runtime processes the locked message
938/// 3. **Ack**: Delete message from queue (success) OR
939/// 4. **Abandon**: Release lock for retry (failure)
940///
941/// Benefits:
942/// - At-least-once delivery (messages survive crashes)
943/// - Automatic retry on worker failure (lock expires)
944/// - Prevents duplicate processing (locked messages invisible to others)
945///
946/// # Transactional Guarantees
947///
948/// **`ack_orchestration_item()` is the atomic boundary:**
949/// - ALL operations in ack must succeed or fail together
950/// - History append + queue enqueues + lock release = atomic
951/// - If commit fails, entire turn is retried
952/// - This ensures exactly-once semantics for orchestration turns
953///
954/// # Queue Message Flow
955///
956/// **Orchestrator Queue:**
957/// - Inputs: StartOrchestration, ActivityCompleted/Failed, TimerFired, ExternalRaised, SubOrchCompleted/Failed, CancelInstance, ContinueAsNew
958/// - Output: Processed by orchestration dispatcher → ack_orchestration_item
959/// - Batching: All messages for an instance processed together
960/// - Ordering: FIFO per instance preferred
961/// - Timers: TimerFired items are enqueued with `visible_at = fire_at_ms` for delayed visibility
962///
963/// **Worker Queue:**
964/// - Inputs: ActivityExecute (from ack_orchestration_item)
965/// - Output: Processed by work dispatcher → ActivityCompleted/Failed to orch queue
966/// - Batching: One message at a time (activities executed independently)
967/// - Ordering: FIFO preferred but not required
968///
969/// **Note:** There is no separate timer queue. Timers are handled by enqueuing TimerFired items
970/// directly to the orchestrator queue with delayed visibility (`visible_at` set to `fire_at_ms`).
971/// The orchestrator dispatcher processes them when `visible_at <= now()`.
972///
973/// # Instance Metadata Management
974///
975/// Providers typically maintain metadata about instances:
976/// - instance_id (primary key)
977/// - orchestration_name, orchestration_version (from StartOrchestration)
978/// - current_execution_id (for multi-execution support)
979/// - status, output (optional, from ExecutionMetadata)
980/// - created_at, updated_at timestamps
981///
982/// This metadata is updated via:
983/// - `ack_orchestration_item()` with ExecutionMetadata (creates instance and updates status/output)
984/// - `ack_orchestration_item()` with metadata.create_next_execution (increments current_execution_id)
985///
986/// **Note:** Instances are NOT created in `enqueue_for_orchestrator()`. Instance creation happens
987/// when the runtime acknowledges the first turn via `ack_orchestration_item()` with metadata.
988///
989/// # Error Handling Philosophy
990///
991/// - **Transient errors** (busy, timeout): Return error, let runtime retry
992/// - **Invalid tokens**: Return Ok (idempotent - already processed)
993/// - **Corruption** (missing instance, invalid data): Return error
994/// - **Concurrency conflicts**: Handle via locks, return error if deadlock
995///
996/// # Required vs Optional Methods
997///
998/// **REQUIRED** (must implement):
999/// - fetch_orchestration_item, ack_orchestration_item, abandon_orchestration_item
1000/// - read, append_with_execution
1001/// - enqueue_for_worker, fetch_work_item, ack_work_item
1002/// - enqueue_for_orchestrator
1003///
1004/// **OPTIONAL** (has defaults):
1005/// - latest_execution_id, read_with_execution
1006/// - list_instances, list_executions
1007///
1008/// # Testing Your Provider
1009///
1010/// See `tests/sqlite_provider_validations.rs` for comprehensive provider validation tests:
1011/// - Basic enqueue/dequeue operations
1012/// - Transactional atomicity
1013/// - Instance locking correctness (including multi-threaded tests)
1014/// - Lock expiration and redelivery
1015/// - Multi-execution support
1016/// - Execution status and output persistence
1017///
1018/// All tests use the Provider trait directly (not runtime), so they're portable to new providers.
1019/// See `docs/provider-implementation-guide.md` for detailed implementation guidance including instance locks.
1020///
1021/// Core provider trait for runtime orchestration operations.
1022///
1023/// This trait defines the essential methods required for durable orchestration execution.
1024/// It focuses on runtime-critical operations: fetching work items, processing history,
1025/// and managing queues. Management and observability features are provided through
1026/// optional capability traits.
1027///
1028/// # Capability Discovery
1029///
1030/// Providers can implement additional capability traits (like `ProviderAdmin`)
1031/// to expose richer features. The `Client` automatically discovers these capabilities
1032/// through the `as_management_capability()` method.
1033///
1034/// # Implementation Guide for LLMs
1035///
1036/// When implementing a new provider, focus on these core methods:
1037///
1038/// ## Required Methods (9 total)
1039///
1040/// 1. **Orchestration Processing (3 methods)**
1041/// - `fetch_orchestration_item()` - Atomic batch processing
1042/// - `ack_orchestration_item()` - Commit processing results
1043/// - `abandon_orchestration_item()` - Release locks and retry
1044///
1045/// 2. **History Access (1 method)**
1046/// - `read()` - Get event history for status checks
1047///
1048/// 3. **Worker Queue (2 methods)**
1049/// - `fetch_work_item()` - Get activity work items
1050/// - `ack_work_item()` - Acknowledge activity completion
1051///
1052/// 4. **Orchestrator Queue (1 method)**
1053/// - `enqueue_for_orchestrator()` - Enqueue control messages (including TimerFired with delayed visibility)
1054///
1055/// ## Optional Management Methods (2 methods)
1056///
1057/// These are included for backward compatibility but will be moved to
1058/// `ProviderAdmin` in future versions:
1059///
1060/// - `list_instances()` - List all instance IDs
1061/// - `list_executions()` - List execution IDs for an instance
1062///
1063/// # Capability Detection
1064///
1065/// Implement `as_management_capability()` to expose management features:
1066///
1067/// ```ignore
1068/// impl Provider for MyProvider {
1069/// // ... implement required methods
1070///
1071/// fn as_management_capability(&self) -> Option<&dyn ProviderAdmin> {
1072/// Some(self as &dyn ProviderAdmin)
1073/// }
1074/// }
1075///
1076/// impl ProviderAdmin for MyProvider {
1077/// // ... implement management methods
1078/// }
1079/// ```
1080///
1081/// # Testing Your Provider
1082///
1083/// See `tests/sqlite_provider_validations.rs` for comprehensive provider validation tests:
1084/// - Basic enqueue/dequeue operations
1085/// - Transactional atomicity
1086/// - Instance locking correctness (including multi-threaded tests)
1087/// - Lock expiration and redelivery
1088/// - Multi-execution support
1089/// - Execution status and output persistence
1090///
1091/// All tests use the Provider trait directly (not runtime), so they're portable to new providers.
1092/// See `docs/provider-implementation-guide.md` for detailed implementation guidance including instance locks.
1093///
1094/// # Cancellation
1095///
1096/// Fetch futures can be dropped during shutdown; other in-flight calls can be dropped
1097/// when the shutdown grace period expires. Release resources on drop, let unacknowledged
1098/// locks expire, and preserve atomic acknowledgement even if cancellation races with a commit.
1099///
1100#[async_trait::async_trait]
1101#[allow(clippy::too_many_arguments)]
1102pub trait Provider: Any + Send + Sync {
1103 // ===== Provider Identity =====
1104
1105 /// Returns the name of this provider implementation.
1106 ///
1107 /// Used for logging and diagnostics. Override to provide a meaningful name.
1108 ///
1109 /// Default: "unknown"
1110 fn name(&self) -> &str {
1111 "unknown"
1112 }
1113
1114 /// Returns the version of this provider implementation.
1115 ///
1116 /// Used for logging and diagnostics. Override to provide the version.
1117 ///
1118 /// Default: "0.0.0"
1119 fn version(&self) -> &str {
1120 "0.0.0"
1121 }
1122
1123 // ===== Core Atomic Orchestration Methods (REQUIRED) =====
1124 // These three methods form the heart of reliable orchestration execution.
1125 //
1126 // ⚠️ CRITICAL ID GENERATION CONTRACT:
1127 //
1128 // The provider MUST NOT generate execution_id or event_id values.
1129 // All IDs are generated by the runtime and passed to the provider:
1130 // - execution_id: Passed to ack_orchestration_item()
1131 // - event_id: Set in each Event in the history_delta
1132 //
1133 // The provider's role is to STORE these IDs, not generate them.
1134
1135 /// Fetch the next orchestration work item atomically.
1136 ///
1137 /// # What This Does
1138 ///
1139 /// 1. **Select an instance to process**: Find the next available message in orchestrator queue that is not locked
1140 /// 2. **Acquire instance-level lock**: Atomically claim the instance lock to prevent concurrent processing
1141 /// 3. **Lock ALL messages** for that instance (batch processing)
1142 /// 4. **Load history**: Get complete event history for the current execution
1143 /// 5. **Load metadata**: Get orchestration_name, version, execution_id
1144 /// 6. **Return locked batch**: Provider must prevent other processes from touching this instance
1145 ///
1146 /// # ⚠️ CRITICAL: Instance-Level Locking
1147 ///
1148 /// **You MUST acquire an instance-level lock BEFORE fetching messages.** This prevents concurrent
1149 /// dispatchers from processing the same instance simultaneously, which would cause race conditions
1150 /// and data corruption.
1151 ///
1152 /// **Required Implementation Pattern:**
1153 /// 1. Find available instance (check no other lock is held for it)
1154 /// 2. Atomically acquire instance lock (with conflict handling)
1155 /// 3. Verify lock acquisition succeeded
1156 /// 4. Only then proceed to lock and fetch messages
1157 ///
1158 /// See `docs/provider-implementation-guide.md` for detailed implementation guidance.
1159 ///
1160 /// # Peek-Lock Semantics
1161 ///
1162 /// - Messages remain in queue until `ack_orchestration_item()` is called
1163 /// - Generate a unique `lock_token` (e.g., UUID)
1164 /// - Set `locked_until` timestamp (e.g., now + 30 seconds)
1165 /// - If lock expires before ack, messages become available again (automatic retry)
1166 ///
1167 /// # Visibility Filtering
1168 ///
1169 /// Only return messages where `visible_at <= now()`:
1170 /// - Normal messages: visible immediately
1171 /// - Delayed messages: `visible_at = now + delay` (used for timer backpressure)
1172 ///
1173 /// # Instance Batching
1174 ///
1175 /// **CRITICAL:** All messages in the batch must belong to the SAME instance.
1176 ///
1177 /// Implementation steps:
1178 /// 1. Find the first visible, unlocked instance in the orchestrator queue
1179 /// 2. Atomically acquire the instance lock (prevent concurrent processing)
1180 /// 3. Lock ALL queued messages for that instance
1181 /// 4. Fetch all locked messages for processing
1182 ///
1183 /// # History Loading
1184 ///
1185 /// - For new instances (no history yet): return empty Vec
1186 /// - For existing instances: return ALL events for current execution_id, ordered by event_id
1187 /// - For multi-execution instances: return ONLY the LATEST execution's history
1188 ///
1189 /// SQLite example:
1190 /// ```text
1191 /// // Get current execution ID
1192 /// let exec_id = SELECT current_execution_id FROM instances WHERE instance_id = ?
1193 ///
1194 /// // Load history for that execution
1195 /// SELECT event_data FROM history
1196 /// WHERE instance_id = ? AND execution_id = ?
1197 /// ORDER BY event_id
1198 /// ```
1199 ///
1200 /// # Return Value
1201 ///
1202 /// - `Some(OrchestrationItem)` - Work is available and locked
1203 /// - `None` - No work available (dispatcher will sleep and retry)
1204 ///
1205 /// # Error Handling
1206 ///
1207 /// Don't panic on transient errors - return None and let dispatcher retry.
1208 /// Only panic on unrecoverable errors (e.g., corrupted data or schema).
1209 ///
1210 /// # Concurrency
1211 ///
1212 /// This method is called continuously by the orchestration dispatcher.
1213 /// Must be thread-safe and handle concurrent calls gracefully.
1214 ///
1215 /// # Example Implementation Pattern
1216 ///
1217 /// ```ignore
1218 /// async fn fetch_orchestration_item(&self) -> Option<OrchestrationItem> {
1219 /// let tx = begin_transaction()?;
1220 ///
1221 /// // Step 1: Find next available instance (check instance_locks)
1222 /// let instance_id = SELECT q.instance_id FROM orch_queue q
1223 /// LEFT JOIN instance_locks il ON q.instance_id = il.instance_id
1224 /// WHERE q.visible_at <= now()
1225 /// AND (il.instance_id IS NULL OR il.locked_until <= now())
1226 /// ORDER BY q.id LIMIT 1;
1227 ///
1228 /// if instance_id.is_none() { return None; }
1229 ///
1230 /// // Step 2: Atomically acquire instance lock
1231 /// let lock_token = generate_uuid();
1232 /// let lock_result = INSERT INTO instance_locks (instance_id, lock_token, locked_until, locked_at)
1233 /// VALUES (?, ?, ?, ?)
1234 /// ON CONFLICT(instance_id) DO UPDATE
1235 /// SET lock_token = excluded.lock_token, locked_until = excluded.locked_until
1236 /// WHERE locked_until <= excluded.locked_at;
1237 ///
1238 /// if lock_result.rows_affected == 0 {
1239 /// // Lock acquisition failed - another dispatcher has the lock
1240 /// return None;
1241 /// }
1242 ///
1243 /// // Step 3: Lock all messages for this instance
1244 /// UPDATE orch_queue SET lock_token = ?, locked_until = now() + 30s
1245 /// WHERE instance_id = ? AND visible_at <= now();
1246 ///
1247 /// // Step 4: Fetch locked messages
1248 /// let messages = SELECT work_item FROM orch_queue WHERE lock_token = ?;
1249 ///
1250 /// // Step 5: Load instance metadata
1251 /// let (name, version, exec_id) = SELECT ... FROM instances WHERE instance_id = ?;
1252 ///
1253 /// // Step 6: Load history for current execution
1254 /// let history = SELECT event_data FROM history
1255 /// WHERE instance_id = ? AND execution_id = ?
1256 /// ORDER BY event_id;
1257 ///
1258 /// commit_transaction();
1259 /// Some((OrchestrationItem { instance, orchestration_name, execution_id, version, history, messages }, lock_token, attempt_count))
1260 /// }
1261 /// ```
1262 ///
1263 /// # Return Value
1264 ///
1265 /// * `Ok(Some((item, lock_token, attempt_count)))` - Orchestration item with lock token and attempt count
1266 /// - `item`: The orchestration item to process
1267 /// - `lock_token`: Unique token for ack/abandon operations
1268 /// - `attempt_count`: Number of times this item has been fetched (for poison detection)
1269 /// * `Ok(None)` - No work available
1270 /// * `Err(ProviderError)` - Storage error
1271 async fn fetch_orchestration_item(
1272 &self,
1273 lock_timeout: Duration,
1274 poll_timeout: Duration,
1275 filter: Option<&DispatcherCapabilityFilter>,
1276 ) -> Result<Option<(OrchestrationItem, String, u32)>, ProviderError>;
1277
1278 /// Acknowledge successful orchestration processing atomically.
1279 ///
1280 /// This is the most critical method - it commits all changes from an orchestration turn atomically.
1281 ///
1282 /// # What This Does (ALL must be atomic)
1283 ///
1284 /// 1. **Validate lock token**: Verify instance lock is still valid and matches lock_token
1285 /// 2. **Remove instance lock**: Release the instance lock (processing complete)
1286 /// 3. **Append new events** to history for current execution
1287 /// 4. **Update execution metadata** (status, output) using pre-computed metadata
1288 /// 5. **Create new execution** if metadata.create_next_execution=true (ContinueAsNew)
1289 /// 6. **Enqueue worker_items** to worker queue (activity executions)
1290 /// 7. **Enqueue orchestrator_items** to orchestrator queue (completions, new instances, TimerFired with delayed visibility)
1291 /// 8. **Remove acknowledged messages** from orchestrator queue (release lock)
1292 ///
1293 /// # Atomicity Requirements
1294 ///
1295 /// **CRITICAL:** All 8 operations above must succeed or fail together.
1296 ///
1297 /// - **If ANY operation fails**: Roll back all changes
1298 /// - **If all operations succeed**: All changes are durable and visible
1299 ///
1300 /// This prevents:
1301 /// - Duplicate activity execution (history saved but worker item lost)
1302 /// - Lost completions (worker item enqueued but history not saved)
1303 /// - Orphaned locks (messages deleted but history append failed)
1304 /// - Stale instance locks (instance lock not removed)
1305 ///
1306 /// # Parameters
1307 ///
1308 /// * `lock_token` - Token from fetch_orchestration_item() - identifies locked messages
1309 /// * `history_delta` - New events to append (runtime assigns event_ids, provider stores as-is)
1310 /// * `worker_items` - Activity executions to enqueue (WorkItem::ActivityExecute)
1311 /// * `orchestrator_items` - Orchestrator messages to enqueue (completions, starts, TimerFired with delayed visibility)
1312 /// * `metadata` - Pre-computed execution state (DO NOT inspect events yourself!)
1313 ///
1314 /// # Event Storage (history_delta)
1315 ///
1316 /// **Important:** Store events exactly as provided. DO NOT:
1317 /// - Modify event_id (runtime assigns these)
1318 /// - Inspect event contents to make decisions
1319 /// - Filter or reorder events
1320 ///
1321 /// SQLite example:
1322 /// ```text
1323 /// for event in &history_delta {
1324 /// let event_json = serde_json::to_string(&event)?;
1325 /// let event_type = extract_discriminant_name(&event); // For indexing only
1326 /// INSERT INTO history (instance_id, execution_id, event_id, event_data, event_type)
1327 /// VALUES (?, ?, event.event_id(), event_json, event_type)
1328 /// }
1329 /// ```
1330 ///
1331 /// # Metadata Storage (ExecutionMetadata)
1332 ///
1333 /// **The runtime has ALREADY inspected events and computed metadata.**
1334 /// Provider just stores it:
1335 ///
1336 /// ```text
1337 /// // Update execution status/output from metadata
1338 /// if let Some(status) = &metadata.status {
1339 /// UPDATE executions
1340 /// SET status = ?, output = ?, completed_at = CURRENT_TIMESTAMP
1341 /// WHERE instance_id = ? AND execution_id = ?
1342 /// }
1343 ///
1344 /// // Create next execution if requested
1345 /// if metadata.create_next_execution {
1346 /// if let Some(next_id) = metadata.next_execution_id {
1347 /// INSERT INTO executions (instance_id, execution_id, status)
1348 /// VALUES (?, next_id, 'Running')
1349 ///
1350 /// UPDATE instances SET current_execution_id = next_id
1351 /// }
1352 /// }
1353 /// ```
1354 ///
1355 /// # Queue Item Enqueuing
1356 ///
1357 /// Worker items and orchestrator items must be enqueued within the same transaction:
1358 ///
1359 /// ```text
1360 /// // Worker queue (no special handling)
1361 /// for item in worker_items {
1362 /// INSERT INTO worker_queue (work_item) VALUES (serde_json::to_string(&item))
1363 /// }
1364 ///
1365 /// // Orchestrator queue (may have delayed visibility for TimerFired)
1366 /// for item in orchestrator_items {
1367 /// let visible_at = match &item {
1368 /// WorkItem::TimerFired { fire_at_ms, .. } => *fire_at_ms, // Delayed visibility
1369 /// _ => now() // Immediate visibility
1370 /// };
1371 ///
1372 /// INSERT INTO orchestrator_queue (instance_id, work_item, visible_at)
1373 /// VALUES (extract_instance(&item), serde_json::to_string(&item), visible_at)
1374 /// }
1375 /// ```
1376 ///
1377 /// # Lock Release
1378 ///
1379 /// Remove all acknowledged messages associated with this lock_token.
1380 ///
1381 /// # Error Handling
1382 ///
1383 /// - Return `Ok(())` if all operations succeeded
1384 /// - Return `Err(msg)` if any operation failed (all changes rolled back)
1385 /// - On error, runtime will call `abandon_orchestration_item()` to release lock
1386 ///
1387 /// # Special Cases
1388 ///
1389 /// **StartOrchestration in orchestrator_items:**
1390 /// - May need to create instance metadata record/entry (idempotent create-if-not-exists)
1391 /// - Should create execution record/entry with ID=1 if new instance
1392 ///
1393 /// **Empty history_delta:**
1394 /// - Valid case (e.g., terminal instance being acked with no changes)
1395 /// - Still process queues and release lock
1396 ///
1397 /// # Parameters
1398 ///
1399 /// * `lock_token` - Token from `fetch_orchestration_item` identifying the batch
1400 /// * `execution_id` - **The execution ID this history belongs to** (runtime decides this)
1401 /// * `history_delta` - Events to append to the specified execution
1402 /// * `worker_items` - Activity work items to enqueue
1403 /// * `orchestrator_items` - Orchestration work items to enqueue (StartOrchestration, ContinueAsNew, TimerFired, etc.)
1404 /// - TimerFired items should be enqueued with `visible_at = fire_at_ms` for delayed visibility
1405 /// * `metadata` - Pre-computed execution metadata (status, output)
1406 ///
1407 /// # TimerFired Items
1408 ///
1409 /// When `TimerFired` items are present in `orchestrator_items`, they should be enqueued with
1410 /// delayed visibility using the `fire_at_ms` field for the `visible_at` timestamp.
1411 /// This allows timers to fire at the correct logical time.
1412 ///
1413 /// # execution_id Parameter
1414 ///
1415 /// The `execution_id` parameter tells the provider **which execution this history belongs to**.
1416 /// The runtime is responsible for:
1417 /// - Deciding when to create new executions (e.g., for ContinueAsNew)
1418 /// - Managing execution ID sequencing
1419 /// - Ensuring each execution has its own isolated history
1420 ///
1421 /// The provider should:
1422 /// - **Create the execution record if it doesn't exist** (idempotent create-if-not-exists)
1423 /// - Append `history_delta` to the specified `execution_id`
1424 /// - Update the instance's current execution pointer if this execution_id is newer
1425 /// - NOT inspect WorkItems to decide execution IDs
1426 ///
1427 /// # SQLite Implementation Pattern
1428 ///
1429 /// ```text
1430 /// async fn ack_orchestration_item(execution_id, ...) -> Result<(), String> {
1431 /// let tx = begin_transaction()?;
1432 ///
1433 /// // Step 1: Validate lock token and get instance_id from instance_locks
1434 /// let instance_id = SELECT instance_id FROM instance_locks
1435 /// WHERE lock_token = ? AND locked_until > now();
1436 /// if instance_id.is_none() {
1437 /// return Err("Invalid or expired lock token");
1438 /// }
1439 ///
1440 /// // Step 2: Remove instance lock (processing complete)
1441 /// DELETE FROM instance_locks WHERE instance_id = ? AND lock_token = ?;
1442 ///
1443 /// // Step 3: Create execution record if it doesn't exist (idempotent)
1444 /// INSERT OR IGNORE INTO executions (instance_id, execution_id, status)
1445 /// VALUES (?, ?, 'Running');
1446 ///
1447 /// // Step 4: Append history to the SPECIFIED execution_id
1448 /// for event in history_delta {
1449 /// INSERT INTO history (...) VALUES (instance_id, execution_id, event.event_id(), ...)
1450 /// }
1451 ///
1452 /// // Step 5: Update execution metadata (no event inspection!)
1453 /// if let Some(status) = &metadata.status {
1454 /// UPDATE executions SET status = ?, output = ?, completed_at = NOW()
1455 /// WHERE instance_id = ? AND execution_id = ?
1456 /// }
1457 ///
1458 /// // Step 6: Update current_execution_id if this is a newer execution
1459 /// UPDATE instances SET current_execution_id = GREATEST(current_execution_id, ?)
1460 /// WHERE instance_id = ?;
1461 ///
1462 /// // Step 7: Enqueue worker/orchestrator items
1463 /// // Worker items go to worker queue (populate identity columns)
1464 /// for item in worker_items {
1465 /// if let WorkItem::ActivityExecute { instance, execution_id, id, .. } = &item {
1466 /// INSERT INTO worker_queue (work_item, instance_id, execution_id, activity_id)
1467 /// VALUES (serialize(item), instance, execution_id, id)
1468 /// }
1469 /// }
1470 ///
1471 /// // Orchestrator items go to orchestrator queue (TimerFired uses fire_at_ms for visible_at)
1472 /// for item in orchestrator_items {
1473 /// let visible_at = match &item {
1474 /// WorkItem::TimerFired { fire_at_ms, .. } => *fire_at_ms,
1475 /// _ => now()
1476 /// };
1477 /// INSERT INTO orchestrator_queue (instance_id, work_item, visible_at)
1478 /// VALUES (extract_instance(&item), serialize(item), visible_at)
1479 /// }
1480 ///
1481 /// // Step 8: Delete cancelled activities from worker queue
1482 /// DELETE FROM worker_queue
1483 /// WHERE (instance_id = ? AND execution_id = ? AND activity_id = ?)
1484 /// OR (instance_id = ? AND execution_id = ? AND activity_id = ?)
1485 /// ...
1486 ///
1487 /// // Step 9: Release lock: DELETE FROM orch_queue WHERE lock_token = ?;
1488 ///
1489 /// commit_transaction()?;
1490 /// Ok(())
1491 /// }
1492 /// ```
1493 async fn ack_orchestration_item(
1494 &self,
1495 _lock_token: &str,
1496 _execution_id: u64,
1497 _history_delta: Vec<Event>,
1498 _worker_items: Vec<WorkItem>,
1499 _orchestrator_items: Vec<WorkItem>,
1500 _metadata: ExecutionMetadata,
1501 _cancelled_activities: Vec<ScheduledActivityIdentifier>,
1502 ) -> Result<(), ProviderError>;
1503
1504 /// Abandon orchestration processing (used for errors/retries).
1505 ///
1506 /// Called when orchestration processing fails (e.g., storage contention, runtime crash).
1507 /// The messages must be made available for reprocessing.
1508 ///
1509 /// # What This Does
1510 ///
1511 /// 1. **Clear lock_token** from messages (make available again)
1512 /// 2. **Remove instance lock** (release instance-level lock)
1513 /// 3. **Optionally delay** retry by setting visibility timestamp
1514 /// 4. **Preserve message order** (don't reorder or modify messages)
1515 ///
1516 /// # Parameters
1517 ///
1518 /// * `lock_token` - Token from fetch_orchestration_item()
1519 /// * `delay` - Optional delay before messages become visible again
1520 /// - `None`: immediate retry (visible_at = now)
1521 /// - `Some(duration)`: delayed retry (visible_at = now + duration)
1522 ///
1523 /// # Implementation Pattern
1524 ///
1525 /// ```ignore
1526 /// async fn abandon_orchestration_item(lock_token: &str, delay: Option<Duration>) -> Result<(), String> {
1527 /// let visible_at = if let Some(delay) = delay {
1528 /// now() + delay
1529 /// } else {
1530 /// now()
1531 /// };
1532 ///
1533 /// BEGIN TRANSACTION
1534 /// // Get instance_id from instance_locks
1535 /// let instance_id = SELECT instance_id FROM instance_locks WHERE lock_token = ?;
1536 ///
1537 /// IF instance_id IS NULL:
1538 /// // Invalid lock token - return error
1539 /// ROLLBACK
1540 /// RETURN Err("Invalid lock token")
1541 ///
1542 /// // Clear lock_token from messages
1543 /// UPDATE orchestrator_queue
1544 /// SET lock_token = NULL, locked_until = NULL, visible_at = ?
1545 /// WHERE lock_token = ?;
1546 ///
1547 /// // Remove instance lock
1548 /// DELETE FROM instance_locks WHERE lock_token = ?;
1549 /// COMMIT
1550 ///
1551 /// Ok(())
1552 /// }
1553 /// ```
1554 ///
1555 /// # Use Cases
1556 ///
1557 /// - Storage contention → delay = Some(Duration::from_millis(50)) for backoff
1558 /// - Orchestration turn failed → delay = None for immediate retry
1559 /// - Runtime shutdown during processing → messages auto-recover when lock expires
1560 ///
1561 /// # Error Handling
1562 ///
1563 /// **Invalid lock tokens MUST return an error.** Unlike `ack_orchestration_item()`, this method
1564 /// should not be idempotent for invalid tokens. Invalid tokens indicate a programming error or
1565 /// state corruption and should be surfaced as errors.
1566 ///
1567 /// Return `Err("Invalid lock token")` if the lock token is not recognized or has expired.
1568 ///
1569 /// # Parameters
1570 ///
1571 /// * `lock_token` - The lock token from `fetch_orchestration_item`
1572 /// * `delay` - Optional delay before message becomes visible again
1573 /// * `ignore_attempt` - If true, decrement the attempt count (never below 0)
1574 async fn abandon_orchestration_item(
1575 &self,
1576 _lock_token: &str,
1577 _delay: Option<Duration>,
1578 _ignore_attempt: bool,
1579 ) -> Result<(), ProviderError>;
1580
1581 /// Read the full history for the latest execution of an instance.
1582 ///
1583 /// # What This Does
1584 ///
1585 /// Returns ALL events for the LATEST execution of an instance, ordered by event_id.
1586 ///
1587 /// # Multi-Execution Behavior
1588 ///
1589 /// For instances with multiple executions (from ContinueAsNew):
1590 /// - Find the latest execution_id (MAX(execution_id))
1591 /// - Return events for ONLY that execution
1592 /// - DO NOT return events from earlier executions
1593 ///
1594 /// # Return Value
1595 ///
1596 /// - `Ok(Vec<Event>)` - Events ordered by event_id ascending (event_id 1, 2, 3, ...)
1597 /// - `Err(ProviderError)` - Storage error
1598 ///
1599 /// # Usage
1600 ///
1601 /// **NOT called in runtime hot path.** Used by:
1602 /// - Client.get_orchestration_status() - to determine current state
1603 /// - Runtime.get_orchestration_descriptor() - to get orchestration metadata
1604 /// - Testing and validation suites
1605 ///
1606 /// The runtime hot path uses `fetch_orchestration_item()` which loads history internally.
1607 async fn read(&self, instance: &str) -> Result<Vec<Event>, ProviderError>;
1608
1609 // ===== History Reading and Testing Methods (NOT used by runtime hot path) =====
1610 // These methods are NOT called by the main runtime during orchestration execution.
1611 // They are used by testing, validation suites, client APIs, and debugging tools.
1612
1613 /// Read the full event history for a specific execution within an instance.
1614 ///
1615 /// **NOT called in runtime hot path.** Used by testing, validation suites, and debugging tools.
1616 ///
1617 /// # Parameters
1618 ///
1619 /// * `instance` - The ID of the orchestration instance.
1620 /// * `execution_id` - The specific execution ID to read history for.
1621 ///
1622 /// # Returns
1623 ///
1624 /// Vector of events in chronological order (oldest first).
1625 ///
1626 /// # Implementation Example
1627 ///
1628 /// ```ignore
1629 /// async fn read_with_execution(&self, instance: &str, execution_id: u64) -> Result<Vec<Event>, ProviderError> {
1630 /// SELECT event_data FROM history
1631 /// WHERE instance_id = ? AND execution_id = ?
1632 /// ORDER BY event_id
1633 /// }
1634 /// ```
1635 async fn read_with_execution(&self, _instance: &str, _execution_id: u64) -> Result<Vec<Event>, ProviderError>;
1636
1637 /// Append events to a specific execution.
1638 ///
1639 /// **NOT called in runtime hot path.** Used by testing and validation suites for fault injection
1640 /// and test setup. The runtime uses `ack_orchestration_item()` to append history atomically.
1641 ///
1642 /// # What This Does
1643 ///
1644 /// Add new events to the history log for a specific execution.
1645 ///
1646 /// # CRITICAL: Event ID Assignment
1647 ///
1648 /// **The runtime assigns event_ids BEFORE calling this method.**
1649 /// - DO NOT modify event.event_id()
1650 /// - DO NOT renumber events
1651 /// - Store events exactly as provided
1652 ///
1653 /// # Duplicate Detection
1654 ///
1655 /// Events with duplicate (instance_id, execution_id, event_id) should:
1656 /// - Either: Reject with error (let runtime handle)
1657 /// - Or: IGNORE (idempotent append)
1658 /// - NEVER: Overwrite existing event (corrupts history)
1659 ///
1660 /// # Parameters
1661 ///
1662 /// * `instance` - The ID of the orchestration instance.
1663 /// * `execution_id` - The specific execution ID to append events to.
1664 /// * `new_events` - Vector of events to append (event_ids already assigned).
1665 ///
1666 /// # Returns
1667 ///
1668 /// `Ok(())` on success, `Err(ProviderError)` on failure.
1669 ///
1670 /// # Implementation Example
1671 ///
1672 /// ```ignore
1673 /// async fn append_with_execution(
1674 /// &self,
1675 /// instance: &str,
1676 /// execution_id: u64,
1677 /// new_events: Vec<Event>,
1678 /// ) -> Result<(), ProviderError> {
1679 /// for event in &new_events {
1680 /// INSERT INTO history (instance_id, execution_id, event_id, event_data)
1681 /// VALUES (?, ?, event.event_id(), serialize(event))
1682 /// }
1683 /// Ok(())
1684 /// }
1685 /// ```
1686 async fn append_with_execution(
1687 &self,
1688 _instance: &str,
1689 _execution_id: u64,
1690 _new_events: Vec<Event>,
1691 ) -> Result<(), ProviderError>;
1692
1693 // ===== Worker Queue Operations (REQUIRED) =====
1694 // Worker queue processes activity executions.
1695
1696 /// Enqueue an activity execution request.
1697 ///
1698 /// # What This Does
1699 ///
1700 /// Add a WorkItem::ActivityExecute to the worker queue for background processing.
1701 ///
1702 /// # Implementation
1703 ///
1704 /// ```ignore
1705 /// async fn enqueue_for_worker(&self, item: WorkItem) -> Result<(), String> {
1706 /// INSERT INTO worker_queue (work_item)
1707 /// VALUES (serde_json::to_string(&item)?)
1708 /// }
1709 /// ```
1710 ///
1711 /// # Locking
1712 ///
1713 /// New messages should have lock_token = NULL (available for dequeue).
1714 ///
1715 /// # Ordering
1716 ///
1717 /// FIFO order preferred but not strictly required.
1718 async fn enqueue_for_worker(&self, _item: WorkItem) -> Result<(), ProviderError>;
1719
1720 /// Dequeue a single work item with peek-lock semantics.
1721 ///
1722 /// # What This Does
1723 ///
1724 /// Fetch next available worker task.
1725 ///
1726 /// 1. Find an available task in the worker queue (visible_at <= now, not locked)
1727 /// 2. Lock it with a unique token
1728 /// 3. Return item + token (item stays in queue until ack)
1729 ///
1730 /// # Parameters
1731 ///
1732 /// - `lock_timeout`: Duration to lock the item for processing.
1733 /// - `poll_timeout`: Maximum time to wait for work. Provider MAY wait up to this
1734 /// duration if it supports long polling, or return immediately if it doesn't.
1735 /// - `session`: Session routing configuration.
1736 /// - `Some(config)`: Session-aware fetch — returns non-session + owned + claimable session items.
1737 /// - `None`: Non-session only — returns only items with `session_id IS NULL`.
1738 ///
1739 /// # Return Value
1740 ///
1741 /// - `Ok(Some((WorkItem, String, u32)))` - Item is locked and ready to process
1742 /// - WorkItem: The work item to process
1743 /// - String: Lock token for ack/abandon
1744 /// - u32: Attempt count (number of times this item has been fetched)
1745 /// - `Ok(None)` - No work available
1746 /// - `Err(ProviderError)` - Storage error (allows runtime to distinguish empty queue from failures)
1747 ///
1748 /// # Concurrency
1749 ///
1750 /// Called continuously by work dispatcher. Must prevent double-dequeue.
1751 ///
1752 /// # Session Routing
1753 ///
1754 /// When `session` is `Some(config)`, the provider must filter eligible items:
1755 /// - Non-session items (`session_id IS NULL`): always eligible
1756 /// - Owned-session items: items whose `session_id` has an active session owned by `config.owner_id`
1757 /// - Claimable-session items: items whose `session_id` has no active session (unowned)
1758 ///
1759 /// When fetching a claimable session-bound item, atomically create or update the session
1760 /// record with `config.owner_id` and `config.lock_timeout`.
1761 ///
1762 /// When `session` is `None`, only non-session items (`session_id IS NULL`) are returned.
1763 /// Session-bound items are skipped entirely.
1764 ///
1765 /// # Tag Filtering
1766 ///
1767 /// The `tag_filter` parameter controls which activities this worker processes:
1768 /// - `DefaultOnly`: Only items with `tag IS NULL`
1769 /// - `Tags(set)`: Only items whose `tag` is in `set`
1770 /// - `DefaultAnd(set)`: Items with `tag IS NULL` OR `tag` in `set`
1771 /// - `None`: No items (orchestrator-only mode)
1772 async fn fetch_work_item(
1773 &self,
1774 lock_timeout: Duration,
1775 poll_timeout: Duration,
1776 session: Option<&SessionFetchConfig>,
1777 tag_filter: &TagFilter,
1778 ) -> Result<Option<(WorkItem, String, u32)>, ProviderError>;
1779
1780 /// Acknowledge successful processing of a work item.
1781 ///
1782 /// # What This Does
1783 ///
1784 /// Atomically acknowledge worker item and optionally enqueue completion to orchestrator queue.
1785 ///
1786 /// # Purpose
1787 ///
1788 /// Ensures completion delivery and worker ack happen atomically.
1789 /// Prevents lost completions if enqueue succeeds but ack fails.
1790 /// Prevents duplicate work if ack succeeds but enqueue fails.
1791 ///
1792 /// # Parameters
1793 ///
1794 /// * `token` - Lock token from fetch_work_item
1795 /// * `completion` - Optional completion work item:
1796 /// - `Some(WorkItem)`: Remove worker item AND enqueue completion to orchestrator queue
1797 /// - `None`: Remove worker item WITHOUT enqueueing anything (used when activity was cancelled)
1798 ///
1799 /// # Error on Missing Entry
1800 ///
1801 /// **CRITICAL**: This method MUST return a non-retryable error if the locked worker work-item
1802 /// entry does not exist anymore (was already removed) or cannot be acknowledged as expected.
1803 /// This can happen when:
1804 /// - The activity was cancelled (the backing entry was removed/invalidated by orchestration)
1805 /// - The lock was stolen by another worker (lock expired and work-item was re-fetched/completed)
1806 /// - The lock expired (worker took too long without renewing)
1807 ///
1808 /// The worker dispatcher uses this error to detect cancellation races and log appropriately.
1809 ///
1810 /// # Implementation
1811 ///
1812 /// ```ignore
1813 /// async fn ack_work_item(&self, token: &str, completion: Option<WorkItem>) -> Result<(), ProviderError> {
1814 /// BEGIN TRANSACTION
1815 /// // Relational/SQL example:
1816 /// let result = DELETE FROM worker_queue WHERE lock_token = ?token AND locked_until > now();
1817 ///
1818 /// // CRITICAL: Check if the locked entry existed and lock was still valid
1819 /// if result.rows_affected == 0 {
1820 /// ROLLBACK
1821 /// return Err(ProviderError::permanent("Work item not found - cancelled, stolen, or lock expired"))
1822 /// }
1823 ///
1824 /// if let Some(completion) = completion {
1825 /// INSERT INTO orchestrator_queue (instance_id, work_item, visible_at)
1826 /// VALUES (completion.instance, serialize(completion), now)
1827 /// }
1828 /// COMMIT
1829 /// }
1830 /// ```
1831 async fn ack_work_item(&self, token: &str, completion: Option<WorkItem>) -> Result<(), ProviderError>;
1832
1833 /// Renew the lock on a worker queue item.
1834 ///
1835 /// # What This Does
1836 ///
1837 /// Extends the lock timeout for an in-progress activity execution, preventing
1838 /// the lock from expiring while the activity is still being processed.
1839 ///
1840 /// # Purpose
1841 ///
1842 /// Enables long-running activities to complete without lock timeout:
1843 /// - Worker fetches activity with initial lock timeout (e.g., 30s)
1844 /// - Background renewal task periodically extends the lock
1845 /// - If worker crashes, renewal stops and lock expires naturally
1846 /// - Another worker can then pick up the abandoned activity
1847 ///
1848 /// # Activity Cancellation
1849 ///
1850 /// This method also serves as the cancellation detection mechanism. When the
1851 /// orchestration runtime decides an activity should be cancelled, it removes or invalidates
1852 /// the backing entry for that locked work-item (e.g., by
1853 /// removing the corresponding entry). The next renewal attempt will fail, signaling to the
1854 /// worker that the activity should be cancelled.
1855 ///
1856 /// # Parameters
1857 ///
1858 /// * `token` - Lock token from fetch_work_item
1859 /// * `extend_for` - [`Duration`] to extend lock from now (typically matches `RuntimeOptions::worker_lock_timeout`)
1860 ///
1861 /// # Returns
1862 ///
1863 /// * `Ok(())` - Lock renewed successfully
1864 /// * `Err(ProviderError)` - Lock renewal failed:
1865 /// - Permanent error: Token invalid, expired, entry removed (cancelled), or already acked
1866 /// - Retryable error: Storage connection issues
1867 ///
1868 /// # Implementation Pattern
1869 ///
1870 /// ```ignore
1871 /// async fn renew_work_item_lock(&self, token: &str, extend_for: Duration) -> Result<(), ProviderError> {
1872 /// // Relational/SQL example:
1873 /// let result = UPDATE worker_queue
1874 /// SET locked_until = now() + extend_for
1875 /// WHERE lock_token = ?token AND locked_until > now();
1876 ///
1877 /// if result.rows_affected == 0:
1878 /// // Entry doesn't exist (cancelled), lock expired, or invalid token
1879 /// return Err(ProviderError::permanent("Lock token invalid, expired, or entry removed"))
1880 ///
1881 /// Ok(())
1882 /// }
1883 /// ```
1884 ///
1885 /// # Concurrency
1886 ///
1887 /// - Safe to call while activity is executing
1888 /// - Idempotent: Multiple renewals with same token are safe
1889 /// - Renewal fails gracefully after ack_work_item (token deleted)
1890 ///
1891 /// # Usage in Runtime
1892 ///
1893 /// Called by worker dispatcher's activity manager task:
1894 /// - Renewal interval calculated based on lock timeout
1895 /// - On error: triggers cancellation token (entry may have been removed/invalidated for cancellation)
1896 /// - Stops automatically when activity completes or worker crashes
1897 async fn renew_work_item_lock(&self, token: &str, extend_for: Duration) -> Result<(), ProviderError>;
1898
1899 /// Heartbeat all non-idle sessions owned by the given workers.
1900 ///
1901 /// Extends `locked_until` for sessions where:
1902 /// - `owner_id` matches any of the provided `owner_ids`
1903 /// - `locked_until > now` (still owned)
1904 /// - `last_activity_at + idle_timeout > now` (not idle)
1905 ///
1906 /// Sessions that are idle (no recent activity flow) are NOT renewed,
1907 /// causing their locks to naturally expire.
1908 ///
1909 /// Accepts a slice of owner IDs so the provider can batch the operation
1910 /// into a single storage call.
1911 ///
1912 /// # Returns
1913 ///
1914 /// Total count of sessions renewed across all owners.
1915 async fn renew_session_lock(
1916 &self,
1917 owner_ids: &[&str],
1918 extend_for: Duration,
1919 idle_timeout: Duration,
1920 ) -> Result<usize, ProviderError>;
1921
1922 /// Sweep orphaned session entries.
1923 ///
1924 /// This is a convenience hook so providers don't need background threads for
1925 /// a common cleanup scenario. The runtime calls it periodically on behalf of
1926 /// all workers.
1927 ///
1928 /// Removes sessions where:
1929 /// - `locked_until < now` (lock expired)
1930 /// - No pending work items reference this session
1931 ///
1932 /// Any worker can sweep any worker's orphans.
1933 ///
1934 /// Providers that already clean up expired/ownerless sessions internally
1935 /// (e.g., via TTL, background sweeps, or eager eviction on fetch) may
1936 /// return `Ok(0)` here.
1937 ///
1938 /// # Returns
1939 ///
1940 /// Count of sessions removed (0 is valid if no orphans exist or the
1941 /// provider handles cleanup through other means).
1942 async fn cleanup_orphaned_sessions(&self, idle_timeout: Duration) -> Result<usize, ProviderError>;
1943
1944 /// Abandon work item processing (release lock without completing).
1945 ///
1946 /// Called when activity processing fails with a retryable error (e.g., storage contention)
1947 /// and the work item should be made available for another worker to pick up.
1948 ///
1949 /// # What This Does
1950 ///
1951 /// 1. **Clear lock** - Remove lock_token and reset locked_until
1952 /// 2. **Optionally delay** - Set visible_at to defer retry
1953 /// 3. **Preserve message** - Don't delete or modify the work item content
1954 ///
1955 /// # Parameters
1956 ///
1957 /// * `token` - Lock token from fetch_work_item
1958 /// * `delay` - Optional delay before message becomes visible again
1959 /// - `None`: Immediate visibility (visible_at = now)
1960 /// - `Some(duration)`: Delayed visibility (visible_at = now + duration)
1961 ///
1962 /// # Returns
1963 ///
1964 /// * `Ok(())` - Work item abandoned, lock released
1965 /// * `Err(ProviderError)` - Abandon failed (invalid token, already acked, etc.)
1966 ///
1967 /// # Implementation Pattern
1968 ///
1969 /// ```ignore
1970 /// async fn abandon_work_item(&self, token: &str, delay: Option<Duration>) -> Result<(), ProviderError> {
1971 /// let visible_at = delay.map(|d| now() + d).unwrap_or(now());
1972 ///
1973 /// let result = UPDATE worker_queue
1974 /// SET lock_token = NULL, locked_until = NULL, visible_at = ?visible_at
1975 /// WHERE lock_token = ?token;
1976 ///
1977 /// if result.rows_affected == 0:
1978 /// return Err(ProviderError::permanent("Invalid lock token"))
1979 ///
1980 /// Ok(())
1981 /// }
1982 /// ```
1983 ///
1984 /// # Parameters
1985 ///
1986 /// * `token` - Lock token from fetch_work_item
1987 /// * `delay` - Optional delay before work item becomes visible again
1988 /// * `ignore_attempt` - If true, decrement the attempt count (never below 0)
1989 async fn abandon_work_item(
1990 &self,
1991 token: &str,
1992 delay: Option<Duration>,
1993 ignore_attempt: bool,
1994 ) -> Result<(), ProviderError>;
1995
1996 /// Renew the lock on an orchestration item.
1997 ///
1998 /// Extends the instance lock timeout, preventing lock expiration during
1999 /// long-running orchestration turns.
2000 ///
2001 /// # Parameters
2002 ///
2003 /// * `token` - Lock token from fetch_orchestration_item
2004 /// * `extend_for` - Duration to extend lock from now
2005 ///
2006 /// # Returns
2007 ///
2008 /// * `Ok(())` - Lock renewed successfully
2009 /// * `Err(ProviderError)` - Lock renewal failed (invalid token, expired, etc.)
2010 ///
2011 /// # Implementation Pattern
2012 ///
2013 /// ```ignore
2014 /// async fn renew_orchestration_item_lock(&self, token: &str, extend_for: Duration) -> Result<(), ProviderError> {
2015 /// let locked_until = now() + extend_for;
2016 ///
2017 /// let result = UPDATE instance_locks
2018 /// SET locked_until = ?locked_until
2019 /// WHERE lock_token = ?token
2020 /// AND locked_until > now();
2021 ///
2022 /// if result.rows_affected == 0:
2023 /// return Err(ProviderError::permanent("Lock token invalid or expired"))
2024 ///
2025 /// Ok(())
2026 /// }
2027 /// ```
2028 async fn renew_orchestration_item_lock(&self, token: &str, extend_for: Duration) -> Result<(), ProviderError>;
2029
2030 // ===== Optional Management APIs =====
2031 // These have default implementations and are primarily used for testing/debugging.
2032
2033 /// Enqueue a work item to the orchestrator queue.
2034 ///
2035 /// # Purpose
2036 ///
2037 /// Used by runtime for:
2038 /// - External events: `Client.raise_event()` → enqueues WorkItem::ExternalRaised
2039 /// - Cancellation: `Client.cancel_instance()` → enqueues WorkItem::CancelInstance
2040 /// - Testing: Direct queue manipulation
2041 ///
2042 /// **Note:** In normal operation, orchestrator items are enqueued via `ack_orchestration_item`.
2043 ///
2044 /// # Parameters
2045 ///
2046 /// * `item` - WorkItem to enqueue (usually ExternalRaised or CancelInstance)
2047 /// * `delay` - Optional visibility delay
2048 /// - `None`: Immediate visibility (common case)
2049 /// - `Some(duration)`: Delay visibility by the specified duration
2050 /// - Providers without delayed_visibility support can ignore this (treat as None)
2051 ///
2052 /// # Implementation Pattern
2053 ///
2054 /// ```ignore
2055 /// async fn enqueue_for_orchestrator(&self, item: WorkItem, delay: Option<Duration>) -> Result<(), String> {
2056 /// let instance = extract_instance(&item); // See WorkItem docs for extraction
2057 /// let work_json = serde_json::to_string(&item)?;
2058 ///
2059 /// let visible_at = if let Some(delay) = delay {
2060 /// now() + delay
2061 /// } else {
2062 /// now()
2063 /// };
2064 ///
2065 /// // ⚠️ DO NOT create instance here - runtime will create it via ack_orchestration_item metadata
2066 /// // Instance creation happens when runtime acknowledges the first turn with ExecutionMetadata
2067 ///
2068 /// INSERT INTO orchestrator_queue (instance_id, work_item, visible_at, lock_token, locked_until)
2069 /// VALUES (instance, work_json, visible_at, NULL, NULL);
2070 ///
2071 /// Ok(())
2072 /// }
2073 /// ```
2074 ///
2075 /// # Special Cases
2076 ///
2077 /// **StartOrchestration:**
2078 /// - ⚠️ DO NOT create instance here - instance creation happens via `ack_orchestration_item()` metadata
2079 /// - Just enqueue the work item; runtime will create instance when acknowledging the first turn
2080 ///
2081 /// **ExternalRaised:**
2082 /// - Can arrive before instance is started (race condition)
2083 /// - Should still enqueue - runtime will handle gracefully
2084 ///
2085 /// # Error Handling
2086 ///
2087 /// Return Err if storage fails. Return Ok if item was enqueued successfully.
2088 async fn enqueue_for_orchestrator(&self, _item: WorkItem, _delay: Option<Duration>) -> Result<(), ProviderError>;
2089
2090 // - Add timeout parameter to fetch_work_item and dequeue_timer_peek_lock
2091 // - Add refresh_worker_lock(token, extend_ms) and refresh_timer_lock(token, extend_ms)
2092 // - Provider should auto-abandon messages if lock expires without ack
2093 // This would enable graceful handling of worker crashes and long-running activities
2094
2095 // ===== Capability Discovery =====
2096
2097 /// Check if this provider implements management capabilities.
2098 ///
2099 /// # Purpose
2100 ///
2101 /// This method enables automatic capability discovery by the `Client`.
2102 /// When a provider implements `ProviderAdmin`, it should return
2103 /// `Some(self as &dyn ProviderAdmin)` to expose management features.
2104 ///
2105 /// # Default Implementation
2106 ///
2107 /// Returns `None` (no management capabilities). Override to expose capabilities:
2108 ///
2109 /// ```ignore
2110 /// impl Provider for MyProvider {
2111 /// fn as_management_capability(&self) -> Option<&dyn ProviderAdmin> {
2112 /// Some(self as &dyn ProviderAdmin)
2113 /// }
2114 /// }
2115 /// ```
2116 ///
2117 /// # Usage
2118 ///
2119 /// The `Client` automatically discovers capabilities:
2120 ///
2121 /// ```ignore
2122 /// let client = Client::new(provider);
2123 /// if client.has_management_capability() {
2124 /// let instances = client.list_all_instances().await?;
2125 /// }
2126 /// ```
2127 fn as_management_capability(&self) -> Option<&dyn ProviderAdmin> {
2128 None
2129 }
2130
2131 /// Lightweight check for custom status changes.
2132 ///
2133 /// Returns `Some((custom_status, version))` if `custom_status_version > last_seen_version`,
2134 /// `None` if unchanged. Used by `Client::wait_for_status_change()` polling loop.
2135 ///
2136 /// # Parameters
2137 ///
2138 /// * `instance` - Instance ID to check
2139 /// * `last_seen_version` - The version the caller last observed
2140 ///
2141 /// # Returns
2142 ///
2143 /// * `Ok(Some((custom_status, version)))` - Status has changed
2144 /// * `Ok(None)` - No change since `last_seen_version`
2145 /// * `Err(ProviderError)` - Storage error
2146 async fn get_custom_status(
2147 &self,
2148 _instance: &str,
2149 _last_seen_version: u64,
2150 ) -> Result<Option<(Option<String>, u64)>, ProviderError>;
2151
2152 /// Read a single KV entry for the given instance.
2153 ///
2154 /// Returns `Ok(Some(value))` if the key exists, `Ok(None)` otherwise.
2155 /// Reads directly from the materialized `kv_store` table (not history events).
2156 async fn get_kv_value(&self, instance: &str, key: &str) -> Result<Option<String>, ProviderError>;
2157
2158 /// Read all KV entries for the given instance.
2159 ///
2160 /// Returns a map of key→value pairs. Returns an empty map if no keys exist.
2161 /// Reads directly from the materialized `kv_store` table (not history events).
2162 async fn get_kv_all_values(
2163 &self,
2164 instance: &str,
2165 ) -> Result<std::collections::HashMap<String, String>, ProviderError>;
2166
2167 /// Compute runtime introspection stats for an orchestration instance.
2168 ///
2169 /// Returns `Ok(None)` if the instance does not exist.
2170 /// Providers compute this directly from storage (history table, kv_store table)
2171 /// rather than the runtime injecting stats into the KV store on every turn.
2172 async fn get_instance_stats(&self, instance: &str) -> Result<Option<crate::SystemStats>, ProviderError>;
2173}
2174pub mod management;
2175
2176pub mod instrumented;
2177
2178/// SQLite-backed provider with full transactional support.
2179///
2180/// Enable with the `sqlite` feature:
2181/// ```toml
2182/// duroxide = { version = "0.1", features = ["sqlite"] }
2183/// ```
2184#[cfg(feature = "sqlite")]
2185pub mod sqlite;
2186
2187// Re-export management types for convenience
2188pub use management::{
2189 DeleteInstanceResult, ExecutionInfo, InstanceFilter, InstanceInfo, InstanceTree, ManagementProvider, PruneOptions,
2190 PruneResult, QueueDepths, SystemMetrics,
2191};
2192
2193/// Administrative capability trait for observability and management operations.
2194///
2195/// This trait provides rich management and observability features that extend
2196/// the core `Provider` functionality. Providers can implement this trait to
2197/// expose administrative capabilities to the `Client`.
2198///
2199/// # Automatic Discovery
2200///
2201/// The `Client` automatically discovers this capability through the
2202/// `Provider::as_management_capability()` method. When available, management
2203/// methods become accessible through the client.
2204///
2205/// # Implementation Guide for LLMs
2206///
2207/// When implementing a new provider, you can optionally implement this trait
2208/// to expose management features:
2209///
2210/// ```ignore
2211/// impl Provider for MyProvider {
2212/// // ... implement required Provider methods
2213///
2214/// fn as_management_capability(&self) -> Option<&dyn ProviderAdmin> {
2215/// Some(self as &dyn ProviderAdmin)
2216/// }
2217/// }
2218///
2219/// impl ProviderAdmin for MyProvider {
2220/// async fn list_instances(&self) -> Result<Vec<String>, String> {
2221/// // Query your storage for all instance IDs
2222/// Ok(vec!["instance-1".to_string(), "instance-2".to_string()])
2223/// }
2224///
2225/// async fn get_instance_info(&self, instance: &str) -> Result<InstanceInfo, String> {
2226/// // Query instance metadata from your storage
2227/// Ok(InstanceInfo {
2228/// instance_id: instance.to_string(),
2229/// orchestration_name: "ProcessOrder".to_string(),
2230/// orchestration_version: "1.0.0".to_string(),
2231/// current_execution_id: 1,
2232/// status: "Running".to_string(),
2233/// output: None,
2234/// created_at: 1234567890,
2235/// updated_at: 1234567890,
2236/// })
2237/// }
2238///
2239/// // ... implement other management methods
2240/// }
2241/// ```
2242///
2243/// # Required Methods (8 total)
2244///
2245/// 1. **Instance Discovery (2 methods)**
2246/// - `list_instances()` - List all instance IDs
2247/// - `list_instances_by_status()` - Filter instances by status
2248///
2249/// 2. **Execution Inspection (3 methods)**
2250/// - `list_executions()` - List execution IDs for an instance
2251/// - `read_execution()` - Read history for a specific execution
2252/// - `latest_execution_id()` - Get the latest execution ID
2253///
2254/// 3. **Metadata Access (2 methods)**
2255/// - `get_instance_info()` - Get comprehensive instance information
2256/// - `get_execution_info()` - Get detailed execution information
2257///
2258/// 4. **System Metrics (2 methods)**
2259/// - `get_system_metrics()` - Get system-wide metrics
2260/// - `get_queue_depths()` - Get current queue depths
2261///
2262/// # Usage
2263///
2264/// ```ignore
2265/// let client = Client::new(provider);
2266///
2267/// // Check if management features are available
2268/// if client.has_management_capability() {
2269/// let instances = client.list_all_instances().await?;
2270/// let metrics = client.get_system_metrics().await?;
2271/// println!("System has {} instances", metrics.total_instances);
2272/// } else {
2273/// println!("Management features not available");
2274/// }
2275/// ```
2276#[async_trait::async_trait]
2277pub trait ProviderAdmin: Any + Send + Sync {
2278 // ===== Instance Discovery =====
2279
2280 /// List all known instance IDs.
2281 ///
2282 /// # Returns
2283 ///
2284 /// Vector of instance IDs, typically sorted by creation time (newest first).
2285 ///
2286 /// # Use Cases
2287 ///
2288 /// - Admin dashboards showing all workflows
2289 /// - Bulk operations across instances
2290 /// - Testing (verify instance creation)
2291 ///
2292 /// # Implementation Example
2293 ///
2294 /// ```ignore
2295 /// async fn list_instances(&self) -> Result<Vec<String>, String> {
2296 /// SELECT instance_id FROM instances ORDER BY created_at DESC
2297 /// }
2298 /// ```
2299 async fn list_instances(&self) -> Result<Vec<String>, ProviderError>;
2300
2301 /// List instances matching a status filter.
2302 ///
2303 /// # Parameters
2304 ///
2305 /// * `status` - Filter by execution status: "Running", "Completed", "Failed", "ContinuedAsNew"
2306 ///
2307 /// # Returns
2308 ///
2309 /// Vector of instance IDs with the specified status.
2310 ///
2311 /// # Implementation Example
2312 ///
2313 /// ```ignore
2314 /// async fn list_instances_by_status(&self, status: &str) -> Result<Vec<String>, String> {
2315 /// SELECT i.instance_id FROM instances i
2316 /// JOIN executions e ON i.instance_id = e.instance_id AND i.current_execution_id = e.execution_id
2317 /// WHERE e.status = ?
2318 /// ORDER BY i.created_at DESC
2319 /// }
2320 /// ```
2321 async fn list_instances_by_status(&self, status: &str) -> Result<Vec<String>, ProviderError>;
2322
2323 // ===== Execution Inspection =====
2324
2325 /// List all execution IDs for an instance.
2326 ///
2327 /// # Returns
2328 ///
2329 /// Vector of execution IDs in ascending order: `[1]`, `[1, 2]`, `[1, 2, 3]`, etc.
2330 ///
2331 /// # Multi-Execution Context
2332 ///
2333 /// When an orchestration uses ContinueAsNew, multiple executions exist:
2334 /// - Execution 1: Original execution
2335 /// - Execution 2: First ContinueAsNew
2336 /// - Execution 3: Second ContinueAsNew
2337 /// - etc.
2338 ///
2339 /// # Implementation Example
2340 ///
2341 /// ```ignore
2342 /// async fn list_executions(&self, instance: &str) -> Result<Vec<u64>, String> {
2343 /// SELECT execution_id FROM executions
2344 /// WHERE instance_id = ?
2345 /// ORDER BY execution_id
2346 /// }
2347 /// ```
2348 async fn list_executions(&self, instance: &str) -> Result<Vec<u64>, ProviderError>;
2349
2350 /// Read the full event history for a specific execution within an instance.
2351 ///
2352 /// # Parameters
2353 ///
2354 /// * `instance` - The ID of the orchestration instance.
2355 /// * `execution_id` - The specific execution ID to read history for.
2356 ///
2357 /// # Returns
2358 ///
2359 /// Vector of events in chronological order (oldest first).
2360 ///
2361 /// # Implementation Example
2362 ///
2363 /// ```ignore
2364 /// async fn read_execution(&self, instance: &str, execution_id: u64) -> Result<Vec<Event>, String> {
2365 /// SELECT event_data FROM history
2366 /// WHERE instance_id = ? AND execution_id = ?
2367 /// ORDER BY event_id
2368 /// }
2369 /// ```
2370 async fn read_history_with_execution_id(
2371 &self,
2372 instance: &str,
2373 execution_id: u64,
2374 ) -> Result<Vec<Event>, ProviderError>;
2375
2376 /// Read the full event history for the latest execution of an instance.
2377 ///
2378 /// # Parameters
2379 ///
2380 /// * `instance` - The ID of the orchestration instance.
2381 ///
2382 /// # Returns
2383 ///
2384 /// Vector of events in chronological order (oldest first) for the latest execution.
2385 ///
2386 /// # Implementation
2387 ///
2388 /// This method gets the latest execution ID and delegates to `read_history_with_execution_id`.
2389 ///
2390 /// ```ignore
2391 /// async fn read_history(&self, instance: &str) -> Result<Vec<Event>, ProviderError> {
2392 /// let execution_id = self.latest_execution_id(instance).await?;
2393 /// self.read_history_with_execution_id(instance, execution_id).await
2394 /// }
2395 /// ```
2396 async fn read_history(&self, instance: &str) -> Result<Vec<Event>, ProviderError>;
2397
2398 /// Get the latest (current) execution ID for an instance.
2399 ///
2400 /// # Parameters
2401 ///
2402 /// * `instance` - The ID of the orchestration instance.
2403 ///
2404 /// # Implementation Pattern
2405 ///
2406 /// ```ignore
2407 /// async fn latest_execution_id(&self, instance: &str) -> Result<u64, String> {
2408 /// SELECT COALESCE(MAX(execution_id), 1) FROM executions WHERE instance_id = ?
2409 /// }
2410 /// ```
2411 async fn latest_execution_id(&self, instance: &str) -> Result<u64, ProviderError>;
2412
2413 // ===== Instance Metadata =====
2414
2415 /// Get comprehensive information about an instance.
2416 ///
2417 /// # Parameters
2418 ///
2419 /// * `instance` - The ID of the orchestration instance.
2420 ///
2421 /// # Returns
2422 ///
2423 /// Detailed instance information including status, output, and metadata.
2424 ///
2425 /// # Implementation Example
2426 ///
2427 /// ```ignore
2428 /// async fn get_instance_info(&self, instance: &str) -> Result<InstanceInfo, String> {
2429 /// SELECT i.*, e.status, e.output
2430 /// FROM instances i
2431 /// JOIN executions e ON i.instance_id = e.instance_id AND i.current_execution_id = e.execution_id
2432 /// WHERE i.instance_id = ?
2433 /// }
2434 /// ```
2435 async fn get_instance_info(&self, instance: &str) -> Result<InstanceInfo, ProviderError>;
2436
2437 /// Get detailed information about a specific execution.
2438 ///
2439 /// # Parameters
2440 ///
2441 /// * `instance` - The ID of the orchestration instance.
2442 /// * `execution_id` - The specific execution ID.
2443 ///
2444 /// # Returns
2445 ///
2446 /// Detailed execution information including status, output, and event count.
2447 ///
2448 /// # Implementation Example
2449 ///
2450 /// ```ignore
2451 /// async fn get_execution_info(&self, instance: &str, execution_id: u64) -> Result<ExecutionInfo, String> {
2452 /// SELECT e.*, COUNT(h.event_id) as event_count
2453 /// FROM executions e
2454 /// LEFT JOIN history h ON e.instance_id = h.instance_id AND e.execution_id = h.execution_id
2455 /// WHERE e.instance_id = ? AND e.execution_id = ?
2456 /// GROUP BY e.execution_id
2457 /// }
2458 /// ```
2459 async fn get_execution_info(&self, instance: &str, execution_id: u64) -> Result<ExecutionInfo, ProviderError>;
2460
2461 // ===== System Metrics =====
2462
2463 /// Get system-wide metrics for the orchestration engine.
2464 ///
2465 /// # Returns
2466 ///
2467 /// System metrics including instance counts, execution counts, and status breakdown.
2468 ///
2469 /// # Implementation Example
2470 ///
2471 /// ```text
2472 /// async fn get_system_metrics(&self) -> Result<SystemMetrics, String> {
2473 /// SELECT
2474 /// COUNT(*) as total_instances,
2475 /// SUM(CASE WHEN e.status = 'Running' THEN 1 ELSE 0 END) as running_instances,
2476 /// SUM(CASE WHEN e.status = 'Completed' THEN 1 ELSE 0 END) as completed_instances,
2477 /// SUM(CASE WHEN e.status = 'Failed' THEN 1 ELSE 0 END) as failed_instances
2478 /// FROM instances i
2479 /// JOIN executions e ON i.instance_id = e.instance_id AND i.current_execution_id = e.execution_id
2480 /// }
2481 /// ```
2482 async fn get_system_metrics(&self) -> Result<SystemMetrics, ProviderError>;
2483
2484 /// Get the current depths of the internal work queues.
2485 ///
2486 /// # Returns
2487 ///
2488 /// Queue depths for orchestrator and worker queues.
2489 ///
2490 /// **Note:** Timer queue depth is not applicable since timers are handled via
2491 /// delayed visibility in the orchestrator queue.
2492 ///
2493 /// # Implementation Example
2494 ///
2495 /// ```ignore
2496 /// async fn get_queue_depths(&self) -> Result<QueueDepths, String> {
2497 /// SELECT
2498 /// (SELECT COUNT(*) FROM orchestrator_queue WHERE lock_token IS NULL) as orchestrator_queue,
2499 /// (SELECT COUNT(*) FROM worker_queue WHERE lock_token IS NULL) as worker_queue
2500 /// }
2501 /// ```
2502 async fn get_queue_depths(&self) -> Result<QueueDepths, ProviderError>;
2503
2504 // ===== Hierarchy Primitive Operations =====
2505 // These are simple database operations that providers MUST implement.
2506 // Composite operations like get_instance_tree and delete_instance have
2507 // default implementations that use these primitives.
2508
2509 /// List direct children of an instance.
2510 ///
2511 /// Returns instance IDs that have `parent_instance_id = instance_id`.
2512 /// Returns empty vec if instance has no children or doesn't exist.
2513 ///
2514 /// # Implementation Example
2515 ///
2516 /// ```ignore
2517 /// async fn list_children(&self, instance_id: &str) -> Result<Vec<String>, ProviderError> {
2518 /// SELECT instance_id FROM instances WHERE parent_instance_id = ?
2519 /// }
2520 /// ```
2521 async fn list_children(&self, instance_id: &str) -> Result<Vec<String>, ProviderError>;
2522
2523 /// Get the parent instance ID.
2524 ///
2525 /// Returns `Some(parent_id)` for sub-orchestrations, `None` for root orchestrations.
2526 /// Returns `Err` if instance doesn't exist.
2527 ///
2528 /// # Implementation Example
2529 ///
2530 /// ```ignore
2531 /// async fn get_parent_id(&self, instance_id: &str) -> Result<Option<String>, ProviderError> {
2532 /// SELECT parent_instance_id FROM instances WHERE instance_id = ?
2533 /// }
2534 /// ```
2535 async fn get_parent_id(&self, instance_id: &str) -> Result<Option<String>, ProviderError>;
2536
2537 /// Atomically delete a batch of instances.
2538 ///
2539 /// # Parameters
2540 ///
2541 /// * `ids` - Instance IDs to delete.
2542 /// * `force` - If true, delete regardless of status. If false, all instances must be terminal.
2543 ///
2544 /// # Provider Contract (MUST implement)
2545 ///
2546 /// 1. **Atomicity**: All deletions MUST be atomic (all-or-nothing).
2547 /// If any instance fails validation, the entire batch MUST be rolled back.
2548 ///
2549 /// 2. **Force semantics**:
2550 /// - `force=false`: Return error if ANY instance has `status = 'Running'`
2551 /// - `force=true`: Delete regardless of status (for stuck/abandoned instances)
2552 ///
2553 /// 3. **Orphan detection**: Atomically check for children not in `ids`:
2554 /// ```sql
2555 /// SELECT instance_id FROM instances
2556 /// WHERE parent_instance_id IN (?) AND instance_id NOT IN (?)
2557 /// ```
2558 /// If any exist, return error (race condition: new child spawned after get_instance_tree).
2559 ///
2560 /// 4. **Complete cleanup**: Remove ALL related data in a single atomic operation:
2561 /// - event history
2562 /// - executions
2563 /// - orchestrator queue entries
2564 /// - worker queue entries
2565 /// - instance locks
2566 /// - instance metadata
2567 ///
2568 /// 5. **Prevent ack recreation**: When force-deleting Running instances, the removal
2569 /// of the instance lock prevents in-flight `ack_orchestration_item` from recreating state.
2570 ///
2571 /// # Race Condition Protection
2572 ///
2573 /// This method MUST validate within the transaction that no orphans would be created:
2574 /// - If any instance has `parent_instance_id` pointing to an instance in `ids`,
2575 /// but that child is NOT also in `ids`, return an error.
2576 /// - This prevents orphans from children spawned between `get_instance_tree()` and
2577 /// this call.
2578 ///
2579 /// # Transaction Semantics
2580 ///
2581 /// All instances must be deleted atomically (all-or-nothing).
2582 /// The orphan check must be done within the transaction to prevent TOCTOU races.
2583 ///
2584 /// # Implementation Notes
2585 ///
2586 /// - Delete all related data: event history, executions, orchestrator queue, worker queue, instance locks, instance metadata
2587 /// - Order within transaction doesn't matter for correctness (single atomic transaction)
2588 /// - Count deletions and aggregate into result
2589 async fn delete_instances_atomic(&self, ids: &[String], force: bool)
2590 -> Result<DeleteInstanceResult, ProviderError>;
2591
2592 // ===== Hierarchy Composite Operations =====
2593 // These have default implementations using the primitives above.
2594 // Providers can override for better performance if needed.
2595
2596 /// Get the full instance tree rooted at the given instance.
2597 ///
2598 /// Returns all instances in the tree: the root, all children, grandchildren, etc.
2599 ///
2600 /// Default implementation uses `list_children` recursively.
2601 async fn get_instance_tree(&self, instance_id: &str) -> Result<InstanceTree, ProviderError> {
2602 let mut all_ids = vec![];
2603
2604 // BFS to collect all descendants
2605 let mut to_process = vec![instance_id.to_string()];
2606 while let Some(parent_id) = to_process.pop() {
2607 all_ids.push(parent_id.clone());
2608 let children = self.list_children(&parent_id).await?;
2609 to_process.extend(children);
2610 }
2611
2612 Ok(InstanceTree {
2613 root_id: instance_id.to_string(),
2614 all_ids,
2615 })
2616 }
2617
2618 // ===== Deletion/Pruning Operations =====
2619
2620 /// Delete a single orchestration instance and all its associated data.
2621 ///
2622 /// This removes the instance, all executions, all history events, and any
2623 /// pending queue messages (orchestrator, worker, timer).
2624 ///
2625 /// # Default Implementation
2626 ///
2627 /// Uses primitives: `get_parent_id`, `get_instance_tree`, `delete_instances_atomic`.
2628 /// Providers can override for better performance if needed.
2629 ///
2630 /// # Parameters
2631 ///
2632 /// * `instance_id` - The ID of the instance to delete.
2633 /// * `force` - If true, delete even if the instance is in Running state.
2634 /// WARNING: Force delete only removes stored state; it does NOT cancel
2635 /// in-flight tokio tasks. Use `cancel_instance` first for graceful termination.
2636 ///
2637 /// # Returns
2638 ///
2639 /// * `Ok(DeleteResult)` - Details of what was deleted.
2640 /// * `Err(ProviderError)` with `is_retryable() = false` for:
2641 /// - Instance is running and force=false
2642 /// - Instance is a sub-orchestration (must delete root instead)
2643 /// - Instance not found
2644 ///
2645 /// # Safety
2646 ///
2647 /// - Deleting a running instance with force=true may cause in-flight operations
2648 /// to fail when they try to persist state.
2649 /// - Sub-orchestrations cannot be deleted directly; delete the root to cascade.
2650 /// - The instance lock entry is removed to prevent zombie recreation.
2651 async fn delete_instance(&self, instance_id: &str, force: bool) -> Result<DeleteInstanceResult, ProviderError> {
2652 // Step 1: Check if this is a sub-orchestration
2653 let parent = self.get_parent_id(instance_id).await?;
2654 if parent.is_some() {
2655 return Err(ProviderError::permanent(
2656 "delete_instance",
2657 format!("Cannot delete sub-orchestration {instance_id} directly. Delete root instance instead."),
2658 ));
2659 }
2660
2661 // Step 2: Get full tree (includes all descendants)
2662 let tree = self.get_instance_tree(instance_id).await?;
2663
2664 // Step 3: Atomic delete of entire tree
2665 self.delete_instances_atomic(&tree.all_ids, force).await
2666 }
2667
2668 /// Delete multiple orchestration instances matching the filter criteria.
2669 ///
2670 /// Only instances in terminal states (Completed, Failed) are eligible.
2671 /// Running instances are silently skipped (not an error).
2672 ///
2673 /// # Parameters
2674 ///
2675 /// * `filter` - Criteria for selecting instances to delete. All criteria are ANDed.
2676 ///
2677 /// # Filter Behavior
2678 ///
2679 /// - `instance_ids`: Allowlist of specific IDs to consider
2680 /// - `completed_before`: Only delete instances completed before this timestamp (ms)
2681 /// - `limit`: Maximum number of instances to delete (applied last)
2682 ///
2683 /// # Returns
2684 ///
2685 /// Aggregated counts of all deleted data across all deleted instances.
2686 ///
2687 /// # Safety
2688 ///
2689 /// - Running instances are NEVER deleted (silently skipped)
2690 /// - Use `limit` to avoid long-running transactions
2691 /// - Sub-orchestrations are skipped (only roots are deleted with cascade)
2692 async fn delete_instance_bulk(&self, filter: InstanceFilter) -> Result<DeleteInstanceResult, ProviderError>;
2693
2694 /// Prune old executions from a single instance.
2695 ///
2696 /// Use this for `ContinueAsNew` workflows that accumulate many executions.
2697 ///
2698 /// # Parameters
2699 ///
2700 /// * `instance_id` - The instance to prune executions from.
2701 /// * `options` - Criteria for selecting executions to delete. All criteria are ANDed.
2702 ///
2703 /// # Provider Contract (MUST implement)
2704 ///
2705 /// 1. **Current execution protection**: The current execution of the instance
2706 /// MUST NEVER be pruned, regardless of options.
2707 ///
2708 /// 2. **Running execution protection**: Executions with status 'Running'
2709 /// MUST NEVER be pruned.
2710 ///
2711 /// 3. **Atomicity**: All deletions for a single prune call MUST be atomic
2712 /// (all-or-nothing).
2713 ///
2714 /// # `keep_last` Semantics
2715 ///
2716 /// Since current_execution_id is always the highest execution_id:
2717 /// - `keep_last: None` → prune all except current
2718 /// - `keep_last: Some(0)` → same as None (current is protected)
2719 /// - `keep_last: Some(1)` → same as above (top 1 = current)
2720 /// - `keep_last: Some(N)` → keep current + (N-1) most recent
2721 ///
2722 /// **All three (`None`, `Some(0)`, `Some(1)`) are equivalent** because the
2723 /// current execution is always protected regardless of this setting.
2724 ///
2725 /// # Implementation Example
2726 ///
2727 /// ```sql
2728 /// DELETE FROM executions
2729 /// WHERE instance_id = ?
2730 /// AND execution_id != ? -- current_execution_id (CRITICAL)
2731 /// AND status != 'Running'
2732 /// AND execution_id NOT IN (
2733 /// SELECT execution_id FROM executions
2734 /// WHERE instance_id = ?
2735 /// ORDER BY execution_id DESC
2736 /// LIMIT ? -- keep_last
2737 /// )
2738 /// ```
2739 async fn prune_executions(&self, instance_id: &str, options: PruneOptions) -> Result<PruneResult, ProviderError>;
2740
2741 /// Prune old executions from multiple instances matching the filter.
2742 ///
2743 /// Applies the same prune options to all matching instances.
2744 ///
2745 /// # Parameters
2746 ///
2747 /// * `filter` - Criteria for selecting instances to process.
2748 /// * `options` - Criteria for selecting executions to delete within each instance.
2749 ///
2750 /// # Returns
2751 ///
2752 /// Aggregated counts including how many instances were processed.
2753 async fn prune_executions_bulk(
2754 &self,
2755 filter: InstanceFilter,
2756 options: PruneOptions,
2757 ) -> Result<PruneResult, ProviderError>;
2758}
2759
2760#[cfg(test)]
2761mod tests {
2762 use super::*;
2763
2764 #[test]
2765 fn default_only_matches_none_rejects_tag() {
2766 let f = TagFilter::DefaultOnly;
2767 assert!(f.matches(None));
2768 assert!(!f.matches(Some("gpu")));
2769 }
2770
2771 #[test]
2772 fn tags_matches_member_rejects_others() {
2773 let f = TagFilter::tags(["gpu"]);
2774 assert!(f.matches(Some("gpu")));
2775 assert!(!f.matches(Some("cpu")));
2776 assert!(!f.matches(None));
2777 }
2778
2779 #[test]
2780 fn default_and_matches_both() {
2781 let f = TagFilter::default_and(["gpu"]);
2782 assert!(f.matches(None));
2783 assert!(f.matches(Some("gpu")));
2784 assert!(!f.matches(Some("cpu")));
2785 }
2786
2787 #[test]
2788 fn none_matches_nothing() {
2789 let f = TagFilter::None;
2790 assert!(!f.matches(None));
2791 assert!(!f.matches(Some("x")));
2792 }
2793
2794 #[test]
2795 fn any_matches_everything() {
2796 let f = TagFilter::Any;
2797 assert!(f.matches(None));
2798 assert!(f.matches(Some("gpu")));
2799 assert!(f.matches(Some("cpu")));
2800 assert!(f.matches(Some("anything")));
2801 }
2802
2803 #[test]
2804 fn default_impl_is_default_only() {
2805 assert_eq!(TagFilter::default(), TagFilter::DefaultOnly);
2806 }
2807
2808 #[test]
2809 #[should_panic(expected = "at most 5 tags")]
2810 fn panics_over_max_tags() {
2811 TagFilter::tags(["a", "b", "c", "d", "e", "f"]);
2812 }
2813
2814 #[test]
2815 #[should_panic(expected = "requires at least one tag")]
2816 fn panics_on_empty_tags() {
2817 TagFilter::tags(Vec::<String>::new());
2818 }
2819
2820 #[test]
2821 #[should_panic(expected = "requires at least one tag")]
2822 fn panics_on_empty_default_and() {
2823 TagFilter::default_and(Vec::<String>::new());
2824 }
2825}