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}: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    /// Maximum concurrent SQL write operations.
159    /// 
160    /// Limits simultaneous sql_put_batch and merkle_nodes updates to reduce
161    /// row-level lock contention and deadlocks under high load.
162    /// Default: 4 (good balance for most MySQL setups)
163    #[serde(default = "default_sql_write_concurrency")]
164    pub sql_write_concurrency: usize,
165    
166    /// Merkle diagnostic tick interval in seconds (DEPRECATED, no longer used).
167    /// 
168    /// Cold path logging now provides better visibility into merkle sync state.
169    /// This field is retained for backwards compatibility with existing configs.
170    #[serde(default)]
171    #[deprecated(since = "0.2.0", note = "Merkle tick removed; use cold path logging instead")]
172    pub merkle_log_interval_secs: u64,
173}
174
175fn default_l1_max_bytes() -> usize { 256 * 1024 * 1024 } // 256 MB
176fn default_max_payload_bytes() -> usize { 16 * 1024 * 1024 } // 16 MB
177fn default_backpressure_warn() -> f64 { 0.7 }
178fn default_backpressure_critical() -> f64 { 0.9 }
179fn default_batch_flush_ms() -> u64 { 100 }
180fn default_batch_flush_count() -> usize { 1000 }
181fn default_batch_flush_bytes() -> usize { 1024 * 1024 } // 1 MB
182fn default_cuckoo_warmup_batch_size() -> usize { 10000 }
183fn default_wal_drain_batch_size() -> usize { 100 }
184fn default_cf_snapshot_interval_secs() -> u64 { 30 }
185fn default_cf_snapshot_insert_threshold() -> u64 { 10_000 }
186fn default_redis_eviction_enabled() -> bool { true }
187fn default_redis_eviction_start() -> f64 { 0.75 }
188fn default_redis_eviction_target() -> f64 { 0.60 }
189fn default_merkle_calc_enabled() -> bool { true }
190fn default_cdc_stream_maxlen() -> u64 { 100_000 }
191fn default_redis_timeout_ms() -> u64 { 5_000 }
192fn default_redis_response_timeout_ms() -> u64 { 5_000 }
193fn default_sql_write_concurrency() -> usize { 4 }
194
195impl Default for SyncEngineConfig {
196    #[allow(deprecated)]
197    fn default() -> Self {
198        Self {
199            redis_url: None,
200            sql_url: None,
201            redis_prefix: None,
202            l1_max_bytes: default_l1_max_bytes(),
203            max_payload_bytes: default_max_payload_bytes(),
204            backpressure_warn: default_backpressure_warn(),
205            backpressure_critical: default_backpressure_critical(),
206            batch_flush_ms: default_batch_flush_ms(),
207            batch_flush_count: default_batch_flush_count(),
208            batch_flush_bytes: default_batch_flush_bytes(),
209            cuckoo_warmup_batch_size: default_cuckoo_warmup_batch_size(),
210            wal_path: None,
211            wal_max_items: None,
212            wal_drain_batch_size: default_wal_drain_batch_size(),
213            cf_snapshot_interval_secs: default_cf_snapshot_interval_secs(),
214            cf_snapshot_insert_threshold: default_cf_snapshot_insert_threshold(),
215            redis_eviction_enabled: default_redis_eviction_enabled(),
216            redis_eviction_start: default_redis_eviction_start(),
217            redis_eviction_target: default_redis_eviction_target(),
218            merkle_calc_enabled: default_merkle_calc_enabled(),
219            merkle_calc_jitter_ms: 0,
220            enable_cdc_stream: false,
221            cdc_stream_maxlen: default_cdc_stream_maxlen(),
222            redis_timeout_ms: default_redis_timeout_ms(),
223            redis_response_timeout_ms: default_redis_response_timeout_ms(),
224            sql_write_concurrency: default_sql_write_concurrency(),
225            merkle_log_interval_secs: 0,
226        }
227    }
228}