use super::{
Budget, CheckpointId, Deserialize, Duration, Error, Future, NonZeroU32, NonZeroU64, Pin,
Serialize, Usage, Value, WorkflowInterruptRequest, WorkflowLineage, WorkflowWait, WorkflowWake,
};
pub type WorkflowStoreFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct WorkflowTenantId(String);
impl WorkflowTenantId {
pub fn parse(value: impl Into<String>) -> Result<Self, WorkflowStoreError> {
let value = value.into();
if value.is_empty()
|| value.len() > 128
|| !value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.'))
{
return Err(WorkflowStoreError::invalid_input(
"workflow tenant must contain 1..=128 portable ASCII characters",
));
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Default for WorkflowTenantId {
fn default() -> Self {
Self("default".into())
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct WorkflowTenantPolicy {
max_outstanding_tasks: NonZeroU32,
max_concurrent_leases: NonZeroU32,
}
impl WorkflowTenantPolicy {
pub fn new(
max_outstanding_tasks: u32,
max_concurrent_leases: u32,
) -> Result<Self, WorkflowStoreError> {
let max_outstanding_tasks = NonZeroU32::new(max_outstanding_tasks).ok_or_else(|| {
WorkflowStoreError::invalid_input("tenant outstanding workflow limit must be positive")
})?;
let max_concurrent_leases = NonZeroU32::new(max_concurrent_leases).ok_or_else(|| {
WorkflowStoreError::invalid_input(
"tenant concurrent workflow lease limit must be positive",
)
})?;
if max_concurrent_leases > max_outstanding_tasks {
return Err(WorkflowStoreError::invalid_input(
"tenant concurrent workflow lease limit cannot exceed outstanding limit",
));
}
Ok(Self {
max_outstanding_tasks,
max_concurrent_leases,
})
}
pub const fn max_outstanding_tasks(self) -> u32 {
self.max_outstanding_tasks.get()
}
pub const fn max_concurrent_leases(self) -> u32 {
self.max_concurrent_leases.get()
}
}
impl Default for WorkflowTenantPolicy {
fn default() -> Self {
Self {
max_outstanding_tasks: NonZeroU32::new(10_000)
.expect("default outstanding limit is positive"),
max_concurrent_leases: NonZeroU32::new(100)
.expect("default concurrent lease limit is positive"),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct WorkflowTenantListLimit(NonZeroU32);
impl WorkflowTenantListLimit {
pub fn new(value: u32) -> Result<Self, WorkflowStoreError> {
let value = NonZeroU32::new(value).ok_or_else(|| {
WorkflowStoreError::invalid_input("workflow tenant list limit must be positive")
})?;
if value.get() > 1_000 {
return Err(WorkflowStoreError::invalid_input(
"workflow tenant list limit cannot exceed 1,000",
));
}
Ok(Self(value))
}
pub const fn get(self) -> u32 {
self.0.get()
}
}
impl Default for WorkflowTenantListLimit {
fn default() -> Self {
Self(NonZeroU32::new(100).expect("default tenant page size is positive"))
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct WorkflowTenantBudgetPolicy {
limit: Budget,
window_ms: NonZeroU64,
recovery_grace_ms: u64,
}
impl WorkflowTenantBudgetPolicy {
pub fn new(
limit: Budget,
window: Duration,
recovery_grace: Duration,
) -> Result<Self, WorkflowStoreError> {
if budget_is_unbounded(limit) {
return Err(WorkflowStoreError::invalid_input(
"tenant budget policy must limit at least one resource",
));
}
validate_budget_duration(limit)?;
let window_ms = u64::try_from(window.as_millis())
.ok()
.and_then(NonZeroU64::new)
.ok_or_else(|| {
WorkflowStoreError::invalid_input(
"tenant budget window must fit in positive whole milliseconds",
)
})?;
let recovery_grace_ms = u64::try_from(recovery_grace.as_millis()).map_err(|_| {
WorkflowStoreError::invalid_input(
"tenant budget recovery grace exceeds supported milliseconds",
)
})?;
Ok(Self {
limit,
window_ms,
recovery_grace_ms,
})
}
pub const fn limit(self) -> Budget {
self.limit
}
pub const fn window_millis(self) -> u64 {
self.window_ms.get()
}
pub const fn recovery_grace_millis(self) -> u64 {
self.recovery_grace_ms
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkflowTenantBudgetSnapshot {
pub tenant_id: WorkflowTenantId,
pub limit: Budget,
pub window_started_at_ms: u64,
pub committed: Usage,
pub reserved: Usage,
pub active_reservations: u64,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct WorkflowBudgetAuditCursor(u64);
impl WorkflowBudgetAuditCursor {
pub const fn new(sequence: u64) -> Self {
Self(sequence)
}
pub const fn sequence(self) -> u64 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct WorkflowBudgetAuditLimit(NonZeroU32);
impl WorkflowBudgetAuditLimit {
pub fn new(value: u32) -> Result<Self, WorkflowStoreError> {
let value = NonZeroU32::new(value).ok_or_else(|| {
WorkflowStoreError::invalid_input("workflow budget audit limit must be positive")
})?;
if value.get() > 1_000 {
return Err(WorkflowStoreError::invalid_input(
"workflow budget audit limit cannot exceed 1,000",
));
}
Ok(Self(value))
}
pub const fn get(self) -> u32 {
self.0.get()
}
}
impl Default for WorkflowBudgetAuditLimit {
fn default() -> Self {
Self(NonZeroU32::new(100).expect("default audit page size is positive"))
}
}
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct WorkflowBudgetAuditProjectionId(String);
impl WorkflowBudgetAuditProjectionId {
pub fn parse(value: impl Into<String>) -> Result<Self, WorkflowStoreError> {
let value = value.into();
if value.is_empty()
|| value.len() > 128
|| !value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.'))
{
return Err(WorkflowStoreError::invalid_input(
"workflow budget audit projection must contain 1..=128 portable ASCII characters",
));
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkflowBudgetAuditProjectionLease {
pub tenant_id: WorkflowTenantId,
pub projection_id: WorkflowBudgetAuditProjectionId,
pub owner: WorkerId,
pub cursor: WorkflowBudgetAuditCursor,
pub fencing_token: u64,
pub expires_at_ms: u64,
}
impl WorkflowBudgetAuditProjectionLease {
pub fn tenant_id(&self) -> &WorkflowTenantId {
&self.tenant_id
}
pub fn projection_id(&self) -> &WorkflowBudgetAuditProjectionId {
&self.projection_id
}
pub fn owner(&self) -> &WorkerId {
&self.owner
}
pub const fn cursor(&self) -> WorkflowBudgetAuditCursor {
self.cursor
}
pub const fn fencing_token(&self) -> u64 {
self.fencing_token
}
pub const fn expires_at_ms(&self) -> u64 {
self.expires_at_ms
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
pub enum WorkflowBudgetForfeitReason {
Cancelled,
RecoveryExpired,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
pub enum WorkflowBudgetAuditKind {
PolicyConfigured,
Reserved,
Adopted,
AdmissionDenied,
UsageExceeded,
Settled,
Forfeited(WorkflowBudgetForfeitReason),
WindowReset,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkflowBudgetAuditEvent {
pub cursor: WorkflowBudgetAuditCursor,
pub tenant_id: WorkflowTenantId,
pub checkpoint_id: Option<CheckpointId>,
pub occurred_at_ms: u64,
pub kind: WorkflowBudgetAuditKind,
pub usage: Usage,
pub reservation_age_ms: Option<u64>,
pub limit: Budget,
pub committed: Usage,
pub reserved: Usage,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum WorkflowBudgetReservationOutcome {
NotConfigured,
Reserved,
}
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct WorkerId(String);
impl WorkerId {
pub fn parse(value: impl Into<String>) -> Result<Self, WorkflowStoreError> {
let value = value.into();
if value.is_empty()
|| value.len() > 128
|| !value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.'))
{
return Err(WorkflowStoreError::invalid_input(
"worker identity must contain 1..=128 portable ASCII characters",
));
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct LeaseDuration(NonZeroU64);
impl LeaseDuration {
pub fn new(duration: Duration) -> Result<Self, WorkflowStoreError> {
let millis = u64::try_from(duration.as_millis())
.ok()
.and_then(NonZeroU64::new)
.ok_or_else(|| {
WorkflowStoreError::invalid_input(
"workflow lease must fit in a positive whole-millisecond duration",
)
})?;
Ok(Self(millis))
}
pub const fn as_millis(self) -> u64 {
self.0.get()
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct WorkflowTask {
pub checkpoint_id: CheckpointId,
pub tenant_id: WorkflowTenantId,
pub workflow: String,
pub workflow_version: u32,
pub input: Value,
pub priority: i32,
}
impl WorkflowTask {
pub fn new(
workflow: impl Into<String>,
workflow_version: u32,
input: Value,
) -> Result<Self, WorkflowStoreError> {
let workflow = workflow.into();
if workflow.trim().is_empty() || workflow.len() > 256 {
return Err(WorkflowStoreError::invalid_input(
"workflow name must contain 1..=256 bytes",
));
}
if workflow_version == 0 {
return Err(WorkflowStoreError::invalid_input(
"workflow version must be greater than zero",
));
}
Ok(Self {
checkpoint_id: CheckpointId::new(),
tenant_id: WorkflowTenantId::default(),
workflow,
workflow_version,
input,
priority: 0,
})
}
#[must_use]
pub fn with_tenant(mut self, tenant_id: WorkflowTenantId) -> Self {
self.tenant_id = tenant_id;
self
}
#[must_use]
pub const fn with_checkpoint_id(mut self, checkpoint_id: CheckpointId) -> Self {
self.checkpoint_id = checkpoint_id;
self
}
#[must_use]
pub const fn with_priority(mut self, priority: i32) -> Self {
self.priority = priority;
self
}
pub fn validate(&self) -> Result<(), WorkflowStoreError> {
if self.workflow.trim().is_empty() || self.workflow.len() > 256 {
return Err(WorkflowStoreError::invalid_input(
"workflow name must contain 1..=256 bytes",
));
}
if self.workflow_version == 0 {
return Err(WorkflowStoreError::invalid_input(
"workflow version must be greater than zero",
));
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct WorkflowLease {
pub checkpoint_id: CheckpointId,
pub tenant_id: WorkflowTenantId,
pub worker: WorkerId,
pub fencing_token: u64,
pub attempt: u64,
pub expires_at_ms: u64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ClaimedWorkflow {
pub task: WorkflowTask,
pub lease: WorkflowLease,
pub wake: Option<WorkflowWake>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum WorkflowDisposition {
Completed,
RetryAfter(Duration),
Suspend(WorkflowWait),
Failed(String),
Cancelled,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum WorkflowTaskStatus {
Queued,
Leased,
Waiting,
Completed,
Failed,
Cancelled,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct WorkflowTaskRetention(NonZeroU64);
impl WorkflowTaskRetention {
pub fn new(duration: Duration) -> Result<Self, WorkflowStoreError> {
let millis = u64::try_from(duration.as_millis())
.ok()
.and_then(NonZeroU64::new)
.ok_or_else(|| {
WorkflowStoreError::invalid_input(
"workflow Task retention must fit in positive whole milliseconds",
)
})?;
Ok(Self(millis))
}
pub const fn as_millis(self) -> u64 {
self.0.get()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct WorkflowTaskCleanupLimit(NonZeroU32);
impl WorkflowTaskCleanupLimit {
pub fn new(value: u32) -> Result<Self, WorkflowStoreError> {
let value = NonZeroU32::new(value).ok_or_else(|| {
WorkflowStoreError::invalid_input("workflow Task cleanup limit must be positive")
})?;
if value.get() > 1_000 {
return Err(WorkflowStoreError::invalid_input(
"workflow Task cleanup limit cannot exceed 1,000",
));
}
Ok(Self(value))
}
pub const fn get(self) -> u32 {
self.0.get()
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
pub struct WorkflowTaskTombstoneCursor(u64);
impl WorkflowTaskTombstoneCursor {
pub const fn new(sequence: u64) -> Self {
Self(sequence)
}
pub const fn get(self) -> u64 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct WorkflowTaskTombstoneLimit(NonZeroU32);
impl WorkflowTaskTombstoneLimit {
pub fn new(value: u32) -> Result<Self, WorkflowStoreError> {
let value = NonZeroU32::new(value).ok_or_else(|| {
WorkflowStoreError::invalid_input("workflow Task tombstone limit must be positive")
})?;
if value.get() > 1_000 {
return Err(WorkflowStoreError::invalid_input(
"workflow Task tombstone limit cannot exceed 1,000",
));
}
Ok(Self(value))
}
pub const fn get(self) -> u32 {
self.0.get()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkflowTaskCleanupLease {
pub tenant_id: WorkflowTenantId,
pub owner: WorkerId,
pub fencing_token: u64,
pub expires_at_ms: u64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkflowTaskTombstone {
pub cursor: WorkflowTaskTombstoneCursor,
pub checkpoint_id: CheckpointId,
pub tenant_id: WorkflowTenantId,
pub workflow: String,
pub workflow_version: u32,
pub final_status: WorkflowTaskStatus,
pub created_at_ms: u64,
pub terminal_at_ms: u64,
pub deleted_at_ms: u64,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum WorkflowCancelOutcome {
Cancelled,
AlreadyTerminal,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkflowTaskSnapshot {
pub checkpoint_id: CheckpointId,
pub tenant_id: WorkflowTenantId,
pub workflow: String,
pub workflow_version: u32,
pub status: WorkflowTaskStatus,
pub created_at_ms: u64,
pub updated_at_ms: u64,
pub attempts: u64,
pub fencing_token: u64,
pub owner: Option<WorkerId>,
pub lease_expires_at_ms: Option<u64>,
pub interrupt: Option<WorkflowInterruptRequest>,
pub failure_message: Option<String>,
pub lineage: Option<WorkflowLineage>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum WorkflowStoreErrorKind {
InvalidInput,
NotFound,
Conflict,
LeaseLost,
AdmissionDenied,
TenantMismatch,
Storage,
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
#[error("{kind:?}: {message}")]
pub struct WorkflowStoreError {
pub kind: WorkflowStoreErrorKind,
pub message: String,
}
impl WorkflowStoreError {
pub fn new(kind: WorkflowStoreErrorKind, message: impl Into<String>) -> Self {
Self {
kind,
message: message.into(),
}
}
pub(super) fn invalid_input(message: impl Into<String>) -> Self {
Self::new(WorkflowStoreErrorKind::InvalidInput, message)
}
}
fn budget_is_unbounded(budget: Budget) -> bool {
budget.tokens.is_none()
&& budget.cost_microusd.is_none()
&& budget.duration.is_none()
&& budget.turns.is_none()
&& budget.tool_calls.is_none()
&& budget.delegations.is_none()
}
fn validate_budget_duration(budget: Budget) -> Result<(), WorkflowStoreError> {
if let Some(duration) = budget.duration {
duration_micros(duration)?;
}
Ok(())
}
pub(super) fn duration_micros(duration: Duration) -> Result<u64, WorkflowStoreError> {
u64::try_from(duration.as_micros()).map_err(|_| {
WorkflowStoreError::invalid_input("budget duration exceeds supported microseconds")
})
}