#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InstanceStatus {
Unknown,
Pending,
Running,
Suspended,
Completed,
Failed,
Cancelled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SignalType {
Cancel,
Pause,
Resume,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Signal {
pub signal_type: SignalType,
pub payload: Vec<u8>,
pub checkpoint_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CheckpointResult {
pub found: bool,
pub state: Vec<u8>,
pub pending_signal: Option<Signal>,
pub custom_signal: Option<CustomSignal>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CustomSignal {
pub checkpoint_id: String,
pub payload: Vec<u8>,
}
impl CheckpointResult {
pub fn existing_state(&self) -> Option<&[u8]> {
if self.found { Some(&self.state) } else { None }
}
pub fn should_pause(&self) -> bool {
matches!(
self.pending_signal.as_ref().map(|s| s.signal_type),
Some(SignalType::Pause)
)
}
pub fn should_cancel(&self) -> bool {
matches!(
self.pending_signal.as_ref().map(|s| s.signal_type),
Some(SignalType::Cancel)
)
}
pub fn should_exit(&self) -> bool {
matches!(
self.pending_signal.as_ref().map(|s| s.signal_type),
Some(SignalType::Pause) | Some(SignalType::Cancel)
)
}
}
#[derive(Debug, Clone)]
pub struct StatusResponse {
pub found: bool,
pub status: InstanceStatus,
pub checkpoint_id: Option<String>,
pub output: Option<Vec<u8>>,
pub error: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RetryStrategy {
#[default]
ExponentialBackoff,
}
#[derive(Debug, Clone)]
pub struct RetryConfig {
pub max_retries: u32,
pub delay_ms: u64,
pub strategy: RetryStrategy,
}
impl RetryConfig {
pub fn new(max_retries: u32, delay_ms: u64, strategy: RetryStrategy) -> Self {
Self {
max_retries,
delay_ms,
strategy,
}
}
pub fn delay_for_attempt(&self, attempt: u32) -> std::time::Duration {
let multiplier = match self.strategy {
RetryStrategy::ExponentialBackoff => 2u64.saturating_pow(attempt.saturating_sub(1)),
};
std::time::Duration::from_millis(self.delay_ms.saturating_mul(multiplier))
}
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_retries: 0,
delay_ms: 1000,
strategy: RetryStrategy::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_instance_status_clone_eq() {
let status = InstanceStatus::Running;
let cloned = status;
assert_eq!(status, cloned);
}
#[test]
fn test_instance_status_debug() {
let status = InstanceStatus::Completed;
let debug_str = format!("{:?}", status);
assert!(debug_str.contains("Completed"));
}
#[test]
fn test_signal_type_clone_eq() {
let signal = SignalType::Pause;
let cloned = signal;
assert_eq!(signal, cloned);
}
#[test]
fn test_signal_creation() {
let signal = Signal {
signal_type: SignalType::Cancel,
payload: vec![1, 2, 3],
checkpoint_id: Some("checkpoint-1".to_string()),
};
assert_eq!(signal.signal_type, SignalType::Cancel);
assert_eq!(signal.payload, vec![1, 2, 3]);
assert_eq!(signal.checkpoint_id, Some("checkpoint-1".to_string()));
}
#[test]
fn test_signal_without_checkpoint() {
let signal = Signal {
signal_type: SignalType::Pause,
payload: vec![],
checkpoint_id: None,
};
assert_eq!(signal.signal_type, SignalType::Pause);
assert!(signal.payload.is_empty());
assert!(signal.checkpoint_id.is_none());
}
#[test]
fn test_signal_clone() {
let signal = Signal {
signal_type: SignalType::Resume,
payload: vec![42],
checkpoint_id: Some("cp".to_string()),
};
let cloned = signal.clone();
assert_eq!(signal, cloned);
}
#[test]
fn test_signal_debug() {
let signal = Signal {
signal_type: SignalType::Cancel,
payload: vec![1],
checkpoint_id: None,
};
let debug_str = format!("{:?}", signal);
assert!(debug_str.contains("Cancel"));
}
#[test]
fn test_checkpoint_result_existing_state_found() {
let result = CheckpointResult {
found: true,
state: vec![1, 2, 3],
pending_signal: None,
custom_signal: None,
};
assert!(result.found);
assert_eq!(result.existing_state(), Some(&[1u8, 2, 3][..]));
}
#[test]
fn test_checkpoint_result_existing_state_not_found() {
let result = CheckpointResult {
found: false,
state: vec![1, 2, 3], pending_signal: None,
custom_signal: None,
};
assert!(!result.found);
assert_eq!(result.existing_state(), None);
}
#[test]
fn test_checkpoint_result_should_pause() {
let result = CheckpointResult {
found: false,
state: vec![],
pending_signal: Some(Signal {
signal_type: SignalType::Pause,
payload: vec![],
checkpoint_id: None,
}),
custom_signal: None,
};
assert!(result.should_pause());
assert!(!result.should_cancel());
assert!(result.should_exit()); }
#[test]
fn test_checkpoint_result_should_cancel() {
let result = CheckpointResult {
found: false,
state: vec![],
pending_signal: Some(Signal {
signal_type: SignalType::Cancel,
payload: vec![],
checkpoint_id: None,
}),
custom_signal: None,
};
assert!(result.should_cancel());
assert!(!result.should_pause());
assert!(result.should_exit()); }
#[test]
fn test_checkpoint_result_should_not_exit_on_resume() {
let result = CheckpointResult {
found: false,
state: vec![],
pending_signal: Some(Signal {
signal_type: SignalType::Resume,
payload: vec![],
checkpoint_id: None,
}),
custom_signal: None,
};
assert!(!result.should_pause());
assert!(!result.should_cancel());
assert!(!result.should_exit()); }
#[test]
fn test_checkpoint_result_no_signal() {
let result = CheckpointResult {
found: true,
state: vec![42],
pending_signal: None,
custom_signal: None,
};
assert!(!result.should_pause());
assert!(!result.should_cancel());
assert!(!result.should_exit());
}
#[test]
fn test_checkpoint_result_with_custom_signal() {
let result = CheckpointResult {
found: false,
state: vec![],
pending_signal: None,
custom_signal: Some(CustomSignal {
checkpoint_id: "wait-key".to_string(),
payload: vec![10, 20, 30],
}),
};
assert!(!result.should_pause());
assert!(!result.should_cancel());
assert!(!result.should_exit());
let custom = result.custom_signal.as_ref().unwrap();
assert_eq!(custom.checkpoint_id, "wait-key");
assert_eq!(custom.payload, vec![10, 20, 30]);
}
#[test]
fn test_checkpoint_result_clone() {
let result = CheckpointResult {
found: true,
state: vec![1, 2, 3],
pending_signal: Some(Signal {
signal_type: SignalType::Pause,
payload: vec![4],
checkpoint_id: Some("cp".to_string()),
}),
custom_signal: Some(CustomSignal {
checkpoint_id: "key".to_string(),
payload: vec![5],
}),
};
let cloned = result.clone();
assert_eq!(result, cloned);
}
#[test]
fn test_checkpoint_result_empty_state() {
let result = CheckpointResult {
found: true,
state: vec![],
pending_signal: None,
custom_signal: None,
};
assert_eq!(result.existing_state(), Some(&[][..]));
}
#[test]
fn test_custom_signal_creation() {
let signal = CustomSignal {
checkpoint_id: "my-wait-key".to_string(),
payload: vec![1, 2, 3, 4],
};
assert_eq!(signal.checkpoint_id, "my-wait-key");
assert_eq!(signal.payload, vec![1, 2, 3, 4]);
}
#[test]
fn test_custom_signal_empty_payload() {
let signal = CustomSignal {
checkpoint_id: "empty-payload".to_string(),
payload: vec![],
};
assert!(signal.payload.is_empty());
}
#[test]
fn test_custom_signal_clone_eq() {
let signal = CustomSignal {
checkpoint_id: "test".to_string(),
payload: vec![42],
};
let cloned = signal.clone();
assert_eq!(signal, cloned);
}
#[test]
fn test_status_response_not_found() {
let response = StatusResponse {
found: false,
status: InstanceStatus::Unknown,
checkpoint_id: None,
output: None,
error: None,
};
assert!(!response.found);
assert_eq!(response.status, InstanceStatus::Unknown);
}
#[test]
fn test_status_response_completed() {
let response = StatusResponse {
found: true,
status: InstanceStatus::Completed,
checkpoint_id: Some("final".to_string()),
output: Some(vec![1, 2, 3]),
error: None,
};
assert!(response.found);
assert_eq!(response.status, InstanceStatus::Completed);
assert_eq!(response.output, Some(vec![1, 2, 3]));
assert!(response.error.is_none());
}
#[test]
fn test_status_response_failed() {
let response = StatusResponse {
found: true,
status: InstanceStatus::Failed,
checkpoint_id: Some("step-3".to_string()),
output: None,
error: Some("something went wrong".to_string()),
};
assert!(response.found);
assert_eq!(response.status, InstanceStatus::Failed);
assert!(response.output.is_none());
assert_eq!(response.error, Some("something went wrong".to_string()));
}
#[test]
fn test_status_response_clone() {
let response = StatusResponse {
found: true,
status: InstanceStatus::Running,
checkpoint_id: Some("cp".to_string()),
output: Some(vec![42]),
error: None,
};
let cloned = response.clone();
assert_eq!(cloned.found, response.found);
assert_eq!(cloned.status, response.status);
assert_eq!(cloned.checkpoint_id, response.checkpoint_id);
assert_eq!(cloned.output, response.output);
}
#[test]
fn test_retry_config_default() {
let config = RetryConfig::default();
assert_eq!(config.max_retries, 0);
assert_eq!(config.delay_ms, 1000);
assert_eq!(config.strategy, RetryStrategy::ExponentialBackoff);
}
#[test]
fn test_retry_config_delay_calculation() {
let config = RetryConfig::new(3, 100, RetryStrategy::ExponentialBackoff);
assert_eq!(
config.delay_for_attempt(1),
std::time::Duration::from_millis(100)
);
assert_eq!(
config.delay_for_attempt(2),
std::time::Duration::from_millis(200)
);
assert_eq!(
config.delay_for_attempt(3),
std::time::Duration::from_millis(400)
);
}
#[test]
fn test_retry_strategy_default() {
assert_eq!(RetryStrategy::default(), RetryStrategy::ExponentialBackoff);
}
#[test]
fn test_retry_config_new() {
let config = RetryConfig::new(5, 500, RetryStrategy::ExponentialBackoff);
assert_eq!(config.max_retries, 5);
assert_eq!(config.delay_ms, 500);
assert_eq!(config.strategy, RetryStrategy::ExponentialBackoff);
}
#[test]
fn test_retry_config_delay_attempt_zero() {
let config = RetryConfig::new(3, 100, RetryStrategy::ExponentialBackoff);
assert_eq!(
config.delay_for_attempt(0),
std::time::Duration::from_millis(100)
);
}
#[test]
fn test_retry_config_delay_large_attempt() {
let config = RetryConfig::new(10, 100, RetryStrategy::ExponentialBackoff);
assert_eq!(
config.delay_for_attempt(10),
std::time::Duration::from_millis(51200)
);
}
#[test]
fn test_retry_config_delay_overflow_protection() {
let config = RetryConfig::new(100, u64::MAX, RetryStrategy::ExponentialBackoff);
let _delay = config.delay_for_attempt(64); }
#[test]
fn test_retry_config_clone() {
let config = RetryConfig::new(3, 200, RetryStrategy::ExponentialBackoff);
let cloned = config.clone();
assert_eq!(config.max_retries, cloned.max_retries);
assert_eq!(config.delay_ms, cloned.delay_ms);
assert_eq!(config.strategy, cloned.strategy);
}
#[test]
fn test_retry_config_debug() {
let config = RetryConfig::new(2, 1000, RetryStrategy::ExponentialBackoff);
let debug_str = format!("{:?}", config);
assert!(debug_str.contains("max_retries"));
assert!(debug_str.contains("delay_ms"));
assert!(debug_str.contains("strategy"));
}
}