1use std::collections::HashMap;
12use std::sync::atomic::{AtomicU64, Ordering};
13use std::time::Instant;
14
15use serde::Serialize;
16
17#[derive(Debug, Clone)]
19pub struct FlowControlConfig {
20 pub max_in_flight: usize,
23 pub min_batch_size: usize,
26 pub max_batch_size: usize,
29 pub initial_batch_size: usize,
32 pub max_pending_count: usize,
35 pub max_pending_bytes: usize,
38}
39
40impl Default for FlowControlConfig {
41 fn default() -> Self {
42 Self {
43 max_in_flight: 64,
44 min_batch_size: 10,
45 max_batch_size: 500,
46 initial_batch_size: 50,
47 max_pending_count: 10_000,
48 max_pending_bytes: 50 * 1024 * 1024,
49 }
50 }
51}
52
53#[derive(Debug, Serialize)]
57pub struct SyncMetricsSnapshot {
58 pub state: &'static str,
60 pub pending_count: u64,
62 pub pending_bytes: u64,
64 pub deltas_pushed: u64,
66 pub deltas_received: u64,
68 pub deltas_rejected: u64,
70 pub avg_rtt_ms: u64,
72 pub in_flight: u64,
74 pub reconnect_count: u64,
76 pub last_sync_ts: u64,
78 pub checksum_failures: u64,
80 pub current_batch_size: u64,
82 pub conflicts_total: u64,
84}
85
86pub struct SyncMetrics {
88 pub deltas_pushed: AtomicU64,
89 pub deltas_received: AtomicU64,
90 pub deltas_rejected: AtomicU64,
91 pub reconnect_count: AtomicU64,
92 pub last_sync_ts: AtomicU64,
93 pub checksum_failures: AtomicU64,
94 pub conflicts_total: AtomicU64,
96 conflicts_by_collection: std::sync::Mutex<HashMap<String, u64>>,
98}
99
100impl SyncMetrics {
101 pub fn new() -> Self {
102 Self {
103 deltas_pushed: AtomicU64::new(0),
104 deltas_received: AtomicU64::new(0),
105 deltas_rejected: AtomicU64::new(0),
106 reconnect_count: AtomicU64::new(0),
107 last_sync_ts: AtomicU64::new(0),
108 checksum_failures: AtomicU64::new(0),
109 conflicts_total: AtomicU64::new(0),
110 conflicts_by_collection: std::sync::Mutex::new(HashMap::new()),
111 }
112 }
113
114 pub fn record_push(&self, count: u64) {
115 self.deltas_pushed.fetch_add(count, Ordering::Relaxed);
116 self.last_sync_ts
117 .store(crate::runtime::now_millis(), Ordering::Relaxed);
118 }
119
120 pub fn record_received(&self) {
121 self.deltas_received.fetch_add(1, Ordering::Relaxed);
122 self.last_sync_ts
123 .store(crate::runtime::now_millis(), Ordering::Relaxed);
124 }
125
126 pub fn record_reject(&self) {
127 self.deltas_rejected.fetch_add(1, Ordering::Relaxed);
128 }
129
130 pub fn record_reconnect(&self) {
131 self.reconnect_count.fetch_add(1, Ordering::Relaxed);
132 }
133
134 pub fn record_checksum_failure(&self) {
135 self.checksum_failures.fetch_add(1, Ordering::Relaxed);
136 }
137
138 pub fn record_conflict(&self, collection: &str) {
140 self.conflicts_total.fetch_add(1, Ordering::Relaxed);
141 if let Ok(mut map) = self.conflicts_by_collection.lock() {
142 *map.entry(collection.to_string()).or_insert(0) += 1;
143 }
144 }
145
146 pub fn conflicts_by_collection(&self) -> HashMap<String, u64> {
148 self.conflicts_by_collection
149 .lock()
150 .map(|m| m.clone())
151 .unwrap_or_default()
152 }
153}
154
155impl Default for SyncMetrics {
156 fn default() -> Self {
157 Self::new()
158 }
159}
160
161pub struct FlowController {
166 config: FlowControlConfig,
167
168 in_flight: HashMap<u64, Instant>,
170
171 ema_rtt_ms: f64,
174
175 current_batch_size: usize,
177
178 consecutive_acks: usize,
180
181 pending_count: usize,
183
184 pending_bytes: usize,
186}
187
188impl FlowController {
189 pub fn new(config: FlowControlConfig) -> Self {
190 let initial_batch = config.initial_batch_size;
191 Self {
192 config,
193 in_flight: HashMap::new(),
194 ema_rtt_ms: 0.0,
195 current_batch_size: initial_batch,
196 consecutive_acks: 0,
197 pending_count: 0,
198 pending_bytes: 0,
199 }
200 }
201
202 pub fn can_push(&self) -> bool {
204 self.in_flight.len() < self.config.max_in_flight
205 }
206
207 pub fn next_batch_size(&self) -> usize {
210 let window_remaining = self
211 .config
212 .max_in_flight
213 .saturating_sub(self.in_flight.len());
214 self.current_batch_size.min(window_remaining)
215 }
216
217 pub fn record_push(&mut self, mutation_ids: &[u64]) {
219 let now = Instant::now();
220 for &mid in mutation_ids {
221 self.in_flight.insert(mid, now);
222 }
223 }
224
225 pub fn record_ack(&mut self, mutation_id: u64) -> Option<u64> {
229 let send_time = self.in_flight.remove(&mutation_id)?;
230 let rtt_ms = send_time.elapsed().as_millis() as f64;
231
232 if self.ema_rtt_ms == 0.0 {
234 self.ema_rtt_ms = rtt_ms;
235 } else {
236 self.ema_rtt_ms = 0.875 * self.ema_rtt_ms + 0.125 * rtt_ms;
237 }
238
239 self.consecutive_acks += 1;
241 if self.consecutive_acks >= 8 {
242 self.consecutive_acks = 0;
243 self.current_batch_size = (self.current_batch_size + 1).min(self.config.max_batch_size);
244 }
245
246 Some(rtt_ms as u64)
247 }
248
249 pub fn record_reject(&mut self, mutation_id: u64) {
251 self.in_flight.remove(&mutation_id);
252 self.consecutive_acks = 0;
253 self.current_batch_size = (self.current_batch_size / 2).max(self.config.min_batch_size);
255 }
256
257 pub fn update_pending(&mut self, count: usize, bytes: usize) {
259 self.pending_count = count;
260 self.pending_bytes = bytes;
261 }
262
263 pub fn is_queue_full(&self) -> bool {
265 self.pending_count >= self.config.max_pending_count
266 || self.pending_bytes >= self.config.max_pending_bytes
267 }
268
269 pub fn in_flight_count(&self) -> usize {
271 self.in_flight.len()
272 }
273
274 pub fn avg_rtt_ms(&self) -> u64 {
276 self.ema_rtt_ms as u64
277 }
278
279 pub fn current_batch_size(&self) -> usize {
281 self.current_batch_size
282 }
283
284 pub fn pending_count(&self) -> usize {
286 self.pending_count
287 }
288
289 pub fn pending_bytes(&self) -> usize {
291 self.pending_bytes
292 }
293
294 pub fn cleanup_stale(&mut self, timeout: std::time::Duration) -> usize {
297 let now = Instant::now();
298 let before = self.in_flight.len();
299 self.in_flight
300 .retain(|_, sent_at| now.duration_since(*sent_at) < timeout);
301 let cleaned = before - self.in_flight.len();
302 if cleaned > 0 {
303 self.consecutive_acks = 0;
305 self.current_batch_size = (self.current_batch_size / 2).max(self.config.min_batch_size);
306 }
307 cleaned
308 }
309
310 pub fn snapshot(&self, state: &'static str, metrics: &SyncMetrics) -> SyncMetricsSnapshot {
312 SyncMetricsSnapshot {
313 state,
314 pending_count: self.pending_count as u64,
315 pending_bytes: self.pending_bytes as u64,
316 deltas_pushed: metrics.deltas_pushed.load(Ordering::Relaxed),
317 deltas_received: metrics.deltas_received.load(Ordering::Relaxed),
318 deltas_rejected: metrics.deltas_rejected.load(Ordering::Relaxed),
319 avg_rtt_ms: self.avg_rtt_ms(),
320 in_flight: self.in_flight.len() as u64,
321 reconnect_count: metrics.reconnect_count.load(Ordering::Relaxed),
322 last_sync_ts: metrics.last_sync_ts.load(Ordering::Relaxed),
323 checksum_failures: metrics.checksum_failures.load(Ordering::Relaxed),
324 current_batch_size: self.current_batch_size as u64,
325 conflicts_total: metrics.conflicts_total.load(Ordering::Relaxed),
326 }
327 }
328
329 pub fn reset(&mut self) {
331 self.in_flight.clear();
332 self.consecutive_acks = 0;
333 }
336}
337
338impl Default for FlowController {
339 fn default() -> Self {
340 Self::new(FlowControlConfig::default())
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 #[test]
349 fn default_config_values() {
350 let cfg = FlowControlConfig::default();
351 assert_eq!(cfg.max_in_flight, 64);
352 assert_eq!(cfg.min_batch_size, 10);
353 assert_eq!(cfg.max_batch_size, 500);
354 assert_eq!(cfg.initial_batch_size, 50);
355 assert_eq!(cfg.max_pending_count, 10_000);
356 assert_eq!(cfg.max_pending_bytes, 50 * 1024 * 1024);
357 }
358
359 #[test]
360 fn flow_control_window() {
361 let mut fc = FlowController::new(FlowControlConfig {
362 max_in_flight: 3,
363 ..Default::default()
364 });
365
366 assert!(fc.can_push());
367 assert_eq!(fc.next_batch_size(), 3); fc.record_push(&[1, 2, 3]);
370 assert!(!fc.can_push());
371 assert_eq!(fc.next_batch_size(), 0);
372 assert_eq!(fc.in_flight_count(), 3);
373
374 fc.record_ack(2);
376 assert!(fc.can_push());
377 assert_eq!(fc.next_batch_size(), 1);
378 }
379
380 #[test]
381 fn aimd_additive_increase() {
382 let mut fc = FlowController::new(FlowControlConfig {
383 initial_batch_size: 10,
384 max_batch_size: 500,
385 max_in_flight: 1000,
386 ..Default::default()
387 });
388
389 assert_eq!(fc.current_batch_size(), 10);
390
391 for i in 0..8 {
393 fc.record_push(&[i]);
394 fc.record_ack(i);
395 }
396 assert_eq!(fc.current_batch_size(), 11);
397
398 for i in 8..16 {
400 fc.record_push(&[i]);
401 fc.record_ack(i);
402 }
403 assert_eq!(fc.current_batch_size(), 12);
404 }
405
406 #[test]
407 fn aimd_multiplicative_decrease() {
408 let mut fc = FlowController::new(FlowControlConfig {
409 initial_batch_size: 100,
410 min_batch_size: 10,
411 max_in_flight: 1000,
412 ..Default::default()
413 });
414
415 assert_eq!(fc.current_batch_size(), 100);
416
417 fc.record_push(&[1]);
419 fc.record_reject(1);
420 assert_eq!(fc.current_batch_size(), 50);
421
422 fc.record_push(&[2]);
424 fc.record_reject(2);
425 assert_eq!(fc.current_batch_size(), 25);
426
427 fc.record_push(&[3]);
429 fc.record_reject(3);
430 assert_eq!(fc.current_batch_size(), 12);
431
432 fc.record_push(&[4]);
433 fc.record_reject(4);
434 assert_eq!(fc.current_batch_size(), 10); fc.record_push(&[5]);
437 fc.record_reject(5);
438 assert_eq!(fc.current_batch_size(), 10); }
440
441 #[test]
442 fn rtt_ewma() {
443 let mut fc = FlowController::default();
444 fc.record_push(&[1]);
445 let rtt = fc.record_ack(1);
448 assert!(rtt.is_some());
449 assert!(fc.avg_rtt_ms() < 100); }
451
452 #[test]
453 fn bounded_pending_queue_count() {
454 let mut fc = FlowController::new(FlowControlConfig {
455 max_pending_count: 100,
456 max_pending_bytes: 1_000_000,
457 ..Default::default()
458 });
459
460 fc.update_pending(99, 500);
461 assert!(!fc.is_queue_full());
462
463 fc.update_pending(100, 500);
464 assert!(fc.is_queue_full());
465 }
466
467 #[test]
468 fn bounded_pending_queue_bytes() {
469 let mut fc = FlowController::new(FlowControlConfig {
470 max_pending_count: 100_000,
471 max_pending_bytes: 1000,
472 ..Default::default()
473 });
474
475 fc.update_pending(5, 999);
476 assert!(!fc.is_queue_full());
477
478 fc.update_pending(5, 1000);
479 assert!(fc.is_queue_full());
480 }
481
482 #[test]
483 fn cleanup_stale_in_flight() {
484 let mut fc = FlowController::new(FlowControlConfig {
485 initial_batch_size: 100,
486 min_batch_size: 10,
487 max_in_flight: 1000,
488 ..Default::default()
489 });
490
491 fc.record_push(&[1, 2, 3]);
492 assert_eq!(fc.in_flight_count(), 3);
493
494 let cleaned = fc.cleanup_stale(std::time::Duration::from_secs(3600));
496 assert_eq!(cleaned, 0);
497 assert_eq!(fc.in_flight_count(), 3);
498
499 let cleaned = fc.cleanup_stale(std::time::Duration::ZERO);
501 assert_eq!(cleaned, 3);
502 assert_eq!(fc.in_flight_count(), 0);
503 assert_eq!(fc.current_batch_size(), 50);
505 }
506
507 #[test]
508 fn reset_clears_in_flight() {
509 let mut fc = FlowController::default();
510 fc.record_push(&[1, 2, 3]);
511 assert_eq!(fc.in_flight_count(), 3);
512
513 fc.reset();
514 assert_eq!(fc.in_flight_count(), 0);
515 assert_eq!(fc.current_batch_size(), 50);
517 }
518
519 #[test]
520 fn metrics_snapshot() {
521 let mut fc = FlowController::default();
522 let metrics = SyncMetrics::new();
523 metrics.record_push(5);
524 metrics.record_received();
525 metrics.record_reject();
526 metrics.record_reconnect();
527
528 fc.update_pending(42, 8192);
529
530 let snap = fc.snapshot("connected", &metrics);
531 assert_eq!(snap.state, "connected");
532 assert_eq!(snap.pending_count, 42);
533 assert_eq!(snap.pending_bytes, 8192);
534 assert_eq!(snap.deltas_pushed, 5);
535 assert_eq!(snap.deltas_received, 1);
536 assert_eq!(snap.deltas_rejected, 1);
537 assert_eq!(snap.reconnect_count, 1);
538 assert_eq!(snap.current_batch_size, 50);
539 }
540
541 #[test]
542 fn ack_unknown_mutation_returns_none() {
543 let mut fc = FlowController::default();
544 assert!(fc.record_ack(999).is_none());
545 }
546
547 #[test]
548 fn batch_size_capped_at_max() {
549 let mut fc = FlowController::new(FlowControlConfig {
550 initial_batch_size: 499,
551 max_batch_size: 500,
552 max_in_flight: 10_000,
553 ..Default::default()
554 });
555
556 for i in 0..8 {
558 fc.record_push(&[i]);
559 fc.record_ack(i);
560 }
561 assert_eq!(fc.current_batch_size(), 500);
562
563 for i in 8..16 {
565 fc.record_push(&[i]);
566 fc.record_ack(i);
567 }
568 assert_eq!(fc.current_batch_size(), 500);
569 }
570}