Skip to main content

oximedia_workflow/
lib.rs

1//! Comprehensive workflow orchestration engine for `OxiMedia`.
2//!
3//! This crate provides a complete workflow orchestration system with:
4//! - DAG-based workflow definition
5//! - Task dependencies and parallel execution
6//! - State persistence with `SQLite`
7//! - Cron-style scheduling
8//! - REST API for workflow management
9//! - Real-time monitoring and metrics
10//! - Multiple task types (transcode, QC, transfer, etc.)
11//!
12//! # Examples
13//!
14//! ## Creating a Simple Workflow
15//!
16//! ```rust
17//! use oximedia_workflow::{Workflow, Task, TaskType};
18//! use std::time::Duration;
19//!
20//! let mut workflow = Workflow::new("my-workflow");
21//!
22//! let task = Task::new("wait-task", TaskType::Wait {
23//!     duration: Duration::from_secs(5),
24//! });
25//!
26//! workflow.add_task(task);
27//! ```
28//!
29//! ## Creating a Multi-Pass Encoding Workflow
30//!
31//! ```rust
32//! use oximedia_workflow::patterns::multi_pass_encoding;
33//! use std::path::PathBuf;
34//!
35//! let workflow = multi_pass_encoding(
36//!     PathBuf::from("/source.mp4"),
37//!     PathBuf::from("/proxy.mp4"),
38//!     PathBuf::from("/output.mp4"),
39//!     "broadcast".to_string(),
40//! );
41//! ```
42
43#![forbid(unsafe_code)]
44#![warn(missing_docs)]
45
46#[cfg(all(not(target_arch = "wasm32"), feature = "sqlite"))]
47pub mod api;
48pub mod approval_gate;
49pub mod audit_log;
50pub mod batch_status;
51pub mod builder;
52pub mod checkpoint;
53pub mod circuit_breaker;
54#[cfg(all(not(target_arch = "wasm32"), feature = "sqlite"))]
55pub mod cli;
56pub mod cost_tracking;
57pub mod dag;
58pub mod error;
59pub mod event_bus;
60pub mod execution_trace;
61#[cfg(not(target_arch = "wasm32"))]
62pub mod executor;
63#[cfg(not(target_arch = "wasm32"))]
64pub mod fan_pattern;
65pub mod fanout;
66pub mod monitoring;
67pub mod notification_sink;
68pub mod notification_system;
69pub mod parallel_steps;
70pub mod patterns;
71pub mod pause_resume;
72#[cfg(all(not(target_arch = "wasm32"), feature = "sqlite"))]
73pub mod persistence;
74#[cfg(not(target_arch = "wasm32"))]
75pub mod queue;
76pub mod resource_pool;
77pub mod retry_policy;
78#[cfg(not(target_arch = "wasm32"))]
79pub mod scheduler;
80pub mod sla;
81pub mod sla_tracking;
82pub mod state_machine;
83pub mod step_condition;
84pub mod step_conditions;
85pub mod step_result;
86pub mod task;
87pub mod task_dependency;
88pub mod task_graph;
89pub mod task_priority_queue;
90pub mod task_template;
91pub mod templates;
92pub mod triggers;
93pub mod utils;
94pub mod validation;
95pub mod versioning;
96pub mod webhook;
97pub mod webhook_trigger;
98#[cfg(not(target_arch = "wasm32"))]
99pub mod websocket;
100pub mod workflow;
101pub mod workflow_audit;
102pub mod workflow_bundle;
103pub mod workflow_checkpoint;
104pub mod workflow_compose;
105pub mod workflow_dashboard;
106pub mod workflow_diff;
107pub mod workflow_health_check;
108pub mod workflow_import_export;
109pub mod workflow_log;
110pub mod workflow_marketplace;
111pub mod workflow_metrics;
112pub mod workflow_migration;
113pub mod workflow_retry;
114pub mod workflow_simulation;
115pub mod workflow_snapshot;
116pub mod workflow_template;
117pub mod workflow_throttle;
118pub mod workflow_version;
119
120// Re-exports for convenience
121pub use batch_status::{BatchStatusWriter, FlushResult, StatusUpdate};
122pub use builder::{
123    QcTaskBuilder, TaskBuilder, TranscodeTaskBuilder, TransferTaskBuilder, WorkflowBuilder,
124};
125pub use circuit_breaker::{
126    CircuitBreaker, CircuitBreakerConfig, CircuitBreakerMetrics, CircuitBreakerRegistry,
127    CircuitBreakerSummary, CircuitDecision, CircuitState,
128};
129#[cfg(all(not(target_arch = "wasm32"), feature = "sqlite"))]
130pub use cli::Cli;
131pub use dag::{
132    audio_normalize, ingest_transcode, subtitle_burn, BranchEvaluator, BranchNode, BranchType,
133    DagError, DagRunStatus, DagWorkflowEngine, NodeId, NodeStatus, WorkflowDag, WorkflowEdge,
134    WorkflowNode, WorkflowTemplate,
135};
136pub use error::{Result, WorkflowError};
137pub use event_bus::{
138    BusEvent, EventBus, EventBusConfig, EventBusStats, EventFilter, SubscriptionId,
139};
140#[cfg(not(target_arch = "wasm32"))]
141pub use executor::{
142    parse_condition, DefaultTaskExecutor, ExecutionContext, TaskExecutor, WorkflowControl,
143    WorkflowExecutor,
144};
145pub use monitoring::{MonitoringService, SystemStatistics, TaskMetrics, WorkflowMetrics};
146pub use parallel_steps::{
147    execute_step, ParallelStepError, ParallelSteps, StepResult, StepType, WorkflowStage,
148    WorkflowStep,
149};
150pub use patterns::*;
151#[cfg(all(not(target_arch = "wasm32"), feature = "sqlite"))]
152pub use persistence::PersistenceManager;
153#[cfg(not(target_arch = "wasm32"))]
154pub use queue::{QueueStatistics, TaskQueue};
155pub use retry_policy::{ExponentialRetryPolicy, RetryDecision, RetryPolicyState};
156#[cfg(not(target_arch = "wasm32"))]
157pub use scheduler::{
158    Clock, FileWatcher, ScheduledWorkflow, SystemClock, Trigger, WorkflowScheduler,
159};
160pub use task::{
161    AnalysisType, HttpMethod, NotificationChannel, RetryPolicy, Task, TaskId, TaskPriority,
162    TaskResult, TaskState, TaskType, TransferProtocol,
163};
164pub use task_priority_queue::{PriorityEntry, PriorityLevel, TaskPriorityQueue};
165pub use utils::{
166    calculate_parallelism, clone_workflow, estimate_workflow_duration, expand_env_vars,
167    expand_template, find_critical_path, format_duration, generate_task_name,
168    get_workflow_statistics, merge_configs, normalize_paths, parse_duration, sanitize_task_name,
169    WorkflowStatistics,
170};
171pub use validation::{
172    ComplexityAnalyzer, ComplexityLevel, ComplexityMetrics, TaskValidator, ValidationReport,
173    ValidationRule, WorkflowValidator,
174};
175pub use webhook::{WebhookConfig, WebhookEvent, WebhookNotifier, WorkflowContext};
176#[cfg(not(target_arch = "wasm32"))]
177pub use websocket::{WebSocketManager, WebSocketState, WorkflowEvent};
178pub use workflow::{Edge, Workflow, WorkflowConfig, WorkflowId, WorkflowState};
179pub use workflow_import_export::{
180    deserialize_bundle, export_workflow, import_workflow, serialize_bundle, BundleFormat,
181    DagBundle, WorkflowBundle, WorkflowDefinition,
182};
183pub use workflow_migration::{
184    FieldChange, MigrationError, MigrationRegistry, MigrationStep, SchemaVersion,
185};
186pub use workflow_simulation::{
187    quick_simulate, ConditionSource, EvaluatedCondition, SimulatedOutcome, SimulationConfig,
188    SimulationResult, SimulationSummary, WorkflowSimulator,
189};
190
191/// Workflow engine - main entry point for the orchestration system.
192#[cfg(all(not(target_arch = "wasm32"), feature = "sqlite"))]
193pub struct WorkflowEngine {
194    persistence: std::sync::Arc<PersistenceManager>,
195    scheduler: std::sync::Arc<WorkflowScheduler>,
196    monitoring: std::sync::Arc<MonitoringService>,
197    executor: std::sync::Arc<dyn TaskExecutor>,
198}
199
200#[cfg(all(not(target_arch = "wasm32"), feature = "sqlite"))]
201impl WorkflowEngine {
202    /// Create a new workflow engine with the specified database path.
203    ///
204    /// # Errors
205    ///
206    /// Returns an error if the database cannot be initialized.
207    pub fn new(db_path: impl AsRef<std::path::Path>) -> Result<Self> {
208        let persistence = std::sync::Arc::new(PersistenceManager::new(db_path)?);
209        let scheduler = std::sync::Arc::new(WorkflowScheduler::new());
210        let monitoring = std::sync::Arc::new(MonitoringService::new());
211        let executor = std::sync::Arc::new(DefaultTaskExecutor);
212
213        Ok(Self {
214            persistence,
215            scheduler,
216            monitoring,
217            executor,
218        })
219    }
220
221    /// Create an in-memory workflow engine (useful for testing).
222    ///
223    /// # Errors
224    ///
225    /// Returns an error if the in-memory database cannot be initialized.
226    pub fn in_memory() -> Result<Self> {
227        let persistence = std::sync::Arc::new(PersistenceManager::in_memory()?);
228        let scheduler = std::sync::Arc::new(WorkflowScheduler::new());
229        let monitoring = std::sync::Arc::new(MonitoringService::new());
230        let executor = std::sync::Arc::new(DefaultTaskExecutor);
231
232        Ok(Self {
233            persistence,
234            scheduler,
235            monitoring,
236            executor,
237        })
238    }
239
240    /// Get persistence manager.
241    #[must_use]
242    pub fn persistence(&self) -> &std::sync::Arc<PersistenceManager> {
243        &self.persistence
244    }
245
246    /// Get scheduler.
247    #[must_use]
248    pub fn scheduler(&self) -> &std::sync::Arc<WorkflowScheduler> {
249        &self.scheduler
250    }
251
252    /// Get monitoring service.
253    #[must_use]
254    pub fn monitoring(&self) -> &std::sync::Arc<MonitoringService> {
255        &self.monitoring
256    }
257
258    /// Get task executor.
259    #[must_use]
260    pub fn executor(&self) -> &std::sync::Arc<dyn TaskExecutor> {
261        &self.executor
262    }
263
264    /// Submit a workflow for execution.
265    ///
266    /// # Errors
267    ///
268    /// Returns an error if the workflow cannot be saved or executed.
269    pub async fn submit_workflow(&self, workflow: &Workflow) -> Result<WorkflowId> {
270        // `SqliteConnectionBlocking` uses an internal current_thread runtime
271        // (`block_local`). Calling it directly from an async worker thread
272        // panics with "Cannot start a runtime from within a runtime."
273        // `block_in_place` temporarily removes this thread from the executor
274        // scheduler, allowing the nested runtime to run safely.
275        tokio::task::block_in_place(|| self.persistence.save_workflow(workflow))?;
276        Ok(workflow.id)
277    }
278
279    /// Execute a workflow immediately.
280    ///
281    /// # Errors
282    ///
283    /// Returns an error if the workflow cannot be loaded or executed.
284    pub async fn execute_workflow(&self, workflow_id: WorkflowId) -> Result<()> {
285        // Both load and save call SqliteConnectionBlocking methods that use
286        // block_local (creates its own current_thread runtime). block_in_place
287        // is required to avoid "cannot start a runtime from within a runtime".
288        let mut workflow =
289            tokio::task::block_in_place(|| self.persistence.load_workflow(workflow_id))?;
290
291        let executor = WorkflowExecutor::new(self.executor.clone());
292
293        self.monitoring
294            .start_workflow(workflow_id, workflow.name.clone(), workflow.tasks.len());
295
296        let result = executor.execute(&mut workflow).await?;
297
298        self.monitoring
299            .complete_workflow(workflow_id, result.state == WorkflowState::Completed);
300
301        tokio::task::block_in_place(|| self.persistence.save_workflow(&workflow))?;
302
303        Ok(())
304    }
305
306    /// Schedule a workflow with a trigger.
307    ///
308    /// # Errors
309    ///
310    /// Returns an error if the workflow cannot be scheduled.
311    pub async fn schedule_workflow(
312        &self,
313        workflow: Workflow,
314        trigger: Trigger,
315    ) -> Result<WorkflowId> {
316        let workflow_id = workflow.id;
317        tokio::task::block_in_place(|| self.persistence.save_workflow(&workflow))?;
318        self.scheduler.add_schedule(workflow, trigger).await?;
319        Ok(workflow_id)
320    }
321
322    /// Start the workflow engine (scheduler and monitoring).
323    ///
324    /// # Errors
325    ///
326    /// Returns an error if the scheduler cannot be started.
327    pub async fn start(&self) -> Result<()> {
328        self.scheduler.start().await?;
329        tracing::info!("Workflow engine started");
330        Ok(())
331    }
332
333    /// Stop the workflow engine.
334    ///
335    /// # Errors
336    ///
337    /// Returns an error if the scheduler cannot be stopped.
338    pub async fn stop(&self) -> Result<()> {
339        self.scheduler.stop().await?;
340        tracing::info!("Workflow engine stopped");
341        Ok(())
342    }
343
344    /// Process scheduled workflows (should be called periodically).
345    pub async fn process_schedules(&self) -> Result<Vec<WorkflowId>> {
346        let ready_workflows = self.scheduler.check_schedules().await;
347        let mut executed = Vec::new();
348
349        for workflow in ready_workflows {
350            let workflow_id = workflow.id;
351            tokio::task::block_in_place(|| self.persistence.save_workflow(&workflow))?;
352
353            // Execute in background
354            let engine = Self {
355                persistence: self.persistence.clone(),
356                scheduler: self.scheduler.clone(),
357                monitoring: self.monitoring.clone(),
358                executor: self.executor.clone(),
359            };
360
361            tokio::spawn(async move {
362                if let Err(e) = engine.execute_workflow(workflow_id).await {
363                    tracing::error!("Scheduled workflow execution failed: {}", e);
364                }
365            });
366
367            executed.push(workflow_id);
368        }
369
370        Ok(executed)
371    }
372
373    /// Create API state for REST API server.
374    #[must_use]
375    pub fn api_state(&self) -> api::ApiState {
376        api::ApiState {
377            persistence: self.persistence.clone(),
378            scheduler: self.scheduler.clone(),
379            monitoring: self.monitoring.clone(),
380            executor: self.executor.clone(),
381            active_workflows: std::sync::Arc::new(tokio::sync::RwLock::new(
382                std::collections::HashMap::new(),
383            )),
384        }
385    }
386
387    /// Create API router for REST API server.
388    pub fn api_router(&self) -> axum::Router {
389        api::create_router(self.api_state())
390    }
391}
392
393#[cfg(all(test, feature = "sqlite"))]
394mod tests {
395    use super::*;
396    use std::time::Duration;
397
398    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
399    async fn test_workflow_engine_creation() {
400        // WorkflowEngine::in_memory() is synchronous but calls
401        // SqliteConnectionBlocking internally which uses block_local (builds a
402        // fresh current_thread runtime). block_in_place lets the nested runtime
403        // run by temporarily removing this worker from the executor pool.
404        let engine = tokio::task::block_in_place(WorkflowEngine::in_memory);
405        assert!(engine.is_ok());
406    }
407
408    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
409    async fn test_submit_workflow() {
410        let engine =
411            tokio::task::block_in_place(WorkflowEngine::in_memory).expect("should succeed in test");
412        let workflow = Workflow::new("test-workflow");
413
414        let workflow_id = engine
415            .submit_workflow(&workflow)
416            .await
417            .expect("should succeed in test");
418        assert_eq!(workflow_id, workflow.id);
419    }
420
421    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
422    async fn test_execute_workflow() {
423        let engine =
424            tokio::task::block_in_place(WorkflowEngine::in_memory).expect("should succeed in test");
425        let mut workflow = Workflow::new("test-workflow");
426
427        let task = Task::new(
428            "wait-task",
429            TaskType::Wait {
430                duration: Duration::from_millis(10),
431            },
432        );
433        workflow.add_task(task);
434
435        let workflow_id = engine
436            .submit_workflow(&workflow)
437            .await
438            .expect("should succeed in test");
439        let result = engine.execute_workflow(workflow_id).await;
440        assert!(result.is_ok());
441    }
442
443    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
444    async fn test_schedule_workflow() {
445        let engine =
446            tokio::task::block_in_place(WorkflowEngine::in_memory).expect("should succeed in test");
447        let workflow = Workflow::new("test-workflow");
448        let trigger = Trigger::Manual;
449
450        let workflow_id = engine
451            .schedule_workflow(workflow, trigger)
452            .await
453            .expect("should succeed in test");
454
455        let schedules = engine.scheduler.list_schedules().await;
456        assert_eq!(schedules.len(), 1);
457        assert_eq!(schedules[0].0, workflow_id);
458    }
459
460    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
461    async fn test_engine_start_stop() {
462        let engine =
463            tokio::task::block_in_place(WorkflowEngine::in_memory).expect("should succeed in test");
464
465        engine.start().await.expect("should succeed in test");
466        engine.stop().await.expect("should succeed in test");
467    }
468}