sync-engine 0.2.19

High-performance tiered sync engine with L1/L2/L3 caching and Redis/SQL backends
Documentation
// Copyright (c) 2025-2026 Adrian Robinson. Licensed under the AGPL-3.0.
// See LICENSE file in the project root for full license text.

//! Configuration for the sync engine.
//!
//! # Example
//!
//! ```
//! use sync_engine::SyncEngineConfig;
//!
//! // Minimal config (uses defaults)
//! let config = SyncEngineConfig::default();
//! assert_eq!(config.l1_max_bytes, 256 * 1024 * 1024); // 256 MB
//!
//! // Full config
//! let config = SyncEngineConfig {
//!     redis_url: Some("redis://localhost:6379".into()),
//!     sql_url: Some("mysql://user:pass@localhost/db".into()),
//!     l1_max_bytes: 128 * 1024 * 1024, // 128 MB
//!     batch_flush_count: 100,
//!     batch_flush_ms: 50,
//!     ..Default::default()
//! };
//! ```

use serde::Deserialize;

/// Configuration for the sync engine.
///
/// All fields have sensible defaults. At minimum, you should configure
/// `redis_url` and `sql_url` for production use.
#[derive(Debug, Clone, Deserialize)]
pub struct SyncEngineConfig {
    /// Redis connection string (e.g., "redis://localhost:6379")
    #[serde(default)]
    pub redis_url: Option<String>,
    
    /// Redis key prefix for namespacing (e.g., "myapp:" → keys become "myapp:user.alice")
    /// Allows sync-engine to coexist with other data in the same Redis instance.
    #[serde(default)]
    pub redis_prefix: Option<String>,
    
    /// SQL connection string (e.g., "sqlite:sync.db" or "mysql://user:pass@host/db")
    #[serde(default)]
    pub sql_url: Option<String>,
    
    /// L1 cache max size in bytes (default: 256 MB)
    #[serde(default = "default_l1_max_bytes")]
    pub l1_max_bytes: usize,
    
    /// Maximum payload size in bytes (default: 16 MB)
    /// 
    /// Payloads larger than this will be rejected with an error.
    /// This prevents a single large item (e.g., 1TB file) from exhausting
    /// the L1 cache. Set to 0 for unlimited (not recommended).
    /// 
    /// **Important**: This is a safety limit. Developers should choose a value
    /// appropriate for their use case. For binary blobs, consider using
    /// external object storage (S3, GCS) and storing only references here.
    #[serde(default = "default_max_payload_bytes")]
    pub max_payload_bytes: usize,
    
    /// Backpressure thresholds
    #[serde(default = "default_backpressure_warn")]
    pub backpressure_warn: f64,
    #[serde(default = "default_backpressure_critical")]
    pub backpressure_critical: f64,
    
    /// Batch flush settings
    #[serde(default = "default_batch_flush_ms")]
    pub batch_flush_ms: u64,
    #[serde(default = "default_batch_flush_count")]
    pub batch_flush_count: usize,
    #[serde(default = "default_batch_flush_bytes")]
    pub batch_flush_bytes: usize,
    
    /// Cuckoo filter warmup
    #[serde(default = "default_cuckoo_warmup_batch_size")]
    pub cuckoo_warmup_batch_size: usize,
    
    /// WAL path (SQLite file for durability during MySQL outages)
    #[serde(default)]
    pub wal_path: Option<String>,
    
    /// WAL max items before backpressure
    #[serde(default)]
    pub wal_max_items: Option<u64>,
    
    /// WAL drain batch size
    #[serde(default = "default_wal_drain_batch_size")]
    pub wal_drain_batch_size: usize,
    
    /// CF snapshot interval in seconds (0 = disabled)
    #[serde(default = "default_cf_snapshot_interval_secs")]
    pub cf_snapshot_interval_secs: u64,
    
    /// CF snapshot after N inserts (0 = disabled)
    #[serde(default = "default_cf_snapshot_insert_threshold")]
    pub cf_snapshot_insert_threshold: u64,
    
    /// Redis eviction: enable proactive eviction before Redis LRU kicks in
    #[serde(default = "default_redis_eviction_enabled")]
    pub redis_eviction_enabled: bool,
    
    /// Redis eviction: pressure threshold to start evicting (0.0-1.0, default: 0.75)
    #[serde(default = "default_redis_eviction_start")]
    pub redis_eviction_start: f64,
    
    /// Redis eviction: target pressure after eviction (0.0-1.0, default: 0.60)
    #[serde(default = "default_redis_eviction_target")]
    pub redis_eviction_target: f64,
    
    /// Merkle calculation: enable merkle tree updates on this instance.
    /// 
    /// In a multi-instance deployment with shared SQL, only a few nodes need to
    /// run merkle calculations for resilience. Set to false on most nodes.
    /// Default: true (single-instance default)
    #[serde(default = "default_merkle_calc_enabled")]
    pub merkle_calc_enabled: bool,
    
    /// Merkle calculation: jitter range in milliseconds.
    /// 
    /// Adds random delay (0 to N ms) before merkle batch calculation to reduce
    /// contention when multiple instances are calculating. Default: 0 (no jitter)
    #[serde(default)]
    pub merkle_calc_jitter_ms: u64,
    
