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