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 pub stale_timeouts: u64,
86}
87
88pub struct SyncMetrics {
90 pub deltas_pushed: AtomicU64,
91 pub deltas_received: AtomicU64,
92 pub deltas_rejected: AtomicU64,
93 pub reconnect_count: AtomicU64,
94 pub last_sync_ts: AtomicU64,
95 pub checksum_failures: AtomicU64,
96 pub conflicts_total: AtomicU64,
98 conflicts_by_collection: std::sync::Mutex<HashMap<String, u64>>,
100 pub clock_skew_warnings: AtomicU64,
102 pub stale_timeouts: AtomicU64,
104}
105
106impl SyncMetrics {
107 pub fn new() -> Self {
108 Self {
109 deltas_pushed: AtomicU64::new(0),
110 deltas_received: AtomicU64::new(0),
111 deltas_rejected: AtomicU64::new(0),
112 reconnect_count: AtomicU64::new(0),
113 last_sync_ts: AtomicU64::new(0),
114 checksum_failures: AtomicU64::new(0),
115 conflicts_total: AtomicU64::new(0),
116 conflicts_by_collection: std::sync::Mutex::new(HashMap::new()),
117 clock_skew_warnings: AtomicU64::new(0),
118 stale_timeouts: AtomicU64::new(0),
119 }
120 }
121
122 pub fn record_stale_timeouts(&self, count: u64) {
124 self.stale_timeouts.fetch_add(count, Ordering::Relaxed);
125 }
126
127 pub fn record_clock_skew_warning(&self) {
129 self.clock_skew_warnings.fetch_add(1, Ordering::Relaxed);
130 }
131
132 pub fn record_push(&self, count: u64) {
133 self.deltas_pushed.fetch_add(count, Ordering::Relaxed);
134 self.last_sync_ts
135 .store(crate::runtime::now_millis(), Ordering::Relaxed);
136 }
137
138 pub fn record_received(&self) {
139 self.deltas_received.fetch_add(1, Ordering::Relaxed);
140 self.last_sync_ts
141 .store(crate::runtime::now_millis(), Ordering::Relaxed);
142 }
143
144 pub fn record_reject(&self) {
145 self.deltas_rejected.fetch_add(1, Ordering::Relaxed);
146 }
147
148 pub fn record_reconnect(&self) {
149 self.reconnect_count.fetch_add(1, Ordering::Relaxed);
150 }
151
152 pub fn record_checksum_failure(&self) {
153 self.checksum_failures.fetch_add(1, Ordering::Relaxed);
154 }
155
156 pub fn record_conflict(&self, collection: &str) {
158 self.conflicts_total.fetch_add(1, Ordering::Relaxed);
159 if let Ok(mut map) = self.conflicts_by_collection.lock() {
160 *map.entry(collection.to_string()).or_insert(0) += 1;
161 }
162 }
163
164 pub fn conflicts_by_collection(&self) -> HashMap<String, u64> {
166 self.conflicts_by_collection
167 .lock()
168 .map(|m| m.clone())
169 .unwrap_or_default()
170 }
171}
172
173impl Default for SyncMetrics {
174 fn default() -> Self {
175 Self::new()
176 }
177}
178
179pub struct FlowController {
184 config: FlowControlConfig,
185
186 in_flight: HashMap<u64, Instant>,
188
189 ema_rtt_ms: f64,
192
193 current_batch_size: usize,
195
196 consecutive_acks: usize,
198
199 pending_count: usize,
201
202 pending_bytes: usize,
204}
205
206impl FlowController {
207 pub fn new(config: FlowControlConfig) -> Self {
208 let initial_batch = config.initial_batch_size;
209 Self {
210 config,
211 in_flight: HashMap::new(),
212 ema_rtt_ms: 0.0,
213 current_batch_size: initial_batch,
214 consecutive_acks: 0,
215 pending_count: 0,
216 pending_bytes: 0,
217 }
218 }
219
220 pub fn can_push(&self) -> bool {
222 self.in_flight.len() < self.config.max_in_flight
223 }
224
225 pub fn next_batch_size(&self) -> usize {
228 let window_remaining = self
229 .config
230 .max_in_flight
231 .saturating_sub(self.in_flight.len());
232 self.current_batch_size.min(window_remaining)
233 }
234
235 pub fn record_push(&mut self, mutation_ids: &[u64]) {
237 let now = Instant::now();
238 for &mid in mutation_ids {
239 self.in_flight.insert(mid, now);
240 }
241 }
242
243 pub fn record_ack(&mut self, mutation_id: u64) -> Option<u64> {
247 let send_time = self.in_flight.remove(&mutation_id)?;
248 let rtt_ms = send_time.elapsed().as_millis() as f64;
249
250 if self.ema_rtt_ms == 0.0 {
252 self.ema_rtt_ms = rtt_ms;
253 } else {
254 self.ema_rtt_ms = 0.875 * self.ema_rtt_ms + 0.125 * rtt_ms;
255 }
256
257 self.consecutive_acks += 1;
259 if self.consecutive_acks >= 8 {
260 self.consecutive_acks = 0;
261 self.current_batch_size = (self.current_batch_size + 1).min(self.config.max_batch_size);
262 }
263
264 Some(rtt_ms as u64)
265 }
266
267 pub fn record_reject(&mut self, mutation_id: u64) {
269 self.in_flight.remove(&mutation_id);
270 self.consecutive_acks = 0;
271 self.current_batch_size = (self.current_batch_size / 2).max(self.config.min_batch_size);
273 }
274
275 pub fn update_pending(&mut self, count: usize, bytes: usize) {
277 self.pending_count = count;
278 self.pending_bytes = bytes;
279 }
280
281 pub fn is_queue_full(&self) -> bool {
283 self.pending_count >= self.config.max_pending_count
284 || self.pending_bytes >= self.config.max_pending_bytes
285 }
286
287 pub fn in_flight_count(&self) -> usize {
289 self.in_flight.len()
290 }
291
292 pub fn avg_rtt_ms(&self) -> u64 {
294 self.ema_rtt_ms as u64
295 }
296
297 pub fn current_batch_size(&self) -> usize {
299 self.current_batch_size
300 }
301
302 pub fn pending_count(&self) -> usize {
304 self.pending_count
305 }
306
307 pub fn pending_bytes(&self) -> usize {
309 self.pending_bytes
310 }
311
312 pub fn cleanup_stale(&mut self, timeout: std::time::Duration) -> usize {
318 let now = Instant::now();
319 let before = self.in_flight.len();
320 self.in_flight
321 .retain(|_, sent_at| now.duration_since(*sent_at) < timeout);
322 let cleaned = before - self.in_flight.len();
323 if cleaned > 0 {
324 self.consecutive_acks = 0;
326 self.current_batch_size = (self.current_batch_size / 2).max(self.config.min_batch_size);
327 }
328 cleaned
329 }
330
331 pub fn cleanup_stale_and_record(
336 &mut self,
337 timeout: std::time::Duration,
338 metrics: &SyncMetrics,
339 ) {
340 let cleaned = self.cleanup_stale(timeout);
341 if cleaned > 0 {
342 metrics.record_stale_timeouts(cleaned as u64);
343 tracing::warn!(
344 evicted = cleaned,
345 "stale in-flight entries evicted; AIMD batch-size decreased"
346 );
347 }
348 }
349
350 pub fn snapshot(&self, state: &'static str, metrics: &SyncMetrics) -> SyncMetricsSnapshot {
352 SyncMetricsSnapshot {
353 state,
354 pending_count: self.pending_count as u64,
355 pending_bytes: self.pending_bytes as u64,
356 deltas_pushed: metrics.deltas_pushed.load(Ordering::Relaxed),
357 deltas_received: metrics.deltas_received.load(Ordering::Relaxed),
358 deltas_rejected: metrics.deltas_rejected.load(Ordering::Relaxed),
359 avg_rtt_ms: self.avg_rtt_ms(),
360 in_flight: self.in_flight.len() as u64,
361 reconnect_count: metrics.reconnect_count.load(Ordering::Relaxed),
362 last_sync_ts: metrics.last_sync_ts.load(Ordering::Relaxed),
363 checksum_failures: metrics.checksum_failures.load(Ordering::Relaxed),
364 current_batch_size: self.current_batch_size as u64,
365 conflicts_total: metrics.conflicts_total.load(Ordering::Relaxed),
366 stale_timeouts: metrics.stale_timeouts.load(Ordering::Relaxed),
367 }
368 }
369
370 pub fn reset(&mut self) {
372 self.in_flight.clear();
373 self.consecutive_acks = 0;
374 }
377}
378
379impl Default for FlowController {
380 fn default() -> Self {
381 Self::new(FlowControlConfig::default())
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 #[test]
390 fn default_config_values() {
391 let cfg = FlowControlConfig::default();
392 assert_eq!(cfg.max_in_flight, 64);
393 assert_eq!(cfg.min_batch_size, 10);
394 assert_eq!(cfg.max_batch_size, 500);
395 assert_eq!(cfg.initial_batch_size, 50);
396 assert_eq!(cfg.max_pending_count, 10_000);
397 assert_eq!(cfg.max_pending_bytes, 50 * 1024 * 1024);
398 }
399
400 #[test]
401 fn flow_control_window() {
402 let mut fc = FlowController::new(FlowControlConfig {
403 max_in_flight: 3,
404 ..Default::default()
405 });
406
407 assert!(fc.can_push());
408 assert_eq!(fc.next_batch_size(), 3); fc.record_push(&[1, 2, 3]);
411 assert!(!fc.can_push());
412 assert_eq!(fc.next_batch_size(), 0);
413 assert_eq!(fc.in_flight_count(), 3);
414
415 fc.record_ack(2);
417 assert!(fc.can_push());
418 assert_eq!(fc.next_batch_size(), 1);
419 }
420
421 #[test]
422 fn aimd_additive_increase() {
423 let mut fc = FlowController::new(FlowControlConfig {
424 initial_batch_size: 10,
425 max_batch_size: 500,
426 max_in_flight: 1000,
427 ..Default::default()
428 });
429
430 assert_eq!(fc.current_batch_size(), 10);
431
432 for i in 0..8 {
434 fc.record_push(&[i]);
435 fc.record_ack(i);
436 }
437 assert_eq!(fc.current_batch_size(), 11);
438
439 for i in 8..16 {
441 fc.record_push(&[i]);
442 fc.record_ack(i);
443 }
444 assert_eq!(fc.current_batch_size(), 12);
445 }
446
447 #[test]
448 fn aimd_multiplicative_decrease() {
449 let mut fc = FlowController::new(FlowControlConfig {
450 initial_batch_size: 100,
451 min_batch_size: 10,
452 max_in_flight: 1000,
453 ..Default::default()
454 });
455
456 assert_eq!(fc.current_batch_size(), 100);
457
458 fc.record_push(&[1]);
460 fc.record_reject(1);
461 assert_eq!(fc.current_batch_size(), 50);
462
463 fc.record_push(&[2]);
465 fc.record_reject(2);
466 assert_eq!(fc.current_batch_size(), 25);
467
468 fc.record_push(&[3]);
470 fc.record_reject(3);
471 assert_eq!(fc.current_batch_size(), 12);
472
473 fc.record_push(&[4]);
474 fc.record_reject(4);
475 assert_eq!(fc.current_batch_size(), 10); fc.record_push(&[5]);
478 fc.record_reject(5);
479 assert_eq!(fc.current_batch_size(), 10); }
481
482 #[test]
483 fn rtt_ewma() {
484 let mut fc = FlowController::default();
485 fc.record_push(&[1]);
486 let rtt = fc.record_ack(1);
489 assert!(rtt.is_some());
490 assert!(fc.avg_rtt_ms() < 100); }
492
493 #[test]
494 fn bounded_pending_queue_count() {
495 let mut fc = FlowController::new(FlowControlConfig {
496 max_pending_count: 100,
497 max_pending_bytes: 1_000_000,
498 ..Default::default()
499 });
500
501 fc.update_pending(99, 500);
502 assert!(!fc.is_queue_full());
503
504 fc.update_pending(100, 500);
505 assert!(fc.is_queue_full());
506 }
507
508 #[test]
509 fn bounded_pending_queue_bytes() {
510 let mut fc = FlowController::new(FlowControlConfig {
511 max_pending_count: 100_000,
512 max_pending_bytes: 1000,
513 ..Default::default()
514 });
515
516 fc.update_pending(5, 999);
517 assert!(!fc.is_queue_full());
518
519 fc.update_pending(5, 1000);
520 assert!(fc.is_queue_full());
521 }
522
523 #[test]
524 fn cleanup_stale_in_flight() {
525 let mut fc = FlowController::new(FlowControlConfig {
526 initial_batch_size: 100,
527 min_batch_size: 10,
528 max_in_flight: 1000,
529 ..Default::default()
530 });
531
532 fc.record_push(&[1, 2, 3]);
533 assert_eq!(fc.in_flight_count(), 3);
534
535 let cleaned = fc.cleanup_stale(std::time::Duration::from_secs(3600));
537 assert_eq!(cleaned, 0);
538 assert_eq!(fc.in_flight_count(), 3);
539
540 let cleaned = fc.cleanup_stale(std::time::Duration::ZERO);
542 assert_eq!(cleaned, 3);
543 assert_eq!(fc.in_flight_count(), 0);
544 assert_eq!(fc.current_batch_size(), 50);
546 }
547
548 #[test]
549 fn reset_clears_in_flight() {
550 let mut fc = FlowController::default();
551 fc.record_push(&[1, 2, 3]);
552 assert_eq!(fc.in_flight_count(), 3);
553
554 fc.reset();
555 assert_eq!(fc.in_flight_count(), 0);
556 assert_eq!(fc.current_batch_size(), 50);
558 }
559
560 #[test]
561 fn metrics_snapshot() {
562 let mut fc = FlowController::default();
563 let metrics = SyncMetrics::new();
564 metrics.record_push(5);
565 metrics.record_received();
566 metrics.record_reject();
567 metrics.record_reconnect();
568
569 fc.update_pending(42, 8192);
570
571 let snap = fc.snapshot("connected", &metrics);
572 assert_eq!(snap.state, "connected");
573 assert_eq!(snap.pending_count, 42);
574 assert_eq!(snap.pending_bytes, 8192);
575 assert_eq!(snap.deltas_pushed, 5);
576 assert_eq!(snap.deltas_received, 1);
577 assert_eq!(snap.deltas_rejected, 1);
578 assert_eq!(snap.reconnect_count, 1);
579 assert_eq!(snap.current_batch_size, 50);
580 }
581
582 #[test]
583 fn cleanup_stale_removes_old_entries_and_halves_batch() {
584 let mut fc = FlowController::new(FlowControlConfig {
585 initial_batch_size: 80,
586 min_batch_size: 10,
587 max_in_flight: 1000,
588 ..Default::default()
589 });
590 let metrics = SyncMetrics::new();
591
592 fc.record_push(&[10, 20, 30]);
593 assert_eq!(fc.in_flight_count(), 3);
594
595 fc.cleanup_stale_and_record(std::time::Duration::ZERO, &metrics);
597 assert_eq!(fc.in_flight_count(), 0);
598 assert_eq!(fc.current_batch_size(), 40);
600 assert_eq!(
602 metrics
603 .stale_timeouts
604 .load(std::sync::atomic::Ordering::Relaxed),
605 3
606 );
607
608 fc.cleanup_stale_and_record(std::time::Duration::ZERO, &metrics);
610 assert_eq!(
611 metrics
612 .stale_timeouts
613 .load(std::sync::atomic::Ordering::Relaxed),
614 3
615 );
616 assert_eq!(fc.current_batch_size(), 40);
618 }
619
620 #[test]
621 fn ack_unknown_mutation_returns_none() {
622 let mut fc = FlowController::default();
623 assert!(fc.record_ack(999).is_none());
624 }
625
626 #[test]
627 fn batch_size_capped_at_max() {
628 let mut fc = FlowController::new(FlowControlConfig {
629 initial_batch_size: 499,
630 max_batch_size: 500,
631 max_in_flight: 10_000,
632 ..Default::default()
633 });
634
635 for i in 0..8 {
637 fc.record_push(&[i]);
638 fc.record_ack(i);
639 }
640 assert_eq!(fc.current_batch_size(), 500);
641
642 for i in 8..16 {
644 fc.record_push(&[i]);
645 fc.record_ack(i);
646 }
647 assert_eq!(fc.current_batch_size(), 500);
648 }
649}