    /// CDC Stream: Enable Change Data Capture output to Redis Stream.
    /// 
    /// When enabled, every Put/Delete writes to `{redis_prefix}:cdc`.
    /// This enables external replication agents to tail changes.
    /// Default: false (opt-in feature)
    #[serde(default)]
    pub enable_cdc_stream: bool,
    
    /// CDC Stream: Maximum entries before approximate trimming (MAXLEN ~).
    /// 
    /// Consumers that fall behind this limit rely on Merkle repair.
    /// Default: 100,000 entries
    #[serde(default = "default_cdc_stream_maxlen")]
    pub cdc_stream_maxlen: u64,

    /// Redis command timeout in milliseconds.
    /// 
    /// Maximum time to wait for a Redis command to complete before timing out.
    /// Under high load, increase this value to avoid spurious timeouts.
    /// Default: 5000 (5 seconds)
    #[serde(default = "default_redis_timeout_ms")]
    pub redis_timeout_ms: u64,

    /// Redis response timeout in milliseconds.
    /// 
    /// Maximum time to wait for Redis to respond after sending a command.
    /// Default: 5000 (5 seconds)
    #[serde(default = "default_redis_response_timeout_ms")]
    pub redis_response_timeout_ms: u64,

    /// Maximum concurrent SQL write operations.
    /// 
    /// Limits simultaneous sql_put_batch and merkle_nodes updates to reduce
    /// row-level lock contention and deadlocks under high load.
    /// Default: 4 (good balance for most MySQL setups)
    #[serde(default = "default_sql_write_concurrency")]
    pub sql_write_concurrency: usize,
    
    /// Merkle diagnostic tick interval in seconds (DEPRECATED, no longer used).
    /// 
    /// Cold path logging now provides better visibility into merkle sync state.
    /// This field is retained for backwards compatibility with existing configs.
    #[serde(default)]
    #[deprecated(since = "0.2.0", note = "Merkle tick removed; use cold path logging instead")]
    pub merkle_log_interval_secs: u64,
}

fn default_l1_max_bytes() -> usize { 256 * 1024 * 1024 } // 256 MB
fn default_max_payload_bytes() -> usize { 16 * 1024 * 1024 } // 16 MB
fn default_backpressure_warn() -> f64 { 0.7 }
fn default_backpressure_critical() -> f64 { 0.9 }
fn default_batch_flush_ms() -> u64 { 100 }
fn default_batch_flush_count() -> usize { 1000 }
fn default_batch_flush_bytes() -> usize { 1024 * 1024 } // 1 MB
fn default_cuckoo_warmup_batch_size() -> usize { 10000 }
fn default_wal_drain_batch_size() -> usize { 100 }
fn default_cf_snapshot_interval_secs() -> u64 { 30 }
fn default_cf_snapshot_insert_threshold() -> u64 { 10_000 }
fn default_redis_eviction_enabled() -> bool { true }
fn default_redis_eviction_start() -> f64 { 0.75 }
fn default_redis_eviction_target() -> f64 { 0.60 }
fn default_merkle_calc_enabled() -> bool { true }
fn default_cdc_stream_maxlen() -> u64 { 100_000 }
fn default_redis_timeout_ms() -> u64 { 5_000 }
fn default_redis_response_timeout_ms() -> u64 { 5_000 }
fn default_sql_write_concurrency() -> usize { 4 }

impl Default for SyncEngineConfig {
    #[allow(deprecated)]
    fn default() -> Self {
        Self {
            redis_url: None,
            sql_url: None,
            redis_prefix: None,
            l1_max_bytes: default_l1_max_bytes(),
            max_payload_bytes: default_max_payload_bytes(),
            backpressure_warn: default_backpressure_warn(),
            backpressure_critical: default_backpressure_critical(),
            batch_flush_ms: default_batch_flush_ms(),
            batch_flush_count: default_batch_flush_count(),
            batch_flush_bytes: default_batch_flush_bytes(),
            cuckoo_warmup_batch_size: default_cuckoo_warmup_batch_size(),
            wal_path: None,
            wal_max_items: None,
            wal_drain_batch_size: default_wal_drain_batch_size(),
            cf_snapshot_interval_secs: default_cf_snapshot_interval_secs(),
            cf_snapshot_insert_threshold: default_cf_snapshot_insert_threshold(),
            redis_eviction_enabled: default_redis_eviction_enabled(),
            redis_eviction_start: default_redis_eviction_start(),
            redis_eviction_target: default_redis_eviction_target(),
            merkle_calc_enabled: default_merkle_calc_enabled(),
            merkle_calc_jitter_ms: 0,
            enable_cdc_stream: false,
            cdc_stream_maxlen: default_cdc_stream_maxlen(),
            redis_timeout_ms: default_redis_timeout_ms(),
            redis_response_timeout_ms: default_redis_response_timeout_ms(),
            sql_write_concurrency: default_sql_write_concurrency(),
            merkle_log_interval_secs: 0,
        }
    }
}