fabro/lib/components/fabro-workflow/src/retry.rs
2026-07-23 17:59:34 -04:00

190 lines
5.9 KiB
Rust

use std::time::Duration;
use fabro_core::retry::{BackoffPolicy, RetryPolicy};
use fabro_graphviz::graph::types::{Graph as GvGraph, Node as GvNode};
const DEFAULT_BACKOFF: BackoffPolicy = BackoffPolicy {
initial_delay: Duration::from_secs(5),
factor: 2.0,
max_delay: Duration::from_mins(1),
jitter: true,
};
/// Build a retry policy from node and graph attributes.
/// If the node has a `retry_policy` attribute naming a preset, use that.
/// Otherwise, fall back to `max_retries` / graph default.
pub(crate) fn build_retry_policy(node: &GvNode, graph: &GvGraph) -> RetryPolicy {
if let Some(preset) = node.retry_policy() {
if let Some(policy) = preset_retry_policy(preset) {
return policy;
}
}
let max_retries = node
.max_retries()
.unwrap_or_else(|| graph.default_max_retries());
let max_attempts = u32::try_from(max_retries + 1).unwrap_or(1).max(1);
RetryPolicy {
max_attempts,
backoff: DEFAULT_BACKOFF,
}
}
fn preset_retry_policy(preset: &str) -> Option<RetryPolicy> {
match preset {
"none" => Some(RetryPolicy {
max_attempts: 1,
backoff: DEFAULT_BACKOFF,
}),
"standard" => Some(RetryPolicy {
max_attempts: 5,
backoff: DEFAULT_BACKOFF,
}),
"aggressive" => Some(RetryPolicy {
max_attempts: 5,
backoff: BackoffPolicy {
initial_delay: Duration::from_millis(500),
..DEFAULT_BACKOFF
},
}),
"linear" => Some(RetryPolicy {
max_attempts: 3,
backoff: BackoffPolicy {
initial_delay: Duration::from_millis(500),
factor: 1.0,
..DEFAULT_BACKOFF
},
}),
"patient" => Some(RetryPolicy {
max_attempts: 3,
backoff: BackoffPolicy {
initial_delay: Duration::from_secs(2),
factor: 3.0,
..DEFAULT_BACKOFF
},
}),
_ => None,
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use fabro_graphviz::graph::{AttrValue, Graph, Node};
use super::*;
#[test]
fn build_retry_policy_from_node() {
let mut node = Node::new("n");
node.attrs
.insert("max_retries".to_string(), AttrValue::Integer(3));
let graph = Graph::new("test");
let policy = build_retry_policy(&node, &graph);
assert_eq!(policy.max_attempts, 4);
}
#[test]
fn build_retry_policy_from_graph_default() {
let node = Node::new("n");
let mut graph = Graph::new("test");
graph
.attrs
.insert("default_max_retries".to_string(), AttrValue::Integer(2));
let policy = build_retry_policy(&node, &graph);
assert_eq!(policy.max_attempts, 3);
}
#[test]
fn build_retry_policy_no_attrs_uses_graph_default_0() {
let node = Node::new("n");
let graph = Graph::new("test");
let policy = build_retry_policy(&node, &graph);
assert_eq!(policy.max_attempts, 1);
}
#[test]
fn build_retry_policy_from_retry_policy_attr() {
let mut node = Node::new("n");
node.attrs.insert(
"retry_policy".to_string(),
AttrValue::String("aggressive".to_string()),
);
let graph = Graph::new("test");
let policy = build_retry_policy(&node, &graph);
assert_eq!(policy.max_attempts, 5);
assert_eq!(policy.backoff.initial_delay, Duration::from_millis(500));
}
#[test]
fn build_retry_policy_fallback_when_no_retry_policy_attr() {
let mut node = Node::new("n");
node.attrs
.insert("max_retries".to_string(), AttrValue::Integer(3));
let graph = Graph::new("test");
let policy = build_retry_policy(&node, &graph);
assert_eq!(policy.max_attempts, 4);
assert_eq!(policy.backoff.initial_delay, Duration::from_secs(5));
}
#[test]
fn build_retry_policy_all_presets() {
let presets = [
("none", 1u32),
("standard", 5),
("aggressive", 5),
("linear", 3),
("patient", 3),
];
let graph = Graph::new("test");
let (name, expected) = presets[0];
let mut node = Node::new("n");
node.attrs.insert(
"retry_policy".to_string(),
AttrValue::String(name.to_string()),
);
assert_eq!(build_retry_policy(&node, &graph).max_attempts, expected);
let (name, expected) = presets[1];
node.attrs.insert(
"retry_policy".to_string(),
AttrValue::String(name.to_string()),
);
assert_eq!(build_retry_policy(&node, &graph).max_attempts, expected);
let (name, expected) = presets[2];
node.attrs.insert(
"retry_policy".to_string(),
AttrValue::String(name.to_string()),
);
assert_eq!(build_retry_policy(&node, &graph).max_attempts, expected);
let (name, expected) = presets[3];
node.attrs.insert(
"retry_policy".to_string(),
AttrValue::String(name.to_string()),
);
assert_eq!(build_retry_policy(&node, &graph).max_attempts, expected);
let (name, expected) = presets[4];
node.attrs.insert(
"retry_policy".to_string(),
AttrValue::String(name.to_string()),
);
assert_eq!(build_retry_policy(&node, &graph).max_attempts, expected);
}
#[test]
fn build_retry_policy_unknown_preset_falls_back() {
let mut node = Node::new("n");
node.attrs.insert(
"retry_policy".to_string(),
AttrValue::String("unknown_preset".to_string()),
);
let graph = Graph::new("test");
let policy = build_retry_policy(&node, &graph);
assert_eq!(policy.max_attempts, 1);
}
}