sync_engine/
config.rs

1// Copyright (c) 2025-2026 Adrian Robinson. Licensed under the AGPL-3.0.
2// See LICENSE file in the project root for full license text.
3
4//! Configuration for the sync engine.
5//!
6//! # Example
7//!
8//! ```
9//! use sync_engine::SyncEngineConfig;
10//!
11//! // Minimal config (uses defaults)
12//! let config = SyncEngineConfig::default();
13//! assert_eq!(config.l1_max_bytes, 256 * 1024 * 1024); // 256 MB
14//!
15//! // Full config
16//! let config = SyncEngineConfig {
17//!     redis_url: Some("redis://localhost:6379".into()),
18//!     sql_url: Some("mysql://user:pass@localhost/db".into()),
19//!     l1_max_bytes: 128 * 1024 * 1024, // 128 MB
20//!     batch_flush_count: 100,
21//!     batch_flush_ms: 50,
22//!     ..Default::default()
23//! };
24//! ```
25
26use serde::Deserialize;
27
28/// Configuration for the sync engine.
29///
30/// All fields have sensible defaults. At minimum, you should configure
31/// `redis_url` and `sql_url` for production use.
32#[derive(Debug, Clone, Deserialize)]
33pub struct SyncEngineConfig {
34    /// Redis connection string (e.g., "redis://localhost:6379")
35    #[serde(default)]
36    pub redis_url: Option<String>,
37    
38    /// Redis key prefix for namespacing (e.g., "myapp:" → keys become "myapp:user.alice")
39    /// Allows sync-engine to coexist with other data in the same Redis instance.
40    #[serde(default)]
41    pub redis_prefix: Option<String>,
42    
43    /// SQL connection string (e.g., "sqlite:sync.db" or "mysql://user:pass@host/db")
44    #[serde(default)]
45    pub sql_url: Option<String>,
46    
47    /// L1 cache max size in bytes (default: 256 MB)
48    #[serde(default = "default_l1_max_bytes")]
49    pub l1_max_bytes: usize,
50    
51    /// Maximum payload size in bytes (default: 16 MB)
52    /// 
53    /// Payloads larger than this will be rejected with an error.
54    /// This prevents a single large item (e.g., 1TB file) from exhausting
55    /// the L1 cache. Set to 0 for unlimited (not recommended).
56    /// 
57    /// **Important**: This is a safety limit. Developers should choose a value
58    /// appropriate for their use case. For binary blobs, consider using
59    /// external object storage (S3, GCS) and storing only references here.
60    #[serde(default = "default_max_payload_bytes")]
61    pub max_payload_bytes: usize,
62    
63    /// Backpressure thresholds
64    #[serde(default = "default_backpressure_warn")]
65    pub backpressure_warn: f64,
66    #[serde(default = "default_backpressure_critical")]
67    pub backpressure_critical: f64,
68    
69    /// Batch flush settings
70    #[serde(default = "default_batch_flush_ms")]
71    pub batch_flush_ms: u64,
72    #[serde(default = "default_batch_flush_count")]
73    pub batch_flush_count: usize,
74    #[serde(default = "default_batch_flush_bytes")]
75    pub batch_flush_bytes: usize,
76    
77    /// Cuckoo filter warmup
78    #[serde(default = "default_cuckoo_warmup_batch_size")]
79    pub cuckoo_warmup_batch_size: usize,
80    
81    /// WAL path (SQLite file for durability during MySQL outages)
82    #[serde(default)]
83    pub wal_path: Option<String>,
84    
85    /// WAL max items before backpressure
86    #[serde(default)]
87    pub wal_max_items: Option<u64>,
88    
89    /// WAL drain batch size
90    #[serde(default = "default_wal_drain_batch_size")]
91    pub wal_drain_batch_size: usize,
92    
93    /// CF snapshot interval in seconds (0 = disabled)
94    #[serde(default = "default_cf_snapshot_interval_secs")]
95    pub cf_snapshot_interval_secs: u64,
96    
97    /// CF snapshot after N inserts (0 = disabled)
98    #[serde(default = "default_cf_snapshot_insert_threshold")]
99    pub cf_snapshot_insert_threshold: u64,
100    
101    /// Redis eviction: enable proactive eviction before Redis LRU kicks in
102    #[serde(default = "default_redis_eviction_enabled")]
103    pub redis_eviction_enabled: bool,
104    
105    /// Redis eviction: pressure threshold to start evicting (0.0-1.0, default: 0.75)
106    #[serde(default = "default_redis_eviction_start")]
107    pub redis_eviction_start: f64,
108    
109    /// Redis eviction: target pressure after eviction (0.0-1.0, default: 0.60)
110    #[serde(default = "default_redis_eviction_target")]
111    pub redis_eviction_target: f64,
112    
113    /// Merkle calculation: enable merkle tree updates on this instance.
114    /// 
115    /// In a multi-instance deployment with shared SQL, only a few nodes need to
116    /// run merkle calculations for resilience. Set to false on most nodes.
117    /// Default: true (single-instance default)
118    #[serde(default = "default_merkle_calc_enabled")]
119    pub merkle_calc_enabled: bool,
120    
121    /// Merkle calculation: jitter range in milliseconds.
122    /// 
123    /// Adds random delay (0 to N ms) before merkle batch calculation to reduce
124    /// contention when multiple instances are calculating. Default: 0 (no jitter)
125    #[serde(default)]
126    pub merkle_calc_jitter_ms: u64,
127    
128    /// CDC Stream: Enable Change Data Capture output to Redis Stream.
129    /// 
130    /// When enabled, every Put/Delete writes to `{redis_prefix}__local__:cdc`.
131    /// This enables external replication agents to tail changes.
132    /// Default: false (opt-in feature)
133    #[serde(default)]
134    pub enable_cdc_stream: bool,
135    
136    /// CDC Stream: Maximum entries before approximate trimming (MAXLEN ~).
137    /// 
138    /// Consumers that fall behind this limit rely on Merkle repair.
139    /// Default: 100,000 entries
140    #[serde(default = "default_cdc_stream_maxlen")]
141    pub cdc_stream_maxlen: u64,
142
143    /// Redis command timeout in milliseconds.
144    /// 
145    /// Maximum time to wait for a Redis command to complete before timing out.
146    /// Under high load, increase this value to avoid spurious timeouts.
147    /// Default: 5000 (5 seconds)
148    #[serde(default = "default_redis_timeout_ms")]
149    pub redis_timeout_ms: u64,
150
151    /// Redis response timeout in milliseconds.
152    /// 
153    /// Maximum time to wait for Redis to respond after sending a command.
154    /// Default: 5000 (5 seconds)
155    #[serde(default = "default_redis_response_timeout_ms")]
156    pub redis_response_timeout_ms: u64,
157}
158
159fn default_l1_max_bytes() -> usize { 256 * 1024 * 1024 } // 256 MB
160fn default_max_payload_bytes() -> usize { 16 * 1024 * 1024 } // 16 MB
161fn default_backpressure_warn() -> f64 { 0.7 }
162fn default_backpressure_critical() -> f64 { 0.9 }
163fn default_batch_flush_ms() -> u64 { 100 }
164fn default_batch_flush_count() -> usize { 1000 }
165fn default_batch_flush_bytes() -> usize { 1024 * 1024 } // 1 MB
166fn default_cuckoo_warmup_batch_size() -> usize { 10000 }
167fn default_wal_drain_batch_size() -> usize { 100 }
168fn default_cf_snapshot_interval_secs() -> u64 { 30 }
169fn default_cf_snapshot_insert_threshold() -> u64 { 10_000 }
170fn default_redis_eviction_enabled() -> bool { true }
171fn default_redis_eviction_start() -> f64 { 0.75 }
172fn default_redis_eviction_target() -> f64 { 0.60 }
173fn default_merkle_calc_enabled() -> bool { true }
174fn default_cdc_stream_maxlen() -> u64 { 100_000 }
175fn default_redis_timeout_ms() -> u64 { 5_000 }
176fn default_redis_response_timeout_ms() -> u64 { 5_000 }
177
178impl Default for SyncEngineConfig {
179    fn default() -> Self {
180        Self {
181            redis_url: None,
182            sql_url: None,
183            redis_prefix: None,
184            l1_max_bytes: default_l1_max_bytes(),
185            max_payload_bytes: default_max_payload_bytes(),
186            backpressure_warn: default_backpressure_warn(),
187            backpressure_critical: default_backpressure_critical(),
188            batch_flush_ms: default_batch_flush_ms(),
189            batch_flush_count: default_batch_flush_count(),
190            batch_flush_bytes: default_batch_flush_bytes(),
191            cuckoo_warmup_batch_size: default_cuckoo_warmup_batch_size(),
192            wal_path: None,
193            wal_max_items: None,
194            wal_drain_batch_size: default_wal_drain_batch_size(),
195            cf_snapshot_interval_secs: default_cf_snapshot_interval_secs(),
196            cf_snapshot_insert_threshold: default_cf_snapshot_insert_threshold(),
197            redis_eviction_enabled: default_redis_eviction_enabled(),
198            redis_eviction_start: default_redis_eviction_start(),
199            redis_eviction_target: default_redis_eviction_target(),
200            merkle_calc_enabled: default_merkle_calc_enabled(),
201            merkle_calc_jitter_ms: 0,
202            enable_cdc_stream: false,
203            cdc_stream_maxlen: default_cdc_stream_maxlen(),
204            redis_timeout_ms: default_redis_timeout_ms(),
205            redis_response_timeout_ms: default_redis_response_timeout_ms(),
206        }
207    }
208}