use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::time::Duration;
use stillwater::effect::prelude::*;
use stillwater::{RetryPolicy, TimeoutError};
async fn example_basic_retry() {
println!("\n=== Example 1: Basic Retry ===");
let attempts = Arc::new(AtomicU32::new(0));
let effect = retry(
{
let attempts = attempts.clone();
move || {
let attempts = attempts.clone();
from_async(move |_: &()| {
let attempts = attempts.clone();
async move {
let n = attempts.fetch_add(1, Ordering::SeqCst);
println!(" Attempt {}", n + 1);
if n < 2 {
Err("transient failure")
} else {
Ok("success!")
}
}
})
}
},
RetryPolicy::exponential(Duration::from_millis(100)).with_max_retries(5),
);
let result = effect.run_standalone().await;
match result {
Ok(success) => {
let attempts = success.attempts;
let value = success.into_value();
println!("Success after {} attempts: {}", attempts, value);
}
Err(exhausted) => {
println!(
"Failed after {} attempts: {}",
exhausted.attempts, exhausted.final_error
);
}
}
}
async fn example_backoff_strategies() {
println!("\n=== Example 2: Backoff Strategies ===");
let constant = RetryPolicy::constant(Duration::from_millis(100)).with_max_retries(5);
println!("Constant delays:");
for i in 0..5 {
if let Some(d) = constant.delay_for_attempt(i) {
println!(" Attempt {}: {:?}", i + 1, d);
}
}
let linear = RetryPolicy::linear(Duration::from_millis(100)).with_max_retries(5);
println!("\nLinear delays:");
for i in 0..5 {
if let Some(d) = linear.delay_for_attempt(i) {
println!(" Attempt {}: {:?}", i + 1, d);
}
}
let exponential = RetryPolicy::exponential(Duration::from_millis(100)).with_max_retries(5);
println!("\nExponential delays:");
for i in 0..5 {
if let Some(d) = exponential.delay_for_attempt(i) {
println!(" Attempt {}: {:?}", i + 1, d);
}
}
let fibonacci = RetryPolicy::fibonacci(Duration::from_millis(100)).with_max_retries(5);
println!("\nFibonacci delays:");
for i in 0..5 {
if let Some(d) = fibonacci.delay_for_attempt(i) {
println!(" Attempt {}: {:?}", i + 1, d);
}
}
}
async fn example_conditional_retry() {
println!("\n=== Example 3: Conditional Retry ===");
#[derive(Debug, Clone, PartialEq)]
enum AppError {
Transient(String),
Permanent(String),
}
let attempts = Arc::new(AtomicU32::new(0));
let effect = retry_if(
{
let attempts = attempts.clone();
move || {
let attempts = attempts.clone();
from_async(move |_: &()| {
let attempts = attempts.clone();
async move {
attempts.fetch_add(1, Ordering::SeqCst);
println!(" Attempting...");
Err::<(), _>(AppError::Permanent("invalid credentials".to_string()))
}
})
}
},
RetryPolicy::constant(Duration::from_millis(100)).with_max_retries(5),
|err| matches!(err, AppError::Transient(_)),
);
let result = effect.run_standalone().await;
println!("Permanent error (no retries): {:?}", result.unwrap_err());
println!("Total attempts: {}", attempts.load(Ordering::SeqCst));
attempts.store(0, Ordering::SeqCst);
let effect = retry_if(
{
let attempts = attempts.clone();
move || {
let attempts = attempts.clone();
from_async(move |_: &()| {
let attempts = attempts.clone();
async move {
let n = attempts.fetch_add(1, Ordering::SeqCst);
println!(" Attempt {}", n + 1);
if n < 2 {
Err(AppError::Transient("connection timeout".to_string()))
} else {
Ok("connected!")
}
}
})
}
},
RetryPolicy::constant(Duration::from_millis(50)).with_max_retries(5),
|err| matches!(err, AppError::Transient(_)),
);
let result = effect.run_standalone().await;
println!("\nTransient errors then success: {:?}", result.unwrap());
println!("Total attempts: {}", attempts.load(Ordering::SeqCst));
}
async fn example_retry_with_hooks() {
println!("\n=== Example 4: Retry with Hooks ===");
let attempts = Arc::new(AtomicU32::new(0));
let effect = retry_with_hooks(
{
let attempts = attempts.clone();
move || {
let attempts = attempts.clone();
from_async(move |_: &()| {
let attempts = attempts.clone();
async move {
let n = attempts.fetch_add(1, Ordering::SeqCst);
if n < 3 {
Err(format!("error on attempt {}", n + 1))
} else {
Ok("finally succeeded!")
}
}
})
}
},
RetryPolicy::exponential(Duration::from_millis(50)).with_max_retries(5),
|event| {
println!(
" [HOOK] Attempt {} failed with: {:?}",
event.attempt, event.error
);
if let Some(delay) = event.next_delay {
println!(" Waiting {:?} before retry...", delay);
} else {
println!(" No more retries!");
}
println!(" Total elapsed: {:?}", event.elapsed);
},
);
let result = effect.run_standalone().await;
match result {
Ok(success) => {
let attempts = success.attempts;
let value = success.into_value();
println!("\nSuccess after {} attempts: {}", attempts, value);
}
Err(exhausted) => {
println!(
"\nFailed after {} attempts: {}",
exhausted.attempts, exhausted.final_error
);
}
}
}
async fn example_timeout() {
println!("\n=== Example 5: Timeout ===");
let slow_effect = with_timeout(
from_async(|_: &()| async {
println!(" Starting slow operation...");
tokio::time::sleep(Duration::from_secs(10)).await;
Ok::<_, String>("done")
}),
Duration::from_millis(100),
);
println!("Running with 100ms timeout:");
match slow_effect.run_standalone().await {
Ok(value) => println!(" Completed: {}", value),
Err(TimeoutError::Timeout { duration }) => {
println!(" Timed out after {:?}", duration);
}
Err(TimeoutError::Inner(e)) => println!(" Inner error: {}", e),
}
let fast_effect = with_timeout(
from_async(|_: &()| async {
println!("\n Starting fast operation...");
tokio::time::sleep(Duration::from_millis(10)).await;
Ok::<_, String>("done quickly!")
}),
Duration::from_millis(100),
);
println!("Running with 100ms timeout:");
match fast_effect.run_standalone().await {
Ok(value) => println!(" Completed: {}", value),
Err(TimeoutError::Timeout { duration }) => {
println!(" Timed out after {:?}", duration);
}
Err(TimeoutError::Inner(e)) => println!(" Inner error: {}", e),
}
}
async fn example_retry_with_timeout() {
println!("\n=== Example 6: Retry with Timeout ===");
let attempts = Arc::new(AtomicU32::new(0));
let effect = retry(
{
let attempts = attempts.clone();
move || {
let attempts = attempts.clone();
with_timeout(
from_async(move |_: &()| {
let attempts = attempts.clone();
async move {
let n = attempts.fetch_add(1, Ordering::SeqCst);
println!(" Attempt {} starting...", n + 1);
if n < 2 {
tokio::time::sleep(Duration::from_millis(500)).await;
}
tokio::time::sleep(Duration::from_millis(10)).await;
Ok::<_, String>("connected!")
}
}),
Duration::from_millis(100),
)
.map_err(|e| format!("{}", e))
}
},
RetryPolicy::constant(Duration::from_millis(50)).with_max_retries(5),
);
let result = effect.run_standalone().await;
match result {
Ok(success) => {
let attempts = success.attempts;
let value = success.into_value();
println!("\nSuccess after {} attempts: {}", attempts, value);
}
Err(exhausted) => {
println!(
"\nFailed after {} attempts: {}",
exhausted.attempts, exhausted.final_error
);
}
}
}
async fn example_max_delay() {
println!("\n=== Example 7: Max Delay Cap ===");
let policy = RetryPolicy::exponential(Duration::from_millis(100))
.with_max_retries(10)
.with_max_delay(Duration::from_millis(500));
println!("Exponential backoff with 500ms cap:");
for i in 0..10 {
if let Some(d) = policy.delay_for_attempt(i) {
println!(" Attempt {}: {:?}", i + 1, d);
}
}
}
async fn example_http_pattern() {
println!("\n=== Example 8: HTTP Client Pattern ===");
#[derive(Debug, Clone)]
enum HttpError {
Timeout,
ServerError(u16),
ClientError(u16),
}
impl std::fmt::Display for HttpError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HttpError::Timeout => write!(f, "request timed out"),
HttpError::ServerError(code) => write!(f, "server error: {}", code),
HttpError::ClientError(code) => write!(f, "client error: {}", code),
}
}
}
fn is_retryable(err: &HttpError) -> bool {
matches!(err, HttpError::Timeout | HttpError::ServerError(_))
}
let attempts = Arc::new(AtomicU32::new(0));
let effect = retry_if(
{
let attempts = attempts.clone();
move || {
let attempts = attempts.clone();
from_async(move |_: &()| {
let attempts = attempts.clone();
async move {
let n = attempts.fetch_add(1, Ordering::SeqCst);
println!(" HTTP request attempt {}", n + 1);
match n {
0 => Err(HttpError::ServerError(503)), 1 => Err(HttpError::Timeout),
_ => Ok("{ \"status\": \"ok\" }"),
}
}
})
}
},
RetryPolicy::exponential(Duration::from_millis(100))
.with_max_retries(5)
.with_max_delay(Duration::from_secs(2)),
is_retryable,
);
let result = effect.run_standalone().await;
match result {
Ok(body) => println!("\nResponse: {}", body),
Err(e) => println!("\nRequest failed: {}", e),
}
println!("\n--- Client Error (should NOT retry) ---");
let attempts = Arc::new(AtomicU32::new(0));
let effect = retry_if(
{
let attempts = attempts.clone();
move || {
let attempts = attempts.clone();
from_async(move |_: &()| {
let attempts = attempts.clone();
async move {
attempts.fetch_add(1, Ordering::SeqCst);
println!(" HTTP request attempt");
Err::<&str, _>(HttpError::ClientError(400))
}
})
}
},
RetryPolicy::exponential(Duration::from_millis(100))
.with_max_retries(5)
.with_max_delay(Duration::from_secs(2)),
is_retryable,
);
let result = effect.run_standalone().await;
match result {
Ok(body) => println!("\nResponse: {}", body),
Err(e) => println!("\nRequest failed (no retries for client error): {}", e),
}
println!("Total attempts: {}", attempts.load(Ordering::SeqCst));
}
#[tokio::main]
async fn main() {
println!("======================================");
println!(" Retry Patterns Example ");
println!("======================================");
example_basic_retry().await;
example_backoff_strategies().await;
example_conditional_retry().await;
example_retry_with_hooks().await;
example_timeout().await;
example_retry_with_timeout().await;
example_max_delay().await;
example_http_pattern().await;
println!("\n======================================");
println!(" Examples Complete ");
println!("======================================");
}