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]
373 pub fn scan(
374 &self,
375 series_key: &chronix_core::SeriesKey,
376 min_ts: i64,
377 max_ts: i64,
378 ) -> Vec<Point> {
379 let (active, frozen_list) = self.snapshot_memtables();
380
381 let mut sources = Vec::with_capacity(frozen_list.len() + 1);
384 sources.push(active.scan(series_key, min_ts, max_ts));
385 for frozen_mt in frozen_list.iter().rev() {
386 sources.push(frozen_mt.scan(series_key, min_ts, max_ts));
387 }
388 merge_newest_first(sources)
389 }
390
391 #[must_use]
396 pub fn scan_all(&self) -> Vec<Point> {
397 let (active, frozen_list) = self.snapshot_memtables();
398
399 let mut sources = Vec::with_capacity(frozen_list.len() + 1);
400 sources.push(active.scan_all());
401 for frozen_mt in frozen_list.iter().rev() {
402 sources.push(frozen_mt.scan_all());
403 }
404 merge_newest_first(sources)
405 }
406
407 #[must_use]
417 pub fn scan_measurement(&self, measurement: &str, min_ts: i64, max_ts: i64) -> Vec<Point> {
418 let (active, frozen_list) = self.snapshot_memtables();
419
420 let mut sources = Vec::with_capacity(frozen_list.len() + 1);
421 sources.push(active.scan_measurement(measurement, min_ts, max_ts));
422 for frozen_mt in frozen_list.iter().rev() {
423 sources.push(frozen_mt.scan_measurement(measurement, min_ts, max_ts));
424 }
425 merge_newest_first(sources)
426 }
427
428 #[must_use]
430 pub fn all_series_canonical_forms(&self) -> Vec<std::sync::Arc<str>> {
431 let mut out = self.active.read().series_canonical_forms();
432 for frozen in self.frozen.read().iter() {
433 out.extend(frozen.series_canonical_forms());
434 }
435 out
436 }
437
438 pub fn visit_tags(&self, f: &mut impl FnMut(&str, &str)) {
440 self.active.read().visit_tags(&mut *f);
441 for frozen in self.frozen.read().iter() {
442 frozen.visit_tags(&mut *f);
443 }
444 }
445
446 #[must_use]
448 pub fn total_memory(&self) -> usize {
449 let active_size = self.active.read().estimated_size();
450 let frozen_size: usize = self.frozen.read().iter().map(|m| m.estimated_size()).sum();
451 active_size + frozen_size
452 }
453
454 #[must_use]
456 pub fn total_interner_memory(&self) -> usize {
457 let active = self.active.read().interner_bytes();
458 let frozen: usize = self.frozen.read().iter().map(|m| m.interner_bytes()).sum();
459 active + frozen
460 }
461
462 #[must_use]
470 pub fn min_unflushed_wal_seq(&self) -> Option<u64> {
471 let frozen = self.frozen.read();
472 let active = self.active.read();
473 active
474 .min_wal_seq()
475 .into_iter()
476 .chain(frozen.iter().filter_map(|m| m.min_wal_seq()))
477 .min()
478 }
479
480 fn check_capacity(&self) -> Result<()> {
482 let total = self.total_memory();
483 if total >= self.config.max_memory {
484 return Err(MemtableError::CapacityExceeded {
485 current: total,
486 limit: self.config.max_memory,
487 });
488 }
489 Ok(())
490 }
491
492 fn snapshot_memtables(&self) -> (Arc<Memtable>, Vec<Arc<Memtable>>) {
496 let frozen_guard = self.frozen.read();
497 let active_guard = self.active.read();
498 (
499 Arc::clone(&active_guard),
500 frozen_guard.iter().cloned().collect(),
501 )
502 }
503
504 #[must_use]
515 pub fn write_stall_delay(&self) -> std::time::Duration {
516 let total = self.total_memory();
517 let soft_limit = (self.config.max_memory as f64 * self.config.stall_soft_fraction) as usize;
518
519 if total <= soft_limit {
520 return std::time::Duration::ZERO;
521 }
522
523 let hard = self.config.max_memory;
524 let range = hard.saturating_sub(soft_limit);
525 if range == 0 {
526 return self.config.max_stall_delay;
527 }
528
529 let pressure = total.saturating_sub(soft_limit) as f64 / range as f64;
530 let pressure = pressure.min(1.0);
531 self.config.max_stall_delay.mul_f64(pressure)
532 }
533
534 fn next_segment_path(&self) -> PathBuf {
536 let counter = self
537 .segment_counter
538 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
539 let now_ns = std::time::SystemTime::now()
540 .duration_since(std::time::UNIX_EPOCH)
541 .map(|d| d.as_nanos())
542 .unwrap_or(0);
543 self.config
544 .segment_dir
545 .join(format!("seg_{now_ns}_{counter:06}.csx"))
546 }
547}
548
549#[derive(Debug)]
551pub struct FlushResult {
552 pub measurement: String,
554 pub segment_meta: SegmentMeta,
556 pub max_wal_seq: Option<u64>,
559 pub points_flushed: usize,
561 pub series_keys: Vec<chronix_core::SeriesKey>,
563}
564
565fn merge_newest_first(sources: Vec<Vec<Point>>) -> Vec<Point> {
583 let total: usize = sources.iter().map(Vec::len).sum();
584 let mut all = Vec::with_capacity(total);
585 for src in sources {
586 all.extend(src);
587 }
588
589 let mut seen = std::collections::HashSet::with_capacity(total);
594 let mut keep = Vec::with_capacity(total);
595 for p in &all {
596 keep.push(seen.insert((p.series_key().canonical_form(), p.timestamp())));
597 }
598 drop(seen);
599
600 let mut keep = keep.into_iter();
601 all.retain(|_| keep.next().unwrap_or(false));
602 all
603}
604
605fn estimate_max_rows(
613 num_columns: usize,
614 target_segment_size_bytes: usize,
615 min_rows: usize,
616) -> usize {
617 let est_bytes_per_row = (num_columns * 4).max(8);
621 let max_rows = target_segment_size_bytes / est_bytes_per_row;
622 max_rows.max(min_rows)
624}
625
626#[cfg(test)]
627mod tests {
628 use super::*;
629 use std::collections::BTreeMap;
630 use std::path::Path;
631
632 use chronix_core::types::{FieldValue, SeriesKey};
633
634 fn make_point(host: &str, ts: i64, value: f64) -> Point {
635 make_point_with_measurement("cpu", host, ts, value)
636 }
637
638 fn make_point_with_measurement(measurement: &str, host: &str, ts: i64, value: f64) -> Point {
639 let tags: BTreeMap<String, String> = [("host".to_string(), host.to_string())]
640 .into_iter()
641 .collect();
642 let series_key = SeriesKey::new(measurement.to_string(), tags).unwrap();
643 let fields: BTreeMap<String, FieldValue> = [("value".to_string(), FieldValue::F64(value))]
644 .into_iter()
645 .collect();
646 Point::new(series_key, fields, ts).unwrap()
647 }
648
649 fn test_config(dir: &Path) -> FlushConfig {
650 FlushConfig {
651 flush_threshold: 1024, max_memory: 1024 * 1024,
653 segment_dir: dir.to_path_buf(),
654 segment_writer_config: SegmentWriterConfig {
655 compress: false,
656 ..Default::default()
657 },
658 ..Default::default()
659 }
660 }
661
662 #[test]
663 fn insert_and_read_back() {
664 let dir = tempfile::tempdir().unwrap();
665 let fc = FlushController::new(test_config(dir.path()));
666
667 fc.insert(&make_point("a", 100, 1.0)).unwrap();
668 fc.insert(&make_point("a", 200, 2.0)).unwrap();
669
670 let all = fc.scan_all();
671 assert_eq!(all.len(), 2);
672 }
673
674 #[test]
675 fn freeze_and_swap() {
676 let dir = tempfile::tempdir().unwrap();
677 let fc = FlushController::new(test_config(dir.path()));
678
679 fc.insert(&make_point("a", 100, 1.0)).unwrap();
680 fc.insert(&make_point("a", 200, 2.0)).unwrap();
681
682 fc.freeze_and_swap().unwrap();
683
684 assert!(fc.active_memtable().is_empty());
686 assert!(fc.frozen_memtable().is_some());
687 assert_eq!(fc.frozen_memtable().unwrap().len(), 2);
688
689 fc.insert(&make_point("a", 300, 3.0)).unwrap();
691 assert_eq!(fc.active_memtable().len(), 1);
692 }
693
694 #[test]
695 fn scan_merges_active_and_frozen() {
696 let dir = tempfile::tempdir().unwrap();
697 let fc = FlushController::new(test_config(dir.path()));
698
699 fc.insert(&make_point("a", 100, 1.0)).unwrap();
700 fc.insert(&make_point("a", 200, 2.0)).unwrap();
701 fc.freeze_and_swap().unwrap();
702 fc.insert(&make_point("a", 300, 3.0)).unwrap();
703
704 let all = fc.scan_all();
705 assert_eq!(all.len(), 3);
706 }
707
708 #[test]
709 fn active_wins_on_duplicate() {
710 let dir = tempfile::tempdir().unwrap();
711 let fc = FlushController::new(test_config(dir.path()));
712
713 fc.insert(&make_point("a", 100, 1.0)).unwrap();
714 fc.freeze_and_swap().unwrap();
715 fc.insert(&make_point("a", 100, 99.0)).unwrap();
716
717 let all = fc.scan_all();
718 assert_eq!(all.len(), 1);
719 assert_eq!(all[0].field("value"), Some(&FieldValue::F64(99.0)));
720 }
721
722 #[test]
723 fn flush_frozen_writes_segment() {
724 let dir = tempfile::tempdir().unwrap();
725 let fc = FlushController::new(test_config(dir.path()));
726
727 for i in 0..10 {
728 fc.insert(&make_point("a", i * 100, i as f64)).unwrap();
729 }
730
731 fc.freeze_and_swap().unwrap();
732 let results = fc.flush_frozen().unwrap();
733
734 assert_eq!(results.len(), 1);
735 let result = &results[0];
736 assert_eq!(result.measurement, "cpu");
737 assert_eq!(result.points_flushed, 10);
738 assert!(result.segment_meta.path.exists());
739 assert_eq!(result.segment_meta.row_count, 10);
740
741 assert!(fc.frozen_memtable().is_none());
743 }
744
745 #[test]
746 fn should_flush_threshold() {
747 let dir = tempfile::tempdir().unwrap();
748 let mut config = test_config(dir.path());
749 config.flush_threshold = 100; let fc = FlushController::new(config);
751
752 assert!(!fc.should_flush());
753
754 for i in 0..20 {
756 let _ = fc.insert(&make_point("a", i, i as f64));
757 }
758 assert!(fc.should_flush());
759 }
760
761 #[test]
762 fn multi_freeze_respects_limit() {
763 let dir = tempfile::tempdir().unwrap();
764 let fc = FlushController::new(test_config(dir.path()));
765
766 fc.insert(&make_point("a", 100, 1.0)).unwrap();
768 fc.freeze_and_swap().unwrap();
769
770 fc.insert(&make_point("a", 200, 2.0)).unwrap();
771 fc.freeze_and_swap()
772 .expect("second freeze should succeed with multi-slot frozen queue");
773
774 fc.insert(&make_point("a", 300, 3.0)).unwrap();
776 let result = fc.freeze_and_swap();
777 assert!(result.is_err(), "should reject third freeze at capacity");
778 }
779
780 #[test]
781 fn capacity_rejection() {
782 let dir = tempfile::tempdir().unwrap();
783 let mut config = test_config(dir.path());
784 config.max_memory = 1; let fc = FlushController::new(config);
786
787 let _ = fc.insert(&make_point("a", 100, 1.0));
790 let result = fc.insert(&make_point("a", 200, 2.0));
791 assert!(result.is_err());
792 }
793
794 #[test]
795 fn wal_seq_in_flush_result() {
796 let dir = tempfile::tempdir().unwrap();
797 let fc = FlushController::new(test_config(dir.path()));
798
799 fc.insert_with_wal_seq(&make_point("a", 100, 1.0), 5)
800 .unwrap();
801 fc.insert_with_wal_seq(&make_point("a", 200, 2.0), 10)
802 .unwrap();
803
804 fc.freeze_and_swap().unwrap();
805 let results = fc.flush_frozen().unwrap();
806
807 assert_eq!(results.len(), 1);
808 assert_eq!(results[0].max_wal_seq, Some(10));
809 }
810
811 #[test]
812 fn merging_keeps_the_first_source_that_has_a_key() {
813 let merged = merge_newest_first(vec![
817 vec![make_point("a", 100, 10.0), make_point("a", 300, 30.0)],
818 vec![make_point("a", 100, 1.0), make_point("a", 200, 2.0)],
819 vec![make_point("a", 200, 99.0)],
820 ]);
821 assert_eq!(merged.len(), 3);
822
823 let ts100 = merged.iter().find(|p| p.timestamp() == 100).unwrap();
824 assert_eq!(ts100.field("value"), Some(&FieldValue::F64(10.0)));
825 let ts200 = merged.iter().find(|p| p.timestamp() == 200).unwrap();
826 assert_eq!(
827 ts200.field("value"),
828 Some(&FieldValue::F64(2.0)),
829 "the second source wins over the third"
830 );
831 }
832
833 #[test]
842 fn the_newest_frozen_memtable_wins_a_duplicate() {
843 let dir = tempfile::tempdir().unwrap();
844 let fc = FlushController::new(test_config(dir.path()));
845
846 fc.insert(&make_point("a", 100, 1.0)).unwrap();
849 fc.freeze_and_swap().unwrap();
850 fc.insert(&make_point("a", 100, 2.0)).unwrap();
851 fc.freeze_and_swap().unwrap();
852
853 let key = make_point("a", 100, 0.0).series_key().clone();
854 for (path, points) in [
855 ("scan", fc.scan(&key, 0, i64::MAX)),
856 ("scan_all", fc.scan_all()),
857 (
858 "scan_measurement",
859 fc.scan_measurement(key.measurement(), 0, i64::MAX),
860 ),
861 ] {
862 assert_eq!(points.len(), 1, "{path}: one point survives dedup");
863 assert_eq!(
864 points[0].field("value"),
865 Some(&FieldValue::F64(2.0)),
866 "{path}: the newer write wins"
867 );
868 }
869 }
870
871 #[test]
873 fn the_active_memtable_wins_a_duplicate() {
874 let dir = tempfile::tempdir().unwrap();
875 let fc = FlushController::new(test_config(dir.path()));
876 fc.insert(&make_point("a", 100, 1.0)).unwrap();
877 fc.freeze_and_swap().unwrap();
878 fc.insert(&make_point("a", 100, 9.0)).unwrap();
879
880 let points = fc.scan_all();
881 assert_eq!(points.len(), 1);
882 assert_eq!(points[0].field("value"), Some(&FieldValue::F64(9.0)));
883 }
884
885 #[test]
886 fn scan_measurement_filters_active() {
887 let dir = tempfile::tempdir().unwrap();
888 let fc = FlushController::new(test_config(dir.path()));
889
890 fc.insert(&make_point_with_measurement("cpu", "a", 100, 1.0))
891 .unwrap();
892 fc.insert(&make_point_with_measurement("mem", "a", 200, 2.0))
893 .unwrap();
894 fc.insert(&make_point_with_measurement("cpu", "b", 300, 3.0))
895 .unwrap();
896
897 let cpu = fc.scan_measurement("cpu", 0, i64::MAX);
898 assert_eq!(cpu.len(), 2);
899 assert!(cpu.iter().all(|p| p.series_key().measurement() == "cpu"));
900
901 let mem = fc.scan_measurement("mem", 0, i64::MAX);
902 assert_eq!(mem.len(), 1);
903 assert_eq!(mem[0].series_key().measurement(), "mem");
904
905 let none = fc.scan_measurement("disk", 0, i64::MAX);
906 assert!(none.is_empty());
907 }
908
909 #[test]
910 fn scan_measurement_merges_active_and_frozen() {
911 let dir = tempfile::tempdir().unwrap();
912 let fc = FlushController::new(test_config(dir.path()));
913
914 fc.insert(&make_point_with_measurement("cpu", "a", 100, 1.0))
916 .unwrap();
917 fc.insert(&make_point_with_measurement("mem", "a", 150, 9.0))
918 .unwrap();
919 fc.freeze_and_swap().unwrap();
920
921 fc.insert(&make_point_with_measurement("cpu", "a", 200, 2.0))
923 .unwrap();
924 fc.insert(&make_point_with_measurement("mem", "b", 250, 8.0))
925 .unwrap();
926
927 let mut cpu: Vec<i64> = fc
930 .scan_measurement("cpu", 0, i64::MAX)
931 .iter()
932 .map(Point::timestamp)
933 .collect();
934 cpu.sort_unstable();
935 assert_eq!(cpu, vec![100, 200]);
936
937 let mem = fc.scan_measurement("mem", 0, i64::MAX);
938 assert_eq!(mem.len(), 2);
939 }
940
941 #[test]
942 fn min_unflushed_wal_seq_empty() {
943 let fc = FlushController::new(FlushConfig::default());
944 assert!(fc.min_unflushed_wal_seq().is_none());
945 }
946
947 #[test]
948 fn min_unflushed_wal_seq_after_insert() {
949 let fc = FlushController::new(FlushConfig::default());
950 let p = make_point("host", 100, 1.0);
951 fc.insert_with_wal_seq(&p, 42).unwrap();
952 fc.insert_with_wal_seq(&p, 50).unwrap();
953 assert_eq!(fc.min_unflushed_wal_seq(), Some(42));
954 }
955
956 #[test]
961 fn a_frozen_memtable_still_counts_as_unflushed() {
962 let fc = FlushController::new(FlushConfig::default());
963 let p = make_point("host", 100, 1.0);
964 fc.insert_with_wal_seq(&p, 10).unwrap();
965 fc.freeze_and_swap().unwrap();
966 assert_eq!(
967 fc.min_unflushed_wal_seq(),
968 Some(10),
969 "the frozen memtable holds seq 10 and nothing flushed it"
970 );
971 fc.insert_with_wal_seq(&make_point("host", 200, 2.0), 20)
972 .unwrap();
973 assert_eq!(fc.min_unflushed_wal_seq(), Some(10));
974 }
975
976 #[test]
980 fn a_failed_flush_keeps_the_frozen_memtable() {
981 let dir = tempfile::tempdir().unwrap();
982 let mut config = test_config(dir.path());
983 let blocked = dir.path().join("blocked");
985 std::fs::write(&blocked, b"not a directory").unwrap();
986 config.segment_dir = blocked.clone();
987 let fc = FlushController::new(config);
988
989 fc.insert_with_wal_seq(&make_point("a", 100, 1.0), 7)
990 .unwrap();
991 fc.freeze_and_swap().unwrap();
992 assert!(fc.flush_frozen().is_err());
993 assert!(
994 fc.frozen_memtable().is_some(),
995 "the memtable must survive a failed flush"
996 );
997 assert_eq!(fc.min_unflushed_wal_seq(), Some(7));
998
999 std::fs::remove_file(&blocked).unwrap();
1002 std::fs::create_dir_all(&blocked).unwrap();
1003 let err = fc.flush_frozen_with(|results| {
1004 assert_eq!(results.len(), 1);
1005 assert!(results[0].segment_meta.path.exists());
1006 Err(MemtableError::NoFrozenMemtable)
1007 });
1008 assert!(err.is_err());
1009 assert!(fc.frozen_memtable().is_some());
1010 assert_eq!(
1011 std::fs::read_dir(&blocked).unwrap().count(),
1012 0,
1013 "a segment nothing registered is removed, not left as an orphan"
1014 );
1015
1016 let results = fc.flush_frozen().unwrap();
1018 assert_eq!(results[0].points_flushed, 1);
1019 assert!(fc.frozen_memtable().is_none());
1020 assert!(fc.min_unflushed_wal_seq().is_none());
1021 }
1022
1023 #[test]
1024 fn write_stall_zero_below_soft_limit() {
1025 let dir = tempfile::tempdir().unwrap();
1026 let config = FlushConfig {
1027 max_memory: 1024 * 1024,
1028 stall_soft_fraction: 0.75,
1029 max_stall_delay: std::time::Duration::from_millis(100),
1030 segment_dir: dir.path().to_path_buf(),
1031 ..Default::default()
1032 };
1033 let fc = FlushController::new(config);
1034
1035 assert_eq!(fc.write_stall_delay(), std::time::Duration::ZERO);
1037 }
1038
1039 #[test]
1040 fn write_stall_increases_with_pressure() {
1041 let dir = tempfile::tempdir().unwrap();
1042 let config = FlushConfig {
1043 max_memory: 10_000, stall_soft_fraction: 0.5, max_stall_delay: std::time::Duration::from_millis(100),
1046 flush_threshold: 10_000, segment_dir: dir.path().to_path_buf(),
1048 ..Default::default()
1049 };
1050 let fc = FlushController::new(config);
1051
1052 for i in 0..100 {
1054 let _ = fc.insert(&make_point("a", i, i as f64));
1055 }
1056
1057 let delay = fc.write_stall_delay();
1058 let mem = fc.total_memory();
1060 if mem > 5000 {
1061 assert!(
1062 delay > std::time::Duration::ZERO,
1063 "should stall when over soft limit (mem={mem})"
1064 );
1065 assert!(
1066 delay <= std::time::Duration::from_millis(100),
1067 "should not exceed max stall delay"
1068 );
1069 }
1070 }
1071}