1use std::collections::VecDeque;
21use std::path::PathBuf;
22use std::sync::Arc;
23
24use parking_lot::RwLock;
25
26use crate::segment::writer::{SegmentMeta, SegmentWriter, SegmentWriterConfig};
27use chronix_core::types::Point;
28
29use crate::memtable::error::{MemtableError, Result};
30use crate::memtable::memtable::Memtable;
31
32#[derive(Debug, Clone)]
34pub struct FlushConfig {
35 pub flush_threshold: usize,
37 pub max_memory: usize,
39 pub stall_soft_fraction: f64,
44 pub max_stall_delay: std::time::Duration,
47 pub max_frozen_memtables: usize,
51 pub segment_dir: PathBuf,
53 pub segment_writer_config: SegmentWriterConfig,
55}
56
57impl Default for FlushConfig {
58 fn default() -> Self {
59 Self {
60 flush_threshold: 64 * 1024 * 1024, max_memory: 256 * 1024 * 1024, stall_soft_fraction: 0.75,
63 max_stall_delay: std::time::Duration::from_millis(100),
64 max_frozen_memtables: 2,
65 segment_dir: PathBuf::from("/tmp/chronix/segments"),
66 segment_writer_config: SegmentWriterConfig::default(),
67 }
68 }
69}
70
71pub struct FlushController {
78 active: Arc<RwLock<Arc<Memtable>>>,
80 frozen: Arc<RwLock<VecDeque<Arc<Memtable>>>>,
82 config: FlushConfig,
84 segment_counter: std::sync::atomic::AtomicU64,
86 flush_lock: parking_lot::Mutex<()>,
89}
90
91impl std::fmt::Debug for FlushController {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 let active_len = self.active.read().len();
99 let has_frozen = !self.frozen.read().is_empty();
100 let total_memory = self.total_memory();
101 let counter = self
102 .segment_counter
103 .load(std::sync::atomic::Ordering::Relaxed);
104 f.debug_struct("FlushController")
105 .field("config", &self.config)
106 .field("active_len", &active_len)
107 .field("has_frozen", &has_frozen)
108 .field("frozen_count", &self.frozen.read().len())
109 .field("total_memory", &total_memory)
110 .field("segment_counter", &counter)
111 .finish()
112 }
113}
114
115impl FlushController {
116 #[must_use]
118 pub fn new(config: FlushConfig) -> Self {
119 Self {
120 active: Arc::new(RwLock::new(Arc::new(Memtable::new()))),
121 frozen: Arc::new(RwLock::new(VecDeque::new())),
122 config,
123 segment_counter: std::sync::atomic::AtomicU64::new(0),
124 flush_lock: parking_lot::Mutex::new(()),
125 }
126 }
127
128 #[must_use]
130 pub fn active_memtable(&self) -> Arc<Memtable> {
131 Arc::clone(&self.active.read())
132 }
133
134 #[must_use]
136 pub fn frozen_memtable(&self) -> Option<Arc<Memtable>> {
137 self.frozen.read().front().cloned()
138 }
139
140 pub fn insert(&self, point: &Point) -> Result<()> {
147 self.check_capacity()?;
148 self.active.read().insert(point)
149 }
150
151 pub fn insert_with_wal_seq(&self, point: &Point, wal_seq: u64) -> Result<()> {
166 self.active.read().insert_with_wal_seq(point, wal_seq)
167 }
168
169 #[must_use]
171 pub fn frozen_count(&self) -> usize {
172 self.frozen.read().len()
173 }
174
175 #[must_use]
178 pub fn is_idle(&self) -> bool {
179 self.active.read().is_empty() && self.frozen.read().is_empty()
180 }
181
182 #[must_use]
184 pub fn should_flush(&self) -> bool {
185 self.active.read().estimated_size() >= self.config.flush_threshold
186 }
187
188 pub fn freeze_and_swap(&self) -> Result<()> {
197 let mut frozen_guard = self.frozen.write();
198 if self.active.read().is_empty() {
202 return Ok(());
203 }
204 if frozen_guard.len() >= self.config.max_frozen_memtables {
206 let frozen_size: usize = frozen_guard.iter().map(|m| m.estimated_size()).sum();
207 return Err(MemtableError::CapacityExceeded {
208 current: frozen_size,
209 limit: self.config.max_memory,
210 });
211 }
212
213 let mut active_guard = self.active.write();
214
215 active_guard.freeze();
217 let old_active = Arc::clone(&active_guard);
218
219 *active_guard = Arc::new(Memtable::new());
221
222 frozen_guard.push_back(old_active);
224
225 Ok(())
226 }
227
228 pub fn flush_frozen(&self) -> Result<Vec<FlushResult>> {
237 self.flush_frozen_with(|_| Ok(()))
238 }
239
240 pub fn flush_frozen_with<F>(&self, mut register: F) -> Result<Vec<FlushResult>>
263 where
264 F: FnMut(&[FlushResult]) -> Result<()>,
265 {
266 let _one_at_a_time = self.flush_lock.lock();
267
268 let frozen_mt = self
269 .frozen
270 .read()
271 .front()
272 .cloned()
273 .ok_or(MemtableError::NoFrozenMemtable)?;
274
275 let batches = frozen_mt.to_record_batches();
276 if batches.is_empty() {
277 self.frozen.write().pop_front();
279 return Err(MemtableError::NoFrozenMemtable);
280 }
281
282 let max_wal_seq = frozen_mt.max_wal_seq();
283 let flush_started = std::time::Instant::now();
284
285 let mut results = Vec::with_capacity(batches.len());
286 let mut written_paths: Vec<std::path::PathBuf> = Vec::new();
287 let outcome = (|| -> Result<()> {
288 for mb in batches {
289 let max_rows_per_segment = estimate_max_rows(
293 mb.batch.num_columns(),
294 self.config.segment_writer_config.target_segment_size_bytes,
295 self.config.segment_writer_config.max_row_group_size,
296 );
297
298 let total = mb.batch.num_rows();
299 let mut offset = 0;
300 while offset < total {
301 let len = max_rows_per_segment.min(total - offset);
302 let slice = mb.batch.slice(offset, len);
303 offset += len;
304
305 let segment_path = self.next_segment_path();
306 let mut writer = SegmentWriter::new(
307 &segment_path,
308 self.config.segment_writer_config.clone(),
309 )?;
310 let segment_meta =
311 writer.finalize_batch(&slice, &mb.measurement, &mb.tag_columns)?;
312 written_paths.push(segment_path);
313 results.push(FlushResult {
314 measurement: mb.measurement.clone(),
315 series_keys: segment_meta.series_keys.clone(),
316 segment_meta,
317 max_wal_seq,
318 points_flushed: len,
319 });
320 }
321 }
322 register(&results)
323 })();
324
325 match outcome {
326 Ok(()) => {
327 let popped = self.frozen.write().pop_front();
328 debug_assert!(
329 popped.is_some_and(|m| Arc::ptr_eq(&m, &frozen_mt)),
330 "the flush lock keeps the head of the frozen queue stable"
331 );
332 metrics::counter!("chronix_memtable_flushes_total", "outcome" => "ok").increment(1);
337 metrics::histogram!("chronix_memtable_flush_duration_seconds")
338 .record(flush_started.elapsed().as_secs_f64());
339 Ok(results)
340 }
341 Err(e) => {
342 metrics::counter!("chronix_memtable_flushes_total", "outcome" => "error")
343 .increment(1);
344 tracing::error!(
345 error = %e,
346 segments_written = written_paths.len(),
347 "flush failed — the memtable stays frozen for the next attempt"
348 );
349 for path in written_paths {
350 let _ = std::fs::remove_file(&path);
351 }
352 Err(e)
353 }
354 }
355 }
356
357 #[must_use]
369 pub fn scan(
370 &self,
371 series_key: &chronix_core::SeriesKey,
372 min_ts: i64,
373 max_ts: i64,
374 ) -> Vec<Point> {
375 let (active, frozen_list) = self.snapshot_memtables();
376
377 let mut points = active.scan(series_key, min_ts, max_ts);
378
379 for frozen_mt in &frozen_list {
381 let frozen_points = frozen_mt.scan(series_key, min_ts, max_ts);
382 points = merge_points(points, frozen_points);
383 }
384
385 points
386 }
387
388 #[must_use]
390 pub fn scan_all(&self) -> Vec<Point> {
391 let (active, frozen_list) = self.snapshot_memtables();
392
393 let mut all_points = active.scan_all();
394
395 for frozen_mt in &frozen_list {
396 let frozen_points = frozen_mt.scan_all();
397 all_points = merge_points(all_points, frozen_points);
398 }
399
400 all_points
401 }
402
403 #[must_use]
407 pub fn scan_measurement(&self, measurement: &str, min_ts: i64, max_ts: i64) -> Vec<Point> {
408 let (active, frozen_list) = self.snapshot_memtables();
409
410 let mut points = active.scan_measurement(measurement, min_ts, max_ts);
411
412 for frozen_mt in &frozen_list {
413 let frozen_points = frozen_mt.scan_measurement(measurement, min_ts, max_ts);
414 points = merge_points(points, frozen_points);
415 }
416
417 points
418 }
419
420 #[must_use]
422 pub fn total_memory(&self) -> usize {
423 let active_size = self.active.read().estimated_size();
424 let frozen_size: usize = self.frozen.read().iter().map(|m| m.estimated_size()).sum();
425 active_size + frozen_size
426 }
427
428 #[must_use]
430 pub fn total_interner_memory(&self) -> usize {
431 let active = self.active.read().interner_bytes();
432 let frozen: usize = self.frozen.read().iter().map(|m| m.interner_bytes()).sum();
433 active + frozen
434 }
435
436 #[must_use]
444 pub fn min_unflushed_wal_seq(&self) -> Option<u64> {
445 let frozen = self.frozen.read();
446 let active = self.active.read();
447 active
448 .min_wal_seq()
449 .into_iter()
450 .chain(frozen.iter().filter_map(|m| m.min_wal_seq()))
451 .min()
452 }
453
454 fn check_capacity(&self) -> Result<()> {
456 let total = self.total_memory();
457 if total >= self.config.max_memory {
458 return Err(MemtableError::CapacityExceeded {
459 current: total,
460 limit: self.config.max_memory,
461 });
462 }
463 Ok(())
464 }
465
466 fn snapshot_memtables(&self) -> (Arc<Memtable>, Vec<Arc<Memtable>>) {
470 let frozen_guard = self.frozen.read();
471 let active_guard = self.active.read();
472 (
473 Arc::clone(&active_guard),
474 frozen_guard.iter().cloned().collect(),
475 )
476 }
477
478 #[must_use]
489 pub fn write_stall_delay(&self) -> std::time::Duration {
490 let total = self.total_memory();
491 let soft_limit = (self.config.max_memory as f64 * self.config.stall_soft_fraction) as usize;
492
493 if total <= soft_limit {
494 return std::time::Duration::ZERO;
495 }
496
497 let hard = self.config.max_memory;
498 let range = hard.saturating_sub(soft_limit);
499 if range == 0 {
500 return self.config.max_stall_delay;
501 }
502
503 let pressure = total.saturating_sub(soft_limit) as f64 / range as f64;
504 let pressure = pressure.min(1.0);
505 self.config.max_stall_delay.mul_f64(pressure)
506 }
507
508 fn next_segment_path(&self) -> PathBuf {
510 let counter = self
511 .segment_counter
512 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
513 let now_ns = std::time::SystemTime::now()
514 .duration_since(std::time::UNIX_EPOCH)
515 .map(|d| d.as_nanos())
516 .unwrap_or(0);
517 self.config
518 .segment_dir
519 .join(format!("seg_{now_ns}_{counter:06}.csx"))
520 }
521}
522
523#[derive(Debug)]
525pub struct FlushResult {
526 pub measurement: String,
528 pub segment_meta: SegmentMeta,
530 pub max_wal_seq: Option<u64>,
533 pub points_flushed: usize,
535 pub series_keys: Vec<chronix_core::SeriesKey>,
537}
538
539fn merge_points(primary: Vec<Point>, secondary: Vec<Point>) -> Vec<Point> {
548 use std::collections::BTreeMap as Map;
549
550 let mut map: Map<(u64, i64, String), Point> = Map::new();
554
555 for p in secondary {
556 let key = (
557 p.series_key().hash_fnv(),
558 p.timestamp(),
559 p.series_key().canonical_form().to_string(),
560 );
561 map.insert(key, p);
562 }
563 for p in primary {
565 let key = (
566 p.series_key().hash_fnv(),
567 p.timestamp(),
568 p.series_key().canonical_form().to_string(),
569 );
570 map.insert(key, p);
571 }
572
573 map.into_values().collect()
574}
575
576fn estimate_max_rows(
584 num_columns: usize,
585 target_segment_size_bytes: usize,
586 min_rows: usize,
587) -> usize {
588 let est_bytes_per_row = (num_columns * 4).max(8);
592 let max_rows = target_segment_size_bytes / est_bytes_per_row;
593 max_rows.max(min_rows)
595}
596
597#[cfg(test)]
598mod tests {
599 use super::*;
600 use std::collections::BTreeMap;
601 use std::path::Path;
602
603 use chronix_core::types::{FieldValue, SeriesKey};
604
605 fn make_point(host: &str, ts: i64, value: f64) -> Point {
606 make_point_with_measurement("cpu", host, ts, value)
607 }
608
609 fn make_point_with_measurement(measurement: &str, host: &str, ts: i64, value: f64) -> Point {
610 let tags: BTreeMap<String, String> = [("host".to_string(), host.to_string())]
611 .into_iter()
612 .collect();
613 let series_key = SeriesKey::new(measurement.to_string(), tags).unwrap();
614 let fields: BTreeMap<String, FieldValue> = [("value".to_string(), FieldValue::F64(value))]
615 .into_iter()
616 .collect();
617 Point::new(series_key, fields, ts).unwrap()
618 }
619
620 fn test_config(dir: &Path) -> FlushConfig {
621 FlushConfig {
622 flush_threshold: 1024, max_memory: 1024 * 1024,
624 segment_dir: dir.to_path_buf(),
625 segment_writer_config: SegmentWriterConfig {
626 compress: false,
627 ..Default::default()
628 },
629 ..Default::default()
630 }
631 }
632
633 #[test]
634 fn insert_and_read_back() {
635 let dir = tempfile::tempdir().unwrap();
636 let fc = FlushController::new(test_config(dir.path()));
637
638 fc.insert(&make_point("a", 100, 1.0)).unwrap();
639 fc.insert(&make_point("a", 200, 2.0)).unwrap();
640
641 let all = fc.scan_all();
642 assert_eq!(all.len(), 2);
643 }
644
645 #[test]
646 fn freeze_and_swap() {
647 let dir = tempfile::tempdir().unwrap();
648 let fc = FlushController::new(test_config(dir.path()));
649
650 fc.insert(&make_point("a", 100, 1.0)).unwrap();
651 fc.insert(&make_point("a", 200, 2.0)).unwrap();
652
653 fc.freeze_and_swap().unwrap();
654
655 assert!(fc.active_memtable().is_empty());
657 assert!(fc.frozen_memtable().is_some());
658 assert_eq!(fc.frozen_memtable().unwrap().len(), 2);
659
660 fc.insert(&make_point("a", 300, 3.0)).unwrap();
662 assert_eq!(fc.active_memtable().len(), 1);
663 }
664
665 #[test]
666 fn scan_merges_active_and_frozen() {
667 let dir = tempfile::tempdir().unwrap();
668 let fc = FlushController::new(test_config(dir.path()));
669
670 fc.insert(&make_point("a", 100, 1.0)).unwrap();
671 fc.insert(&make_point("a", 200, 2.0)).unwrap();
672 fc.freeze_and_swap().unwrap();
673 fc.insert(&make_point("a", 300, 3.0)).unwrap();
674
675 let all = fc.scan_all();
676 assert_eq!(all.len(), 3);
677 }
678
679 #[test]
680 fn active_wins_on_duplicate() {
681 let dir = tempfile::tempdir().unwrap();
682 let fc = FlushController::new(test_config(dir.path()));
683
684 fc.insert(&make_point("a", 100, 1.0)).unwrap();
685 fc.freeze_and_swap().unwrap();
686 fc.insert(&make_point("a", 100, 99.0)).unwrap();
687
688 let all = fc.scan_all();
689 assert_eq!(all.len(), 1);
690 assert_eq!(all[0].field("value"), Some(&FieldValue::F64(99.0)));
691 }
692
693 #[test]
694 fn flush_frozen_writes_segment() {
695 let dir = tempfile::tempdir().unwrap();
696 let fc = FlushController::new(test_config(dir.path()));
697
698 for i in 0..10 {
699 fc.insert(&make_point("a", i * 100, i as f64)).unwrap();
700 }
701
702 fc.freeze_and_swap().unwrap();
703 let results = fc.flush_frozen().unwrap();
704
705 assert_eq!(results.len(), 1);
706 let result = &results[0];
707 assert_eq!(result.measurement, "cpu");
708 assert_eq!(result.points_flushed, 10);
709 assert!(result.segment_meta.path.exists());
710 assert_eq!(result.segment_meta.row_count, 10);
711
712 assert!(fc.frozen_memtable().is_none());
714 }
715
716 #[test]
717 fn should_flush_threshold() {
718 let dir = tempfile::tempdir().unwrap();
719 let mut config = test_config(dir.path());
720 config.flush_threshold = 100; let fc = FlushController::new(config);
722
723 assert!(!fc.should_flush());
724
725 for i in 0..20 {
727 let _ = fc.insert(&make_point("a", i, i as f64));
728 }
729 assert!(fc.should_flush());
730 }
731
732 #[test]
733 fn multi_freeze_respects_limit() {
734 let dir = tempfile::tempdir().unwrap();
735 let fc = FlushController::new(test_config(dir.path()));
736
737 fc.insert(&make_point("a", 100, 1.0)).unwrap();
739 fc.freeze_and_swap().unwrap();
740
741 fc.insert(&make_point("a", 200, 2.0)).unwrap();
742 fc.freeze_and_swap()
743 .expect("second freeze should succeed with multi-slot frozen queue");
744
745 fc.insert(&make_point("a", 300, 3.0)).unwrap();
747 let result = fc.freeze_and_swap();
748 assert!(result.is_err(), "should reject third freeze at capacity");
749 }
750
751 #[test]
752 fn capacity_rejection() {
753 let dir = tempfile::tempdir().unwrap();
754 let mut config = test_config(dir.path());
755 config.max_memory = 1; let fc = FlushController::new(config);
757
758 let _ = fc.insert(&make_point("a", 100, 1.0));
761 let result = fc.insert(&make_point("a", 200, 2.0));
762 assert!(result.is_err());
763 }
764
765 #[test]
766 fn wal_seq_in_flush_result() {
767 let dir = tempfile::tempdir().unwrap();
768 let fc = FlushController::new(test_config(dir.path()));
769
770 fc.insert_with_wal_seq(&make_point("a", 100, 1.0), 5)
771 .unwrap();
772 fc.insert_with_wal_seq(&make_point("a", 200, 2.0), 10)
773 .unwrap();
774
775 fc.freeze_and_swap().unwrap();
776 let results = fc.flush_frozen().unwrap();
777
778 assert_eq!(results.len(), 1);
779 assert_eq!(results[0].max_wal_seq, Some(10));
780 }
781
782 #[test]
783 fn merge_points_dedup() {
784 let primary = vec![make_point("a", 100, 10.0), make_point("a", 300, 30.0)];
785 let secondary = vec![
786 make_point("a", 100, 1.0), make_point("a", 200, 2.0),
788 ];
789
790 let merged = merge_points(primary, secondary);
791 assert_eq!(merged.len(), 3);
792
793 let ts100 = merged.iter().find(|p| p.timestamp() == 100).unwrap();
795 assert_eq!(ts100.field("value"), Some(&FieldValue::F64(10.0)));
796 }
797
798 #[test]
799 fn scan_measurement_filters_active() {
800 let dir = tempfile::tempdir().unwrap();
801 let fc = FlushController::new(test_config(dir.path()));
802
803 fc.insert(&make_point_with_measurement("cpu", "a", 100, 1.0))
804 .unwrap();
805 fc.insert(&make_point_with_measurement("mem", "a", 200, 2.0))
806 .unwrap();
807 fc.insert(&make_point_with_measurement("cpu", "b", 300, 3.0))
808 .unwrap();
809
810 let cpu = fc.scan_measurement("cpu", 0, i64::MAX);
811 assert_eq!(cpu.len(), 2);
812 assert!(cpu.iter().all(|p| p.series_key().measurement() == "cpu"));
813
814 let mem = fc.scan_measurement("mem", 0, i64::MAX);
815 assert_eq!(mem.len(), 1);
816 assert_eq!(mem[0].series_key().measurement(), "mem");
817
818 let none = fc.scan_measurement("disk", 0, i64::MAX);
819 assert!(none.is_empty());
820 }
821
822 #[test]
823 fn scan_measurement_merges_active_and_frozen() {
824 let dir = tempfile::tempdir().unwrap();
825 let fc = FlushController::new(test_config(dir.path()));
826
827 fc.insert(&make_point_with_measurement("cpu", "a", 100, 1.0))
829 .unwrap();
830 fc.insert(&make_point_with_measurement("mem", "a", 150, 9.0))
831 .unwrap();
832 fc.freeze_and_swap().unwrap();
833
834 fc.insert(&make_point_with_measurement("cpu", "a", 200, 2.0))
836 .unwrap();
837 fc.insert(&make_point_with_measurement("mem", "b", 250, 8.0))
838 .unwrap();
839
840 let cpu = fc.scan_measurement("cpu", 0, i64::MAX);
841 assert_eq!(cpu.len(), 2);
842 assert_eq!(cpu[0].timestamp(), 100);
843 assert_eq!(cpu[1].timestamp(), 200);
844
845 let mem = fc.scan_measurement("mem", 0, i64::MAX);
846 assert_eq!(mem.len(), 2);
847 }
848
849 #[test]
850 fn min_unflushed_wal_seq_empty() {
851 let fc = FlushController::new(FlushConfig::default());
852 assert!(fc.min_unflushed_wal_seq().is_none());
853 }
854
855 #[test]
856 fn min_unflushed_wal_seq_after_insert() {
857 let fc = FlushController::new(FlushConfig::default());
858 let p = make_point("host", 100, 1.0);
859 fc.insert_with_wal_seq(&p, 42).unwrap();
860 fc.insert_with_wal_seq(&p, 50).unwrap();
861 assert_eq!(fc.min_unflushed_wal_seq(), Some(42));
862 }
863
864 #[test]
869 fn a_frozen_memtable_still_counts_as_unflushed() {
870 let fc = FlushController::new(FlushConfig::default());
871 let p = make_point("host", 100, 1.0);
872 fc.insert_with_wal_seq(&p, 10).unwrap();
873 fc.freeze_and_swap().unwrap();
874 assert_eq!(
875 fc.min_unflushed_wal_seq(),
876 Some(10),
877 "the frozen memtable holds seq 10 and nothing flushed it"
878 );
879 fc.insert_with_wal_seq(&make_point("host", 200, 2.0), 20)
880 .unwrap();
881 assert_eq!(fc.min_unflushed_wal_seq(), Some(10));
882 }
883
884 #[test]
888 fn a_failed_flush_keeps_the_frozen_memtable() {
889 let dir = tempfile::tempdir().unwrap();
890 let mut config = test_config(dir.path());
891 let blocked = dir.path().join("blocked");
893 std::fs::write(&blocked, b"not a directory").unwrap();
894 config.segment_dir = blocked.clone();
895 let fc = FlushController::new(config);
896
897 fc.insert_with_wal_seq(&make_point("a", 100, 1.0), 7)
898 .unwrap();
899 fc.freeze_and_swap().unwrap();
900 assert!(fc.flush_frozen().is_err());
901 assert!(
902 fc.frozen_memtable().is_some(),
903 "the memtable must survive a failed flush"
904 );
905 assert_eq!(fc.min_unflushed_wal_seq(), Some(7));
906
907 std::fs::remove_file(&blocked).unwrap();
910 std::fs::create_dir_all(&blocked).unwrap();
911 let err = fc.flush_frozen_with(|results| {
912 assert_eq!(results.len(), 1);
913 assert!(results[0].segment_meta.path.exists());
914 Err(MemtableError::NoFrozenMemtable)
915 });
916 assert!(err.is_err());
917 assert!(fc.frozen_memtable().is_some());
918 assert_eq!(
919 std::fs::read_dir(&blocked).unwrap().count(),
920 0,
921 "a segment nothing registered is removed, not left as an orphan"
922 );
923
924 let results = fc.flush_frozen().unwrap();
926 assert_eq!(results[0].points_flushed, 1);
927 assert!(fc.frozen_memtable().is_none());
928 assert!(fc.min_unflushed_wal_seq().is_none());
929 }
930
931 #[test]
932 fn write_stall_zero_below_soft_limit() {
933 let dir = tempfile::tempdir().unwrap();
934 let config = FlushConfig {
935 max_memory: 1024 * 1024,
936 stall_soft_fraction: 0.75,
937 max_stall_delay: std::time::Duration::from_millis(100),
938 segment_dir: dir.path().to_path_buf(),
939 ..Default::default()
940 };
941 let fc = FlushController::new(config);
942
943 assert_eq!(fc.write_stall_delay(), std::time::Duration::ZERO);
945 }
946
947 #[test]
948 fn write_stall_increases_with_pressure() {
949 let dir = tempfile::tempdir().unwrap();
950 let config = FlushConfig {
951 max_memory: 10_000, stall_soft_fraction: 0.5, max_stall_delay: std::time::Duration::from_millis(100),
954 flush_threshold: 10_000, segment_dir: dir.path().to_path_buf(),
956 ..Default::default()
957 };
958 let fc = FlushController::new(config);
959
960 for i in 0..100 {
962 let _ = fc.insert(&make_point("a", i, i as f64));
963 }
964
965 let delay = fc.write_stall_delay();
966 let mem = fc.total_memory();
968 if mem > 5000 {
969 assert!(
970 delay > std::time::Duration::ZERO,
971 "should stall when over soft limit (mem={mem})"
972 );
973 assert!(
974 delay <= std::time::Duration::from_millis(100),
975 "should not exceed max stall delay"
976 );
977 }
978 }
979}