| Crates.io | prism3-retry |
| lib.rs | prism3-retry |
| version | 0.1.0 |
| created_at | 2025-10-19 04:05:08.387144+00 |
| updated_at | 2025-10-19 04:05:08.387144+00 |
| description | Retry module, providing a feature-complete, type-safe retry management system with support for multiple delay strategies and event listeners |
| homepage | https://github.com/3-prism/prism3-rust-retry |
| repository | https://github.com/3-prism/prism3-rust-retry |
| max_upload_size | |
| id | 1889902 |
| size | 501,527 |
A feature-complete, type-safe retry management system for Rust. This module provides flexible retry mechanisms with multiple delay strategies, event listeners, and configuration management.
Inspired by: The API design is inspired by Java's Failsafe library, but with a completely different implementation approach. This Rust version leverages generics and Rust's zero-cost abstractions for significantly better performance compared to Java's polymorphism-based implementation.
Prism3 Retry is designed to handle transient failures in distributed systems and unreliable operations. It offers a comprehensive retry framework with support for various backoff strategies, conditional retry logic, and detailed event monitoring.
retry/
├── mod.rs # Module entry, exports public API
├── builder.rs # RetryBuilder struct (core retry builder)
├── config.rs # RetryConfig trait (configuration abstraction)
├── default_config.rs # DefaultRetryConfig (Config-based implementation)
├── simple_config.rs # SimpleRetryConfig (simple in-memory implementation)
├── delay_strategy.rs # RetryDelayStrategy enum
├── events.rs # Event type definitions
├── error.rs # Error type definitions
└── executor.rs # RetryExecutor executor
graph TB
A[RetryBuilder <T, C>] --> B[RetryExecutor <T, C>]
A --> C[RetryConfig Trait]
C --> D[DefaultRetryConfig]
C --> E[SimpleRetryConfig]
C --> F[Custom Config...]
D --> G[prism3_config::Config]
B --> H[RetryDelayStrategy]
B --> I[Event Listeners]
B --> J[RetryError]
K[Operation] --> B
B --> L[Result <T, RetryError>]
M[RetryEvent] --> I
N[SuccessEvent] --> I
O[FailureEvent] --> I
P[AbortEvent] --> I
style A fill:#e1f5ff
style B fill:#e1f5ff
style C fill:#fff4e1
Add this to your Cargo.toml:
[dependencies]
prism3-retry = "0.1.0"
use prism3_retry::RetryBuilder;
use std::time::Duration;
// Using default config - generic parameter automatically inferred as DefaultRetryConfig
let executor = RetryBuilder::new()
.set_max_attempts(3)
.set_fixed_delay_strategy(Duration::from_secs(1))
.build();
let result = executor.run(|| {
// Your operation
Ok("SUCCESS".to_string())
});
use prism3_retry::{DefaultRetryBuilder, DefaultRetryExecutor};
// More explicit types
let builder: DefaultRetryBuilder<String> = RetryBuilder::new();
let executor: DefaultRetryExecutor<String> = builder.build();
When you need to load configuration from different sources:
use prism3_retry::{RetryBuilder, RetryConfig, RetryDelayStrategy};
use std::time::Duration;
// 1. Implement custom configuration type
struct FileBasedConfig {
max_attempts: u32,
delay: Duration,
}
impl RetryConfig for FileBasedConfig {
fn max_attempts(&self) -> u32 {
self.max_attempts
}
fn set_max_attempts(&mut self, max_attempts: u32) -> &mut Self {
self.max_attempts = max_attempts;
self
}
fn max_duration(&self) -> Option<Duration> {
None
}
fn set_max_duration(&mut self, _: Option<Duration>) -> &mut Self {
self
}
fn operation_timeout(&self) -> Option<Duration> {
None
}
fn set_operation_timeout(&mut self, _: Option<Duration>) -> &mut Self {
self
}
fn delay_strategy(&self) -> RetryDelayStrategy {
RetryDelayStrategy::fixed(self.delay)
}
fn set_delay_strategy(&mut self, strategy: RetryDelayStrategy) -> &mut Self {
if let RetryDelayStrategy::Fixed { delay } = strategy {
self.delay = delay;
}
self
}
fn jitter_factor(&self) -> f64 {
0.0
}
fn set_jitter_factor(&mut self, _: f64) -> &mut Self {
self
}
}
// 2. Use custom configuration
let file_config = FileBasedConfig {
max_attempts: 5,
delay: Duration::from_secs(2),
};
let executor = RetryBuilder::with_config(file_config)
.failed_on_result("RETRY".to_string())
.build();
Example of dynamically loading configuration from Redis:
use prism3_retry::{RetryBuilder, RetryConfig, RetryDelayStrategy};
struct RedisRetryConfig {
client: redis::Client,
key_prefix: String,
}
impl RedisRetryConfig {
fn connect(url: &str) -> Self {
Self {
client: redis::Client::open(url).unwrap(),
key_prefix: "retry:".to_string(),
}
}
}
impl RetryConfig for RedisRetryConfig {
fn max_attempts(&self) -> u32 {
// Read from Redis
let mut conn = self.client.get_connection().unwrap();
let key = format!("{}max_attempts", self.key_prefix);
conn.get(&key).unwrap_or(5)
}
fn set_max_attempts(&mut self, value: u32) -> &mut Self {
// Save to Redis
let mut conn = self.client.get_connection().unwrap();
let key = format!("{}max_attempts", self.key_prefix);
let _: () = conn.set(&key, value).unwrap();
self
}
// ... implement other required methods ...
}
// Usage
let redis_config = RedisRetryConfig::connect("redis://localhost");
let executor = RetryBuilder::with_config(redis_config)
.build();
use prism3_retry::{RetryBuilder, RetryResult};
use std::time::Duration;
// Create retry executor
let executor = RetryBuilder::<String>::new()
.set_max_attempts(3)
.set_fixed_delay_strategy(Duration::from_secs(1))
.build();
// Execute potentially failing operation
let result: RetryResult<String> = executor.run(|| {
// Simulate potentially failing operation
if rand::random::<f64>() < 0.7 {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Simulated failure"
).into())
} else {
Ok("Success".to_string())
}
});
match result {
Ok(value) => println!("Operation succeeded: {}", value),
Err(e) => println!("Operation failed: {}", e),
}
use prism3_retry::RetryResult;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct ApiResponse {
status: u16,
message: String,
}
let executor = RetryBuilder::<ApiResponse>::new()
.set_max_attempts(5)
.set_exponential_backoff_strategy(
Duration::from_millis(100),
Duration::from_secs(10),
2.0,
)
.failed_on_results(vec![
ApiResponse { status: 503, message: "Service Unavailable".to_string() },
ApiResponse { status: 429, message: "Too Many Requests".to_string() },
])
.build();
let result: RetryResult<ApiResponse> = executor.run(|| {
// API call logic
Ok(ApiResponse { status: 200, message: "Success".to_string() })
});
#[derive(Debug)]
struct NetworkError(String);
impl std::fmt::Display for NetworkError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "NetworkError: {}", self.0)
}
}
impl std::error::Error for NetworkError {}
let executor = RetryBuilder::<String>::new()
.set_max_attempts(3)
.set_random_delay_strategy(Duration::from_millis(100), Duration::from_millis(500))
.failed_on_error::<NetworkError>()
.abort_on_error::<std::io::Error>() // Abort immediately on IO errors
.build();
use std::sync::{Arc, Mutex};
let retry_count = Arc::new(Mutex::new(0));
let retry_count_clone = retry_count.clone();
let executor = RetryBuilder::<String>::new()
.set_max_attempts(5)
.set_fixed_delay_strategy(Duration::from_millis(100))
.on_retry(move |event| {
let mut count = retry_count_clone.lock().unwrap();
*count += 1;
println!("Retry #{}, reason: {:?}", event.attempt_count(), event.reason());
})
.on_success(|event| {
println!("Operation succeeded, duration: {:?}", event.duration());
})
.on_failure(|event| {
println!("Operation ultimately failed, total duration: {:?}", event.duration());
})
.build();
use prism3_retry::RetryDelayStrategy;
// Exponential backoff strategy
let strategy = RetryDelayStrategy::ExponentialBackoff {
initial_delay: Duration::from_millis(100),
max_delay: Duration::from_secs(10),
multiplier: 2.0,
};
let executor = RetryBuilder::<String>::new()
.set_delay_strategy(strategy)
.set_jitter_factor(0.1) // Add 10% jitter
.build();
let executor = RetryBuilder::<ApiResponse>::new()
.set_max_attempts(5)
.failed_on_results_if(|response| response.status >= 500)
.abort_on_results_if(|response| response.status == 403) // Don't retry permission errors
.build();
// Synchronous version uses post-check mechanism, checking timeout after operation completes
let executor = RetryBuilder::<String>::new()
.set_max_attempts(3)
.set_operation_timeout(Some(Duration::from_secs(5))) // Max 5 seconds per operation
.set_max_duration(Some(Duration::from_secs(30))) // Max 30 seconds total
.build();
let result = executor.run(|| {
// Synchronous operation
std::thread::sleep(Duration::from_secs(2));
Ok("Done".to_string())
});
// Async version uses tokio::time::timeout for true timeout interruption
let executor = RetryBuilder::<String>::new()
.set_max_attempts(3)
.set_operation_timeout(Some(Duration::from_secs(5))) // Max 5 seconds per operation
.set_exponential_backoff_strategy(
Duration::from_millis(100),
Duration::from_secs(10),
2.0,
)
.build();
let result = executor.run_async(|| async {
// Async HTTP request, will timeout after 5 seconds
let response = reqwest::get("https://api.example.com/data").await?;
let text = response.text().await?;
Ok(text)
}).await;
let executor = RetryBuilder::<String>::new()
.set_max_attempts(5) // Max 5 attempts
.set_operation_timeout(Some(Duration::from_secs(10))) // Max 10 seconds per operation
.set_max_duration(Some(Duration::from_secs(60))) // Max 60 seconds total
.set_fixed_delay_strategy(Duration::from_secs(2)) // 2 seconds delay between retries
.build();
// - operation_timeout: Single operation max 10 seconds
// - max_duration: Entire retry process (including all retries + delays) max 60 seconds
// - delay: Wait 2 seconds between each retry
use prism3_config::Config;
use prism3_retry::{RetryBuilder, DefaultRetryConfig};
// Load retry configuration from config file
let mut config = Config::new();
config.set("retry.max_attempts", 5u32).unwrap();
config.set("retry.max_duration_millis", 30000u64).unwrap();
config.set("retry.operation_timeout_millis", 5000u64).unwrap();
config.set("retry.delay_strategy", "EXPONENTIAL_BACKOFF").unwrap();
config.set("retry.backoff_initial_delay_millis", 100u64).unwrap();
config.set("retry.backoff_max_delay_millis", 10000u64).unwrap();
config.set("retry.backoff_multiplier", 2.0f64).unwrap();
let retry_config = DefaultRetryConfig::with_config(config);
let executor = RetryBuilder::with_config(retry_config).build();
use prism3_retry::{RetryBuilder, SimpleRetryConfig, RetryDelayStrategy};
use std::time::Duration;
let mut simple_config = SimpleRetryConfig::new();
simple_config
.set_max_attempts(3)
.set_max_duration(Some(Duration::from_secs(30)))
.set_delay_strategy(RetryDelayStrategy::fixed(Duration::from_secs(1)));
let executor = RetryBuilder::with_config(simple_config).build();
Retry builder providing fluent API for configuring retry strategy.
pub struct RetryBuilder<T, C: RetryConfig = DefaultRetryConfig> {
// T: Operation return type
// C: Configuration type, defaults to DefaultRetryConfig
}
Type Alias:
// Builder using default configuration
pub type DefaultRetryBuilder<T> = RetryBuilder<T, DefaultRetryConfig>;
Retry executor responsible for executing retry logic.
pub struct RetryExecutor<T, C: RetryConfig = DefaultRetryConfig> {
// T: Operation return type
// C: Configuration type, defaults to DefaultRetryConfig
}
Type Alias:
// Executor using default configuration
pub type DefaultRetryExecutor<T> = RetryExecutor<T, DefaultRetryConfig>;
Configuration abstraction trait defining the retry configuration interface.
pub trait RetryConfig {
fn max_attempts(&self) -> u32;
fn set_max_attempts(&mut self, max_attempts: u32) -> &mut Self;
fn max_duration(&self) -> Option<Duration>;
fn set_max_duration(&mut self, max_duration: Option<Duration>) -> &mut Self;
fn operation_timeout(&self) -> Option<Duration>;
fn set_operation_timeout(&mut self, timeout: Option<Duration>) -> &mut Self;
fn delay_strategy(&self) -> RetryDelayStrategy;
fn set_delay_strategy(&mut self, delay_strategy: RetryDelayStrategy) -> &mut Self;
fn jitter_factor(&self) -> f64;
fn set_jitter_factor(&mut self, jitter_factor: f64) -> &mut Self;
// ... convenience methods ...
}
Built-in Implementations:
DefaultRetryConfig - Based on Config system, supports configuration file loadingSimpleRetryConfig - Simple in-memory implementation with direct field storageDelay strategy enum supporting multiple delay patterns.
pub enum RetryDelayStrategy {
None,
Fixed { delay: Duration },
Random { min_delay: Duration, max_delay: Duration },
ExponentialBackoff { initial_delay: Duration, max_delay: Duration, multiplier: f64 },
}
Error type for the retry module.
pub enum RetryError {
MaxAttemptsExceeded { attempts: u32, max_attempts: u32 },
MaxDurationExceeded { duration: Duration, max_duration: Duration },
OperationTimeout { duration: Duration, timeout: Duration },
Aborted { reason: String },
ConfigError { message: String },
DelayStrategyError { message: String },
ExecutionError { source: Box<dyn Error + Send + Sync> },
Other { message: String },
}
Type alias for retry operation results.
pub type RetryResult<T> = Result<T, RetryError>;
This type alias simplifies function signatures and makes the code more readable.
RetryEvent<T> - Retry eventSuccessEvent<T> - Success eventFailureEvent<T> - Failure eventAbortEvent<T> - Abort eventpub type RetryEventListener<T> = Box<dyn Fn(RetryEvent<T>) + Send + Sync + 'static>;
pub type SuccessEventListener<T> = Box<dyn Fn(SuccessEvent<T>) + Send + Sync + 'static>;
pub type FailureEventListener<T> = Box<dyn Fn(FailureEvent<T>) + Send + Sync + 'static>;
pub type AbortEventListener<T> = Box<dyn Fn(AbortEvent<T>) + Send + Sync + 'static>;
| Configuration Type | Use Case | Advantages | Disadvantages |
|---|---|---|---|
DefaultRetryConfig |
Need config files, integrate Config system | Supports config persistence, dynamic loading | Requires Config module dependency |
SimpleRetryConfig |
Simple scenarios, pure code configuration | Lightweight, direct, high performance | No config file support |
| Custom Implementation | Special config sources (Redis, database, etc.) | Fully flexible, customizable | Need to implement trait yourself |
| Configuration Key | Type | Default | Description |
|---|---|---|---|
retry.max_attempts |
u32 | 5 | Maximum retry attempts |
retry.max_duration_millis |
u64 | 0 | Maximum duration (milliseconds), 0 means unlimited |
retry.operation_timeout_millis |
u64 | 0 | Single operation timeout (milliseconds), 0 means unlimited |
retry.delay_strategy |
String | "EXPONENTIAL_BACKOFF" | Delay strategy |
retry.fixed_delay_millis |
u64 | 1000 | Fixed delay time (milliseconds) |
retry.random_min_delay_millis |
u64 | 1000 | Random delay minimum (milliseconds) |
retry.random_max_delay_millis |
u64 | 10000 | Random delay maximum (milliseconds) |
retry.backoff_initial_delay_millis |
u64 | 1000 | Exponential backoff initial delay (milliseconds) |
retry.backoff_max_delay_millis |
u64 | 60000 | Exponential backoff maximum delay (milliseconds) |
retry.backoff_multiplier |
f64 | 2.0 | Exponential backoff multiplier |
retry.jitter_factor |
f64 | 0.0 | Jitter factor (0.0-1.0) |
| Strategy Name | Description | Parameters |
|---|---|---|
NONE |
No delay | None |
FIXED |
Fixed delay | retry.fixed_delay_millis |
RANDOM |
Random delay | retry.random_min_delay_millis, retry.random_max_delay_millis |
EXPONENTIAL_BACKOFF |
Exponential backoff | retry.backoff_initial_delay_millis, retry.backoff_max_delay_millis, retry.backoff_multiplier |
After generic refactoring, RetryBuilder and RetryExecutor now support custom configuration types:
pub struct RetryBuilder<T, C: RetryConfig = DefaultRetryConfig>
pub struct RetryExecutor<T, C: RetryConfig = DefaultRetryConfig>
T: Return type of the operationC: Retry configuration type, must implement RetryConfig trait, defaults to DefaultRetryConfigRust compiler automatically infers generic parameters:
// Compiler infers as RetryBuilder<String, DefaultRetryConfig>
let builder = RetryBuilder::new();
// Explicit configuration type specification
let builder = RetryBuilder::<String, FileBasedConfig>::with_config(file_config);
// Or use type inference
let file_config = FileBasedConfig::new();
let builder = RetryBuilder::with_config(file_config); // C is automatically inferred
Performance advantage of generic version:
// Compiled code
// RetryBuilder<String, DefaultRetryConfig>::set_max_attempts
// Will be completely inlined and optimized, performance equivalent to direct field access
// Generic version - direct memory access
let attempts = config.max_attempts(); // mov eax, [rdi + offset]
// VS Trait Object version - dynamic dispatch
// mov rax, [rdi] // Load vtable
// call [rax + offset] // Indirect call
Performance Comparison:
Rust compiler automatically infers generic parameters:
// Compiler infers as RetryBuilder<String, DefaultRetryConfig>
let builder = RetryBuilder::new();
// If you need to explicitly specify config type
let builder = RetryBuilder::<String, FileBasedConfig>::with_config(file_config);
// Or use type inference
let file_config = FileBasedConfig::new();
let builder = RetryBuilder::with_config(file_config); // C is automatically inferred
Refactoring is completely backward compatible, all existing code requires no changes:
// ✅ This code is identical before and after refactoring
let executor = RetryBuilder::new()
.set_max_attempts(3)
.build();
let result = executor.run(|| {
Ok("SUCCESS".to_string())
});
For 90% of scenarios, use default configuration:
let executor = RetryBuilder::new()
.set_max_attempts(3)
.build();
use prism3_retry::DefaultRetryExecutor;
fn create_executor() -> DefaultRetryExecutor<String> {
RetryBuilder::new()
.set_max_attempts(3)
.build()
}
use prism3_retry::{RetryBuilder, RetryExecutor};
struct MyConfig { /* ... */ }
impl RetryConfig for MyConfig { /* ... */ }
fn create_custom_executor(config: MyConfig) -> RetryExecutor<String, MyConfig> {
RetryBuilder::with_config(config)
.set_max_attempts(5)
.build()
}
use prism3_retry::{RetryResult, RetryError};
// Good practice: explicitly handle various error types
let result: RetryResult<String> = executor.run(|| {
// Your operation
Ok("Success".to_string())
});
match result {
Ok(value) => {
// Handle success case
}
Err(RetryError::MaxAttemptsExceeded { attempts, max_attempts }) => {
// Log retry failure
log::warn!("Operation failed after {} attempts", max_attempts);
}
Err(RetryError::Aborted { reason }) => {
// Handle abort case
log::info!("Operation aborted: {}", reason);
}
Err(RetryError::ExecutionError { source }) => {
// Handle execution error
log::error!("Execution error: {}", source);
}
Err(e) => {
// Handle other errors
log::error!("Unknown error: {}", e);
}
}
// Use appropriate delay strategy
let executor = RetryBuilder::<String>::new()
.set_max_attempts(3)
.set_exponential_backoff_strategy(
Duration::from_millis(100), // Initial delay not too long
Duration::from_secs(5), // Reasonable max delay
2.0, // Moderate multiplier
)
.set_jitter_factor(0.1) // Add slight jitter to avoid thundering herd
.build();
let executor = RetryBuilder::<String>::new()
.set_max_attempts(5)
.on_retry(|event| {
// Log retry events
log::warn!(
"Retry #{}, reason: {:?}, duration: {:?}",
event.attempt_count(),
event.reason(),
event.duration()
);
})
.on_success(|event| {
// Log success events
log::info!(
"Operation succeeded, total duration: {:?}, retry count: {}",
event.duration(),
event.attempt_count()
);
})
.on_failure(|event| {
// Log failure events
log::error!(
"Operation ultimately failed, total duration: {:?}, retry count: {}",
event.duration(),
event.attempt_count()
);
})
.build();
// Set reasonable timeout values
let executor = RetryBuilder::<String>::new()
.set_max_attempts(3)
.set_max_duration(Some(Duration::from_secs(30))) // Total timeout
.set_fixed_delay_strategy(Duration::from_secs(1))
.build();
The module includes a comprehensive test suite:
Run tests:
cargo test -p prism3-retry
A: No. Thanks to default generic parameters, all existing code will work normally.
A: When you need to:
A: No. Generics are zero-cost abstractions at compile time. Different configuration types generate different code, but performance is optimal for all.
A:
DefaultRetryConfig: Based on Config system, supports config file loading and persistence, suitable for scenarios requiring configuration managementSimpleRetryConfig: Simple in-memory implementation, all fields directly stored, suitable for pure code configuration scenariosA: You can choose via generic parameters at compile time:
#[cfg(feature = "redis-config")]
type MyRetryExecutor = RetryExecutor<String, RedisRetryConfig>;
#[cfg(not(feature = "redis-config"))]
type MyRetryExecutor = DefaultRetryExecutor<String>;
A: Choose based on your requirements:
Use DefaultRetryConfig when:
Use SimpleRetryConfig when:
Implement custom RetryConfig when:
Clone + PartialEq + Eq + Hash + Send + Sync + 'staticRetryConfig traitError + Send + Sync + 'staticThe retry module provides the following key advantages:
Performance Comparison:
Copyright (c) 2025 3-Prism Co. Ltd. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
See LICENSE for the full license text.
Contributions are welcome! Please feel free to submit a Pull Request.
Hu Haixing - 3-Prism Co. Ltd.
For more information about the Prism3 ecosystem, visit our GitHub homepage.