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 #[must_use]
440 pub fn total_memory(&self) -> usize {
441 let active_size = self.active.read().estimated_size();
442 let frozen_size: usize = self.frozen.read().iter().map(|m| m.estimated_size()).sum();
443 active_size + frozen_size
444 }
445
446 #[must_use]
448 pub fn total_interner_memory(&self) -> usize {
449 let active = self.active.read().interner_bytes();
450 let frozen: usize = self.frozen.read().iter().map(|m| m.interner_bytes()).sum();
451 active + frozen
452 }
453
454 #[must_use]
462 pub fn min_unflushed_wal_seq(&self) -> Option<u64> {
463 let frozen = self.frozen.read();
464 let active = self.active.read();
465 active
466 .min_wal_seq()
467 .into_iter()
468 .chain(frozen.iter().filter_map(|m| m.min_wal_seq()))
469 .min()
470 }
471
472 fn check_capacity(&self) -> Result<()> {
474 let total = self.total_memory();
475 if total >= self.config.max_memory {
476 return Err(MemtableError::CapacityExceeded {
477 current: total,
478 limit: self.config.max_memory,
479 });
480 }
481 Ok(())
482 }
483
484 fn snapshot_memtables(&self) -> (Arc<Memtable>, Vec<Arc<Memtable>>) {
488 let frozen_guard = self.frozen.read();
489 let active_guard = self.active.read();
490 (
491 Arc::clone(&active_guard),
492 frozen_guard.iter().cloned().collect(),
493 )
494 }
495
496 #[must_use]
507 pub fn write_stall_delay(&self) -> std::time::Duration {
508 let total = self.total_memory();
509 let soft_limit = (self.config.max_memory as f64 * self.config.stall_soft_fraction) as usize;
510
511 if total <= soft_limit {
512 return std::time::Duration::ZERO;
513 }
514
515 let hard = self.config.max_memory;
516 let range = hard.saturating_sub(soft_limit);
517 if range == 0 {
518 return self.config.max_stall_delay;
519 }
520
521 let pressure = total.saturating_sub(soft_limit) as f64 / range as f64;
522 let pressure = pressure.min(1.0);
523 self.config.max_stall_delay.mul_f64(pressure)
524 }
525
526 fn next_segment_path(&self) -> PathBuf {
528 let counter = self
529 .segment_counter
530 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
531 let now_ns = std::time::SystemTime::now()
532 .duration_since(std::time::UNIX_EPOCH)
533 .map(|d| d.as_nanos())
534 .unwrap_or(0);
535 self.config
536 .segment_dir
537 .join(format!("seg_{now_ns}_{counter:06}.csx"))
538 }
539}
540
541#[derive(Debug)]
543pub struct FlushResult {
544 pub measurement: String,
546 pub segment_meta: SegmentMeta,
548 pub max_wal_seq: Option<u64>,
551 pub points_flushed: usize,
553 pub series_keys: Vec<chronix_core::SeriesKey>,
555}
556
557fn merge_newest_first(sources: Vec<Vec<Point>>) -> Vec<Point> {
575 let total: usize = sources.iter().map(Vec::len).sum();
576 let mut all = Vec::with_capacity(total);
577 for src in sources {
578 all.extend(src);
579 }
580
581 let mut seen = std::collections::HashSet::with_capacity(total);
586 let mut keep = Vec::with_capacity(total);
587 for p in &all {
588 keep.push(seen.insert((p.series_key().canonical_form(), p.timestamp())));
589 }
590 drop(seen);
591
592 let mut keep = keep.into_iter();
593 all.retain(|_| keep.next().unwrap_or(false));
594 all
595}
596
597fn estimate_max_rows(
605 num_columns: usize,
606 target_segment_size_bytes: usize,
607 min_rows: usize,
608) -> usize {
609 let est_bytes_per_row = (num_columns * 4).max(8);
613 let max_rows = target_segment_size_bytes / est_bytes_per_row;
614 max_rows.max(min_rows)
616}
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621 use std::collections::BTreeMap;
622 use std::path::Path;
623
624 use chronix_core::types::{FieldValue, SeriesKey};
625
626 fn make_point(host: &str, ts: i64, value: f64) -> Point {
627 make_point_with_measurement("cpu", host, ts, value)
628 }
629
630 fn make_point_with_measurement(measurement: &str, host: &str, ts: i64, value: f64) -> Point {
631 let tags: BTreeMap<String, String> = [("host".to_string(), host.to_string())]
632 .into_iter()
633 .collect();
634 let series_key = SeriesKey::new(measurement.to_string(), tags).unwrap();
635 let fields: BTreeMap<String, FieldValue> = [("value".to_string(), FieldValue::F64(value))]
636 .into_iter()
637 .collect();
638 Point::new(series_key, fields, ts).unwrap()
639 }
640
641 fn test_config(dir: &Path) -> FlushConfig {
642 FlushConfig {
643 flush_threshold: 1024, max_memory: 1024 * 1024,
645 segment_dir: dir.to_path_buf(),
646 segment_writer_config: SegmentWriterConfig {
647 compress: false,
648 ..Default::default()
649 },
650 ..Default::default()
651 }
652 }
653
654 #[test]
655 fn insert_and_read_back() {
656 let dir = tempfile::tempdir().unwrap();
657 let fc = FlushController::new(test_config(dir.path()));
658
659 fc.insert(&make_point("a", 100, 1.0)).unwrap();
660 fc.insert(&make_point("a", 200, 2.0)).unwrap();
661
662 let all = fc.scan_all();
663 assert_eq!(all.len(), 2);
664 }
665
666 #[test]
667 fn freeze_and_swap() {
668 let dir = tempfile::tempdir().unwrap();
669 let fc = FlushController::new(test_config(dir.path()));
670
671 fc.insert(&make_point("a", 100, 1.0)).unwrap();
672 fc.insert(&make_point("a", 200, 2.0)).unwrap();
673
674 fc.freeze_and_swap().unwrap();
675
676 assert!(fc.active_memtable().is_empty());
678 assert!(fc.frozen_memtable().is_some());
679 assert_eq!(fc.frozen_memtable().unwrap().len(), 2);
680
681 fc.insert(&make_point("a", 300, 3.0)).unwrap();
683 assert_eq!(fc.active_memtable().len(), 1);
684 }
685
686 #[test]
687 fn scan_merges_active_and_frozen() {
688 let dir = tempfile::tempdir().unwrap();
689 let fc = FlushController::new(test_config(dir.path()));
690
691 fc.insert(&make_point("a", 100, 1.0)).unwrap();
692 fc.insert(&make_point("a", 200, 2.0)).unwrap();
693 fc.freeze_and_swap().unwrap();
694 fc.insert(&make_point("a", 300, 3.0)).unwrap();
695
696 let all = fc.scan_all();
697 assert_eq!(all.len(), 3);
698 }
699
700 #[test]
701 fn active_wins_on_duplicate() {
702 let dir = tempfile::tempdir().unwrap();
703 let fc = FlushController::new(test_config(dir.path()));
704
705 fc.insert(&make_point("a", 100, 1.0)).unwrap();
706 fc.freeze_and_swap().unwrap();
707 fc.insert(&make_point("a", 100, 99.0)).unwrap();
708
709 let all = fc.scan_all();
710 assert_eq!(all.len(), 1);
711 assert_eq!(all[0].field("value"), Some(&FieldValue::F64(99.0)));
712 }
713
714 #[test]
715 fn flush_frozen_writes_segment() {
716 let dir = tempfile::tempdir().unwrap();
717 let fc = FlushController::new(test_config(dir.path()));
718
719 for i in 0..10 {
720 fc.insert(&make_point("a", i * 100, i as f64)).unwrap();
721 }
722
723 fc.freeze_and_swap().unwrap();
724 let results = fc.flush_frozen().unwrap();
725
726 assert_eq!(results.len(), 1);
727 let result = &results[0];
728 assert_eq!(result.measurement, "cpu");
729 assert_eq!(result.points_flushed, 10);
730 assert!(result.segment_meta.path.exists());
731 assert_eq!(result.segment_meta.row_count, 10);
732
733 assert!(fc.frozen_memtable().is_none());
735 }
736
737 #[test]
738 fn should_flush_threshold() {
739 let dir = tempfile::tempdir().unwrap();
740 let mut config = test_config(dir.path());
741 config.flush_threshold = 100; let fc = FlushController::new(config);
743
744 assert!(!fc.should_flush());
745
746 for i in 0..20 {
748 let _ = fc.insert(&make_point("a", i, i as f64));
749 }
750 assert!(fc.should_flush());
751 }
752
753 #[test]
754 fn multi_freeze_respects_limit() {
755 let dir = tempfile::tempdir().unwrap();
756 let fc = FlushController::new(test_config(dir.path()));
757
758 fc.insert(&make_point("a", 100, 1.0)).unwrap();
760 fc.freeze_and_swap().unwrap();
761
762 fc.insert(&make_point("a", 200, 2.0)).unwrap();
763 fc.freeze_and_swap()
764 .expect("second freeze should succeed with multi-slot frozen queue");
765
766 fc.insert(&make_point("a", 300, 3.0)).unwrap();
768 let result = fc.freeze_and_swap();
769 assert!(result.is_err(), "should reject third freeze at capacity");
770 }
771
772 #[test]
773 fn capacity_rejection() {
774 let dir = tempfile::tempdir().unwrap();
775 let mut config = test_config(dir.path());
776 config.max_memory = 1; let fc = FlushController::new(config);
778
779 let _ = fc.insert(&make_point("a", 100, 1.0));
782 let result = fc.insert(&make_point("a", 200, 2.0));
783 assert!(result.is_err());
784 }
785
786 #[test]
787 fn wal_seq_in_flush_result() {
788 let dir = tempfile::tempdir().unwrap();
789 let fc = FlushController::new(test_config(dir.path()));
790
791 fc.insert_with_wal_seq(&make_point("a", 100, 1.0), 5)
792 .unwrap();
793 fc.insert_with_wal_seq(&make_point("a", 200, 2.0), 10)
794 .unwrap();
795
796 fc.freeze_and_swap().unwrap();
797 let results = fc.flush_frozen().unwrap();
798
799 assert_eq!(results.len(), 1);
800 assert_eq!(results[0].max_wal_seq, Some(10));
801 }
802
803 #[test]
804 fn merging_keeps_the_first_source_that_has_a_key() {
805 let merged = merge_newest_first(vec![
809 vec![make_point("a", 100, 10.0), make_point("a", 300, 30.0)],
810 vec![make_point("a", 100, 1.0), make_point("a", 200, 2.0)],
811 vec![make_point("a", 200, 99.0)],
812 ]);
813 assert_eq!(merged.len(), 3);
814
815 let ts100 = merged.iter().find(|p| p.timestamp() == 100).unwrap();
816 assert_eq!(ts100.field("value"), Some(&FieldValue::F64(10.0)));
817 let ts200 = merged.iter().find(|p| p.timestamp() == 200).unwrap();
818 assert_eq!(
819 ts200.field("value"),
820 Some(&FieldValue::F64(2.0)),
821 "the second source wins over the third"
822 );
823 }
824
825 #[test]
834 fn the_newest_frozen_memtable_wins_a_duplicate() {
835 let dir = tempfile::tempdir().unwrap();
836 let fc = FlushController::new(test_config(dir.path()));
837
838 fc.insert(&make_point("a", 100, 1.0)).unwrap();
841 fc.freeze_and_swap().unwrap();
842 fc.insert(&make_point("a", 100, 2.0)).unwrap();
843 fc.freeze_and_swap().unwrap();
844
845 let key = make_point("a", 100, 0.0).series_key().clone();
846 for (path, points) in [
847 ("scan", fc.scan(&key, 0, i64::MAX)),
848 ("scan_all", fc.scan_all()),
849 (
850 "scan_measurement",
851 fc.scan_measurement(key.measurement(), 0, i64::MAX),
852 ),
853 ] {
854 assert_eq!(points.len(), 1, "{path}: one point survives dedup");
855 assert_eq!(
856 points[0].field("value"),
857 Some(&FieldValue::F64(2.0)),
858 "{path}: the newer write wins"
859 );
860 }
861 }
862
863 #[test]
865 fn the_active_memtable_wins_a_duplicate() {
866 let dir = tempfile::tempdir().unwrap();
867 let fc = FlushController::new(test_config(dir.path()));
868 fc.insert(&make_point("a", 100, 1.0)).unwrap();
869 fc.freeze_and_swap().unwrap();
870 fc.insert(&make_point("a", 100, 9.0)).unwrap();
871
872 let points = fc.scan_all();
873 assert_eq!(points.len(), 1);
874 assert_eq!(points[0].field("value"), Some(&FieldValue::F64(9.0)));
875 }
876
877 #[test]
878 fn scan_measurement_filters_active() {
879 let dir = tempfile::tempdir().unwrap();
880 let fc = FlushController::new(test_config(dir.path()));
881
882 fc.insert(&make_point_with_measurement("cpu", "a", 100, 1.0))
883 .unwrap();
884 fc.insert(&make_point_with_measurement("mem", "a", 200, 2.0))
885 .unwrap();
886 fc.insert(&make_point_with_measurement("cpu", "b", 300, 3.0))
887 .unwrap();
888
889 let cpu = fc.scan_measurement("cpu", 0, i64::MAX);
890 assert_eq!(cpu.len(), 2);
891 assert!(cpu.iter().all(|p| p.series_key().measurement() == "cpu"));
892
893 let mem = fc.scan_measurement("mem", 0, i64::MAX);
894 assert_eq!(mem.len(), 1);
895 assert_eq!(mem[0].series_key().measurement(), "mem");
896
897 let none = fc.scan_measurement("disk", 0, i64::MAX);
898 assert!(none.is_empty());
899 }
900
901 #[test]
902 fn scan_measurement_merges_active_and_frozen() {
903 let dir = tempfile::tempdir().unwrap();
904 let fc = FlushController::new(test_config(dir.path()));
905
906 fc.insert(&make_point_with_measurement("cpu", "a", 100, 1.0))
908 .unwrap();
909 fc.insert(&make_point_with_measurement("mem", "a", 150, 9.0))
910 .unwrap();
911 fc.freeze_and_swap().unwrap();
912
913 fc.insert(&make_point_with_measurement("cpu", "a", 200, 2.0))
915 .unwrap();
916 fc.insert(&make_point_with_measurement("mem", "b", 250, 8.0))
917 .unwrap();
918
919 let mut cpu: Vec<i64> = fc
922 .scan_measurement("cpu", 0, i64::MAX)
923 .iter()
924 .map(Point::timestamp)
925 .collect();
926 cpu.sort_unstable();
927 assert_eq!(cpu, vec![100, 200]);
928
929 let mem = fc.scan_measurement("mem", 0, i64::MAX);
930 assert_eq!(mem.len(), 2);
931 }
932
933 #[test]
934 fn min_unflushed_wal_seq_empty() {
935 let fc = FlushController::new(FlushConfig::default());
936 assert!(fc.min_unflushed_wal_seq().is_none());
937 }
938
939 #[test]
940 fn min_unflushed_wal_seq_after_insert() {
941 let fc = FlushController::new(FlushConfig::default());
942 let p = make_point("host", 100, 1.0);
943 fc.insert_with_wal_seq(&p, 42).unwrap();
944 fc.insert_with_wal_seq(&p, 50).unwrap();
945 assert_eq!(fc.min_unflushed_wal_seq(), Some(42));
946 }
947
948 #[test]
953 fn a_frozen_memtable_still_counts_as_unflushed() {
954 let fc = FlushController::new(FlushConfig::default());
955 let p = make_point("host", 100, 1.0);
956 fc.insert_with_wal_seq(&p, 10).unwrap();
957 fc.freeze_and_swap().unwrap();
958 assert_eq!(
959 fc.min_unflushed_wal_seq(),
960 Some(10),
961 "the frozen memtable holds seq 10 and nothing flushed it"
962 );
963 fc.insert_with_wal_seq(&make_point("host", 200, 2.0), 20)
964 .unwrap();
965 assert_eq!(fc.min_unflushed_wal_seq(), Some(10));
966 }
967
968 #[test]
972 fn a_failed_flush_keeps_the_frozen_memtable() {
973 let dir = tempfile::tempdir().unwrap();
974 let mut config = test_config(dir.path());
975 let blocked = dir.path().join("blocked");
977 std::fs::write(&blocked, b"not a directory").unwrap();
978 config.segment_dir = blocked.clone();
979 let fc = FlushController::new(config);
980
981 fc.insert_with_wal_seq(&make_point("a", 100, 1.0), 7)
982 .unwrap();
983 fc.freeze_and_swap().unwrap();
984 assert!(fc.flush_frozen().is_err());
985 assert!(
986 fc.frozen_memtable().is_some(),
987 "the memtable must survive a failed flush"
988 );
989 assert_eq!(fc.min_unflushed_wal_seq(), Some(7));
990
991 std::fs::remove_file(&blocked).unwrap();
994 std::fs::create_dir_all(&blocked).unwrap();
995 let err = fc.flush_frozen_with(|results| {
996 assert_eq!(results.len(), 1);
997 assert!(results[0].segment_meta.path.exists());
998 Err(MemtableError::NoFrozenMemtable)
999 });
1000 assert!(err.is_err());
1001 assert!(fc.frozen_memtable().is_some());
1002 assert_eq!(
1003 std::fs::read_dir(&blocked).unwrap().count(),
1004 0,
1005 "a segment nothing registered is removed, not left as an orphan"
1006 );
1007
1008 let results = fc.flush_frozen().unwrap();
1010 assert_eq!(results[0].points_flushed, 1);
1011 assert!(fc.frozen_memtable().is_none());
1012 assert!(fc.min_unflushed_wal_seq().is_none());
1013 }
1014
1015 #[test]
1016 fn write_stall_zero_below_soft_limit() {
1017 let dir = tempfile::tempdir().unwrap();
1018 let config = FlushConfig {
1019 max_memory: 1024 * 1024,
1020 stall_soft_fraction: 0.75,
1021 max_stall_delay: std::time::Duration::from_millis(100),
1022 segment_dir: dir.path().to_path_buf(),
1023 ..Default::default()
1024 };
1025 let fc = FlushController::new(config);
1026
1027 assert_eq!(fc.write_stall_delay(), std::time::Duration::ZERO);
1029 }
1030
1031 #[test]
1032 fn write_stall_increases_with_pressure() {
1033 let dir = tempfile::tempdir().unwrap();
1034 let config = FlushConfig {
1035 max_memory: 10_000, stall_soft_fraction: 0.5, max_stall_delay: std::time::Duration::from_millis(100),
1038 flush_threshold: 10_000, segment_dir: dir.path().to_path_buf(),
1040 ..Default::default()
1041 };
1042 let fc = FlushController::new(config);
1043
1044 for i in 0..100 {
1046 let _ = fc.insert(&make_point("a", i, i as f64));
1047 }
1048
1049 let delay = fc.write_stall_delay();
1050 let mem = fc.total_memory();
1052 if mem > 5000 {
1053 assert!(
1054 delay > std::time::Duration::ZERO,
1055 "should stall when over soft limit (mem={mem})"
1056 );
1057 assert!(
1058 delay <= std::time::Duration::from_millis(100),
1059 "should not exceed max stall delay"
1060 );
1061 }
1062 }
1063}