use serde::{Deserialize, Serialize};
use crate::error::ValidationError;
use crate::types::CollectiveId;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum SyncDirection {
PushOnly,
PullOnly,
#[default]
Bidirectional,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConflictResolution {
#[default]
ServerWins,
LastWriteWins,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RetryConfig {
pub max_retries: u32,
pub initial_backoff_ms: u64,
pub max_backoff_ms: u64,
pub backoff_multiplier: f64,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_retries: 5,
initial_backoff_ms: 500,
max_backoff_ms: 30_000,
backoff_multiplier: 2.0,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SyncConfig {
pub direction: SyncDirection,
pub conflict_resolution: ConflictResolution,
pub batch_size: usize,
pub push_interval_ms: u64,
pub pull_interval_ms: u64,
pub retry: RetryConfig,
pub collectives: Option<Vec<CollectiveId>>,
pub sync_relations: bool,
pub sync_insights: bool,
}
impl Default for SyncConfig {
fn default() -> Self {
Self {
direction: SyncDirection::default(),
conflict_resolution: ConflictResolution::default(),
batch_size: 500,
push_interval_ms: 1000,
pull_interval_ms: 1000,
retry: RetryConfig::default(),
collectives: None,
sync_relations: true,
sync_insights: true,
}
}
}
impl SyncConfig {
pub fn validate(&self) -> Result<(), ValidationError> {
if self.batch_size == 0 {
return Err(ValidationError::invalid_field(
"batch_size",
"must be greater than 0",
));
}
if self.push_interval_ms == 0 {
return Err(ValidationError::invalid_field(
"push_interval_ms",
"must be greater than 0",
));
}
if self.pull_interval_ms == 0 {
return Err(ValidationError::invalid_field(
"pull_interval_ms",
"must be greater than 0",
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sync_config_defaults() {
let config = SyncConfig::default();
assert_eq!(config.direction, SyncDirection::Bidirectional);
assert_eq!(config.conflict_resolution, ConflictResolution::ServerWins);
assert_eq!(config.batch_size, 500);
assert_eq!(config.push_interval_ms, 1000);
assert_eq!(config.pull_interval_ms, 1000);
assert!(config.collectives.is_none());
assert!(config.sync_relations);
assert!(config.sync_insights);
}
#[test]
fn test_sync_config_validate_success() {
let config = SyncConfig::default();
assert!(config.validate().is_ok());
}
#[test]
fn test_sync_config_validate_zero_batch_size() {
let config = SyncConfig {
batch_size: 0,
..Default::default()
};
let err = config.validate().unwrap_err();
assert!(
matches!(err, ValidationError::InvalidField { field, .. } if field == "batch_size")
);
}
#[test]
fn test_sync_config_validate_zero_push_interval() {
let config = SyncConfig {
push_interval_ms: 0,
..Default::default()
};
let err = config.validate().unwrap_err();
assert!(
matches!(err, ValidationError::InvalidField { field, .. } if field == "push_interval_ms")
);
}
#[test]
fn test_sync_config_validate_zero_pull_interval() {
let config = SyncConfig {
pull_interval_ms: 0,
..Default::default()
};
let err = config.validate().unwrap_err();
assert!(
matches!(err, ValidationError::InvalidField { field, .. } if field == "pull_interval_ms")
);
}
#[test]
fn test_sync_config_postcard_roundtrip() {
let config = SyncConfig {
direction: SyncDirection::PushOnly,
batch_size: 100,
collectives: Some(vec![CollectiveId::new()]),
..Default::default()
};
let bytes = postcard::to_allocvec(&config).unwrap();
let restored: SyncConfig = postcard::from_bytes(&bytes).unwrap();
assert_eq!(config.direction, restored.direction);
assert_eq!(config.batch_size, restored.batch_size);
}
#[test]
fn test_retry_config_defaults() {
let config = RetryConfig::default();
assert_eq!(config.max_retries, 5);
assert_eq!(config.initial_backoff_ms, 500);
assert_eq!(config.max_backoff_ms, 30_000);
assert!((config.backoff_multiplier - 2.0).abs() < f64::EPSILON);
}
}