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}