///|
/// Describes which deterministic failures may be retried by a caller.
pub(all) enum RetryMode {
Never
GuardOnly
UnknownEventOnly
Recoverable
} derive(Eq)
///|
/// A bounded retry policy for integrations that can change context between attempts.
pub(all) struct RetryPolicy {
max_attempts : Int
mode : RetryMode
} derive(Eq)
///|
/// Creates a retry policy. The initial attempt is always counted.
pub fn RetryPolicy::new(max_attempts : Int, mode : RetryMode) -> RetryPolicy {
{ max_attempts: if max_attempts < 1 { 1 } else { max_attempts }, mode }
}
///|
/// A single observable attempt made by a policy-driven dispatch.
pub(all) struct RetryAttempt[E] {
attempt : Int
event : E
accepted : Bool
error : TransitionError?
}
///|
/// Result of a bounded retry operation, including every failed attempt.
pub(all) struct RetryReport[E] {
attempts : Array[RetryAttempt[E]]
accepted : Bool
exhausted : Bool
final_error : TransitionError?
}
///|
fn retryable(mode : RetryMode, error : TransitionError) -> Bool {
match mode {
Never => false
GuardOnly => error is GuardRejected
UnknownEventOnly => error is EventNotHandledInCurrentState
Recoverable =>
error is GuardRejected || error is EventNotHandledInCurrentState
}
}
///|
/// Sends one event repeatedly until it succeeds or the policy is exhausted.
///
/// The engine never sleeps or hides failures: callers can update external
/// context between calls, while the report remains deterministic and auditable.
pub fn[S : Hash + Eq, E : Hash + Eq, Ctx] Engine::try_send_with_retry(
self : Engine[S, E, Ctx],
event : E,
policy : RetryPolicy,
) -> RetryReport[E] {
let attempts = []
let mut attempt = 1
let mut accepted = false
let mut exhausted = false
let mut final_error : TransitionError? = None
while attempt <= policy.max_attempts && !accepted {
match self.try_send(event) {
Ok(_) => {
attempts.push({ attempt, event, accepted: true, error: None })
accepted = true
}
Err(error) => {
attempts.push({ attempt, event, accepted: false, error: Some(error) })
final_error = Some(error)
if !retryable(policy.mode, error) || attempt == policy.max_attempts {
exhausted = true
}
}
}
attempt += 1
}
{ attempts, accepted, exhausted, final_error }
}
///|
/// A budgeted event dispatch stops after too many rejected attempts.
pub(all) struct BudgetReport[E] {
outcomes : Array[DispatchOutcome[E]]
successful : Int
rejected : Int
processed : Int
stopped : Bool
}
///|
/// Dispatches events in order while protecting a workflow from an error storm.
/// A zero budget means that the first rejection stops the batch.
pub fn[S : Hash + Eq, E : Hash + Eq, Ctx] Engine::try_send_with_budget(
self : Engine[S, E, Ctx],
events : Array[E],
rejection_budget : Int,
) -> BudgetReport[E] {
let outcomes = []
let mut successful = 0
let mut rejected = 0
let mut processed = 0
let mut stopped = false
for event in events {
if stopped {
continue
}
match self.try_send(event) {
Ok(_) => {
successful += 1
processed += 1
outcomes.push({ event, success: true, error: None })
}
Err(error) => {
rejected += 1
processed += 1
outcomes.push({ event, success: false, error: Some(error) })
if rejected > rejection_budget {
stopped = true
}
}
}
}
{ outcomes, successful, rejected, processed, stopped }
}
///|
/// Returns the number of successful transitions represented by a history.
pub fn[S, E] successful_transitions(
history : Array[TransitionRecord[S, E]],
) -> Int {
history.length()
}
///|
/// A compact operational summary suitable for dashboards and acceptance logs.
pub(all) struct HistorySummary {
transitions : Int
guarded_transitions : Int
action_transitions : Int
distinct_states : Int
first_state : String?
last_state : String?
}
///|
/// Summarizes a string-labelled workflow history without storing extra runtime state.
pub fn history_summary(
history : Array[TransitionRecord[String, String]],
) -> HistorySummary {
let seen = Map([])
let mut guarded = 0
let mut actions = 0
for record in history {
seen.set(record.from, true)
seen.set(record.to, true)
if record.used_guard {
guarded += 1
}
if record.used_action {
actions += 1
}
}
let first = history.get(0)
let last = history.get(history.length() - 1)
{
transitions: history.length(),
guarded_transitions: guarded,
action_transitions: actions,
distinct_states: seen.length(),
first_state: first_history_state(first),
last_state: last_history_state(last),
}
}
///|
fn first_history_state(record : TransitionRecord[String, String]?) -> String? {
match record {
Some(item) => Some(item.from)
None => None
}
}
///|
fn last_history_state(record : TransitionRecord[String, String]?) -> String? {
match record {
Some(item) => Some(item.to)
None => None
}
}