feat(wfe-core): add models, traits, and error types
Core domain models: WorkflowInstance, ExecutionPointer, WorkflowDefinition, WorkflowStep, Event, EventSubscription, ScheduledCommand, ExecutionError, LifecycleEvent, PollEndpointConfig. All serde-serializable. Provider traits: PersistenceProvider (composite of WorkflowRepository, EventRepository, SubscriptionRepository, ScheduledCommandRepository), DistributedLockProvider, QueueProvider, SearchIndex, LifecyclePublisher, WorkflowMiddleware, StepMiddleware, WorkflowRegistry. StepBody trait with StepExecutionContext for workflow step implementations. WorkflowData marker trait (blanket impl for Serialize + DeserializeOwned).
This commit is contained in:
9
wfe-core/src/traits/lifecycle.rs
Normal file
9
wfe-core/src/traits/lifecycle.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::models::LifecycleEvent;
|
||||
|
||||
/// Publishes lifecycle events for workflow state transitions.
|
||||
#[async_trait]
|
||||
pub trait LifecyclePublisher: Send + Sync {
|
||||
async fn publish(&self, event: LifecycleEvent) -> crate::Result<()>;
|
||||
}
|
||||
10
wfe-core/src/traits/lock.rs
Normal file
10
wfe-core/src/traits/lock.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
/// Distributed lock provider for preventing concurrent execution of the same workflow.
|
||||
#[async_trait]
|
||||
pub trait DistributedLockProvider: Send + Sync {
|
||||
async fn acquire_lock(&self, resource: &str) -> crate::Result<bool>;
|
||||
async fn release_lock(&self, resource: &str) -> crate::Result<()>;
|
||||
async fn start(&self) -> crate::Result<()>;
|
||||
async fn stop(&self) -> crate::Result<()>;
|
||||
}
|
||||
93
wfe-core/src/traits/middleware.rs
Normal file
93
wfe-core/src/traits/middleware.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::models::{ExecutionResult, WorkflowInstance};
|
||||
use crate::traits::step::StepExecutionContext;
|
||||
|
||||
/// Workflow-level middleware with default no-op implementations.
|
||||
#[async_trait]
|
||||
pub trait WorkflowMiddleware: Send + Sync {
|
||||
async fn pre_workflow(&self, _instance: &WorkflowInstance) -> crate::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
async fn post_workflow(&self, _instance: &WorkflowInstance) -> crate::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Step-level middleware with default no-op implementations.
|
||||
#[async_trait]
|
||||
pub trait StepMiddleware: Send + Sync {
|
||||
async fn pre_step(&self, _context: &StepExecutionContext<'_>) -> crate::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
async fn post_step(
|
||||
&self,
|
||||
_context: &StepExecutionContext<'_>,
|
||||
_result: &ExecutionResult,
|
||||
) -> crate::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::models::{ExecutionPointer, ExecutionResult, WorkflowInstance};
|
||||
|
||||
struct NoOpWorkflowMiddleware;
|
||||
impl WorkflowMiddleware for NoOpWorkflowMiddleware {}
|
||||
|
||||
struct NoOpStepMiddleware;
|
||||
impl StepMiddleware for NoOpStepMiddleware {}
|
||||
|
||||
#[tokio::test]
|
||||
async fn workflow_middleware_default_pre_workflow() {
|
||||
let mw = NoOpWorkflowMiddleware;
|
||||
let instance = WorkflowInstance::new("wf", 1, serde_json::json!({}));
|
||||
mw.pre_workflow(&instance).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn workflow_middleware_default_post_workflow() {
|
||||
let mw = NoOpWorkflowMiddleware;
|
||||
let instance = WorkflowInstance::new("wf", 1, serde_json::json!({}));
|
||||
mw.post_workflow(&instance).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn step_middleware_default_pre_step() {
|
||||
use crate::models::WorkflowStep;
|
||||
let mw = NoOpStepMiddleware;
|
||||
let instance = WorkflowInstance::new("wf", 1, serde_json::json!({}));
|
||||
let pointer = ExecutionPointer::new(0);
|
||||
let step = WorkflowStep::new(0, "test_step");
|
||||
let ctx = StepExecutionContext {
|
||||
item: None,
|
||||
execution_pointer: &pointer,
|
||||
persistence_data: None,
|
||||
step: &step,
|
||||
workflow: &instance,
|
||||
cancellation_token: tokio_util::sync::CancellationToken::new(),
|
||||
};
|
||||
mw.pre_step(&ctx).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn step_middleware_default_post_step() {
|
||||
use crate::models::WorkflowStep;
|
||||
let mw = NoOpStepMiddleware;
|
||||
let instance = WorkflowInstance::new("wf", 1, serde_json::json!({}));
|
||||
let pointer = ExecutionPointer::new(0);
|
||||
let step = WorkflowStep::new(0, "test_step");
|
||||
let ctx = StepExecutionContext {
|
||||
item: None,
|
||||
execution_pointer: &pointer,
|
||||
persistence_data: None,
|
||||
step: &step,
|
||||
workflow: &instance,
|
||||
cancellation_token: tokio_util::sync::CancellationToken::new(),
|
||||
};
|
||||
let result = ExecutionResult::next();
|
||||
mw.post_step(&ctx, &result).await.unwrap();
|
||||
}
|
||||
}
|
||||
20
wfe-core/src/traits/mod.rs
Normal file
20
wfe-core/src/traits/mod.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
pub mod lifecycle;
|
||||
pub mod lock;
|
||||
pub mod middleware;
|
||||
pub mod persistence;
|
||||
pub mod queue;
|
||||
pub mod registry;
|
||||
pub mod search;
|
||||
pub mod step;
|
||||
|
||||
pub use lifecycle::LifecyclePublisher;
|
||||
pub use lock::DistributedLockProvider;
|
||||
pub use middleware::{StepMiddleware, WorkflowMiddleware};
|
||||
pub use persistence::{
|
||||
EventRepository, PersistenceProvider, ScheduledCommandRepository, SubscriptionRepository,
|
||||
WorkflowRepository,
|
||||
};
|
||||
pub use queue::QueueProvider;
|
||||
pub use registry::WorkflowRegistry;
|
||||
pub use search::{Page, SearchFilter, SearchIndex, WorkflowSearchResult};
|
||||
pub use step::{StepBody, StepExecutionContext, WorkflowData};
|
||||
95
wfe-core/src/traits/persistence.rs
Normal file
95
wfe-core/src/traits/persistence.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use crate::models::{
|
||||
Event, EventSubscription, ExecutionError, ScheduledCommand, WorkflowInstance,
|
||||
};
|
||||
|
||||
/// Persistence for workflow instances.
|
||||
#[async_trait]
|
||||
pub trait WorkflowRepository: Send + Sync {
|
||||
async fn create_new_workflow(&self, instance: &WorkflowInstance) -> crate::Result<String>;
|
||||
async fn persist_workflow(&self, instance: &WorkflowInstance) -> crate::Result<()>;
|
||||
async fn persist_workflow_with_subscriptions(
|
||||
&self,
|
||||
instance: &WorkflowInstance,
|
||||
subscriptions: &[EventSubscription],
|
||||
) -> crate::Result<()>;
|
||||
async fn get_runnable_instances(&self, as_at: DateTime<Utc>) -> crate::Result<Vec<String>>;
|
||||
async fn get_workflow_instance(&self, id: &str) -> crate::Result<WorkflowInstance>;
|
||||
async fn get_workflow_instances(&self, ids: &[String]) -> crate::Result<Vec<WorkflowInstance>>;
|
||||
}
|
||||
|
||||
/// Persistence for event subscriptions.
|
||||
#[async_trait]
|
||||
pub trait SubscriptionRepository: Send + Sync {
|
||||
async fn create_event_subscription(
|
||||
&self,
|
||||
subscription: &EventSubscription,
|
||||
) -> crate::Result<String>;
|
||||
async fn get_subscriptions(
|
||||
&self,
|
||||
event_name: &str,
|
||||
event_key: &str,
|
||||
as_of: DateTime<Utc>,
|
||||
) -> crate::Result<Vec<EventSubscription>>;
|
||||
async fn terminate_subscription(&self, subscription_id: &str) -> crate::Result<()>;
|
||||
async fn get_subscription(&self, subscription_id: &str) -> crate::Result<EventSubscription>;
|
||||
async fn get_first_open_subscription(
|
||||
&self,
|
||||
event_name: &str,
|
||||
event_key: &str,
|
||||
as_of: DateTime<Utc>,
|
||||
) -> crate::Result<Option<EventSubscription>>;
|
||||
async fn set_subscription_token(
|
||||
&self,
|
||||
subscription_id: &str,
|
||||
token: &str,
|
||||
worker_id: &str,
|
||||
expiry: DateTime<Utc>,
|
||||
) -> crate::Result<bool>;
|
||||
async fn clear_subscription_token(
|
||||
&self,
|
||||
subscription_id: &str,
|
||||
token: &str,
|
||||
) -> crate::Result<()>;
|
||||
}
|
||||
|
||||
/// Persistence for events.
|
||||
#[async_trait]
|
||||
pub trait EventRepository: Send + Sync {
|
||||
async fn create_event(&self, event: &Event) -> crate::Result<String>;
|
||||
async fn get_event(&self, id: &str) -> crate::Result<Event>;
|
||||
async fn get_runnable_events(&self, as_at: DateTime<Utc>) -> crate::Result<Vec<String>>;
|
||||
async fn get_events(
|
||||
&self,
|
||||
event_name: &str,
|
||||
event_key: &str,
|
||||
as_of: DateTime<Utc>,
|
||||
) -> crate::Result<Vec<String>>;
|
||||
async fn mark_event_processed(&self, id: &str) -> crate::Result<()>;
|
||||
async fn mark_event_unprocessed(&self, id: &str) -> crate::Result<()>;
|
||||
}
|
||||
|
||||
/// Persistence for scheduled commands.
|
||||
#[async_trait]
|
||||
pub trait ScheduledCommandRepository: Send + Sync {
|
||||
fn supports_scheduled_commands(&self) -> bool;
|
||||
async fn schedule_command(&self, command: &ScheduledCommand) -> crate::Result<()>;
|
||||
async fn process_commands(
|
||||
&self,
|
||||
as_of: DateTime<Utc>,
|
||||
handler: &(dyn Fn(ScheduledCommand) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::Result<()>> + Send>>
|
||||
+ Send
|
||||
+ Sync),
|
||||
) -> crate::Result<()>;
|
||||
}
|
||||
|
||||
/// Composite persistence provider combining all repository traits.
|
||||
#[async_trait]
|
||||
pub trait PersistenceProvider:
|
||||
WorkflowRepository + EventRepository + SubscriptionRepository + ScheduledCommandRepository
|
||||
{
|
||||
async fn persist_errors(&self, errors: &[ExecutionError]) -> crate::Result<()>;
|
||||
async fn ensure_store_exists(&self) -> crate::Result<()>;
|
||||
}
|
||||
13
wfe-core/src/traits/queue.rs
Normal file
13
wfe-core/src/traits/queue.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::models::QueueType;
|
||||
|
||||
/// Queue provider for distributing workflow execution across workers.
|
||||
#[async_trait]
|
||||
pub trait QueueProvider: Send + Sync {
|
||||
async fn queue_work(&self, id: &str, queue: QueueType) -> crate::Result<()>;
|
||||
async fn dequeue_work(&self, queue: QueueType) -> crate::Result<Option<String>>;
|
||||
fn is_dequeue_blocking(&self) -> bool;
|
||||
async fn start(&self) -> crate::Result<()>;
|
||||
async fn stop(&self) -> crate::Result<()>;
|
||||
}
|
||||
10
wfe-core/src/traits/registry.rs
Normal file
10
wfe-core/src/traits/registry.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use crate::models::WorkflowDefinition;
|
||||
|
||||
/// Registry for workflow definitions with version support.
|
||||
pub trait WorkflowRegistry: Send + Sync {
|
||||
fn register(&mut self, definition: WorkflowDefinition);
|
||||
fn get_definition(&self, id: &str, version: Option<u32>) -> Option<&WorkflowDefinition>;
|
||||
fn is_registered(&self, id: &str, version: u32) -> bool;
|
||||
fn deregister(&mut self, id: &str, version: u32) -> bool;
|
||||
fn get_all_definitions(&self) -> Vec<&WorkflowDefinition>;
|
||||
}
|
||||
48
wfe-core/src/traits/search.rs
Normal file
48
wfe-core/src/traits/search.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::models::WorkflowInstance;
|
||||
|
||||
/// Result from a search query.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WorkflowSearchResult {
|
||||
pub id: String,
|
||||
pub workflow_definition_id: String,
|
||||
pub version: u32,
|
||||
pub status: crate::models::WorkflowStatus,
|
||||
pub reference: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
/// Filter for search queries.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SearchFilter {
|
||||
Status(crate::models::WorkflowStatus),
|
||||
DateRange {
|
||||
field: String,
|
||||
before: Option<chrono::DateTime<chrono::Utc>>,
|
||||
after: Option<chrono::DateTime<chrono::Utc>>,
|
||||
},
|
||||
Reference(String),
|
||||
}
|
||||
|
||||
/// Paginated search results.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Page<T> {
|
||||
pub data: Vec<T>,
|
||||
pub total: u64,
|
||||
}
|
||||
|
||||
/// Search index for querying workflows.
|
||||
#[async_trait]
|
||||
pub trait SearchIndex: Send + Sync {
|
||||
async fn index_workflow(&self, instance: &WorkflowInstance) -> crate::Result<()>;
|
||||
async fn search(
|
||||
&self,
|
||||
terms: &str,
|
||||
skip: u64,
|
||||
take: u64,
|
||||
filters: &[SearchFilter],
|
||||
) -> crate::Result<Page<WorkflowSearchResult>>;
|
||||
async fn start(&self) -> crate::Result<()>;
|
||||
async fn stop(&self) -> crate::Result<()>;
|
||||
}
|
||||
35
wfe-core/src/traits/step.rs
Normal file
35
wfe-core/src/traits/step.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use async_trait::async_trait;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::models::{ExecutionPointer, ExecutionResult, WorkflowInstance, WorkflowStep};
|
||||
|
||||
/// Marker trait for all data types that flow between workflow steps.
|
||||
/// Anything that is serializable and deserializable qualifies.
|
||||
pub trait WorkflowData: Serialize + DeserializeOwned + Send + Sync + Clone + 'static {}
|
||||
|
||||
/// Blanket implementation: any type satisfying the bounds is WorkflowData.
|
||||
impl<T> WorkflowData for T where T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static {}
|
||||
|
||||
/// Context available to a step during execution.
|
||||
#[derive(Debug)]
|
||||
pub struct StepExecutionContext<'a> {
|
||||
/// The current item when iterating (ForEach).
|
||||
pub item: Option<&'a serde_json::Value>,
|
||||
/// The current execution pointer.
|
||||
pub execution_pointer: &'a ExecutionPointer,
|
||||
/// Persistence data from a previous execution of this step.
|
||||
pub persistence_data: Option<&'a serde_json::Value>,
|
||||
/// The step definition.
|
||||
pub step: &'a WorkflowStep,
|
||||
/// The running workflow instance.
|
||||
pub workflow: &'a WorkflowInstance,
|
||||
/// Cancellation token.
|
||||
pub cancellation_token: tokio_util::sync::CancellationToken,
|
||||
}
|
||||
|
||||
/// The core unit of work in a workflow. Each step implements this trait.
|
||||
#[async_trait]
|
||||
pub trait StepBody: Send + Sync {
|
||||
async fn run(&mut self, context: &StepExecutionContext<'_>) -> crate::Result<ExecutionResult>;
|
||||
}
|
||||
Reference in New Issue
Block a user