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:
2026-03-25 20:07:50 +00:00
parent 098564db51
commit d87d888787
25 changed files with 1627 additions and 0 deletions

View 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<()>;
}

View 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<()>;
}

View 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();
}
}

View 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};

View 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<()>;
}

View 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<()>;
}

View 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>;
}

View 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<()>;
}

View 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>;
}