Skip to main content

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}