1use std::collections::HashSet;
16use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
17use std::sync::Arc;
18
19use crossbeam_skiplist::SkipMap;
20use dashmap::mapref::entry::Entry;
21use dashmap::DashMap;
22
23use chronix_core::types::{Point, SeriesKey, Timestamp};
24
25use crate::memtable::error::{MemtableError, Result};
26use crate::memtable::interner::StringInterner;
27use crate::memtable::key::{MemtableEntry, MemtableKey, NODE_OVERHEAD};
28
29pub type TagSet = Arc<[(Arc<str>, Arc<str>)]>;
31
32pub struct Memtable {
43 data: SkipMap<MemtableKey, MemtableEntry>,
45 interner: StringInterner,
47 measurement_index: DashMap<Arc<str>, HashSet<u64>>,
56 series_tags: DashMap<Arc<str>, TagSet>,
59 estimated_size: AtomicUsize,
61 index_size: AtomicUsize,
67 min_wal_seq: AtomicU64,
69 max_wal_seq: AtomicU64,
71 frozen: AtomicBool,
73}
74
75impl std::fmt::Debug for Memtable {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 f.debug_struct("Memtable")
78 .field("len", &self.data.len())
79 .field(
80 "estimated_size",
81 &self.estimated_size.load(Ordering::Relaxed),
82 )
83 .field("interned_strings", &self.interner.len())
84 .field("min_wal_seq", &self.min_wal_seq())
85 .field("max_wal_seq", &self.max_wal_seq())
86 .field("frozen", &self.frozen.load(Ordering::Relaxed))
87 .finish()
88 }
89}
90
91impl Memtable {
92 #[must_use]
94 pub fn new() -> Self {
95 Self {
96 data: SkipMap::new(),
97 interner: StringInterner::new(),
98 measurement_index: DashMap::new(),
99 series_tags: DashMap::new(),
100 estimated_size: AtomicUsize::new(0),
101 index_size: AtomicUsize::new(0),
102 min_wal_seq: AtomicU64::new(u64::MAX),
103 max_wal_seq: AtomicU64::new(0),
104 frozen: AtomicBool::new(false),
105 }
106 }
107
108 pub fn insert(&self, point: &Point) -> Result<()> {
118 if self.frozen.load(Ordering::Acquire) {
119 return Err(MemtableError::Frozen);
120 }
121
122 let canonical: Arc<str> = self.interner.intern(point.series_key().canonical_form());
123 let key = MemtableKey::new(point.series_key().hash_fnv(), point.timestamp(), canonical);
124
125 let measurement = self.interner.intern(point.series_key().measurement());
126 let measurement_for_index = Arc::clone(&measurement);
127 let tags = self.series_tag_set(&key.series_canonical, point.series_key());
128
129 let entry = MemtableEntry {
130 measurement,
131 tags,
132 fields: point
133 .fields()
134 .iter()
135 .map(|(k, v)| (Arc::clone(k), v.clone()))
136 .collect(),
137 };
138
139 let entry_size =
140 entry.estimated_size() + std::mem::size_of::<MemtableKey>() + NODE_OVERHEAD;
141
142 if let Some(existing) = self.data.get(&key) {
150 let old_size = existing.value().estimated_size()
151 + std::mem::size_of::<MemtableKey>()
152 + NODE_OVERHEAD;
153 let _ =
154 self.estimated_size
155 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
156 Some(current.saturating_sub(old_size))
157 });
158 }
159
160 self.data.insert(key, entry);
161 let new_size = self.estimated_size.fetch_add(entry_size, Ordering::Relaxed) + entry_size;
162
163 metrics::gauge!("chronix_memtable_memory_bytes").set(new_size as f64);
166 metrics::gauge!("chronix_memtable_entries").set(self.data.len() as f64);
167
168 {
172 let hash = point.series_key().hash_fnv();
173 let delta = self.index_insert(measurement_for_index, hash);
174 if delta > 0 {
175 self.index_size.fetch_add(delta, Ordering::Relaxed);
176 }
177 }
178
179 Ok(())
180 }
181
182 pub fn insert_with_wal_seq(&self, point: &Point, wal_seq: u64) -> Result<()> {
191 self.insert(point)?;
192 self.update_wal_seq(wal_seq);
193 Ok(())
194 }
195
196 pub fn insert_batch(&self, points: &[Point]) -> Result<usize> {
211 if self.frozen.load(Ordering::Acquire) {
212 return Err(MemtableError::Frozen);
213 }
214
215 let mut total_size_delta: i64 = 0;
216 let mut new_index_entries: Vec<(Arc<str>, u64)> = Vec::new();
218
219 let mut prev_hash: u64 = 0;
223 let mut prev_ts: i64 = i64::MIN;
224
225 for point in points {
226 let canonical: Arc<str> = self.interner.intern(point.series_key().canonical_form());
227 let key = MemtableKey::new(point.series_key().hash_fnv(), point.timestamp(), canonical);
228
229 let measurement = self.interner.intern(point.series_key().measurement());
230 let tags = self.series_tag_set(&key.series_canonical, point.series_key());
231
232 let entry = MemtableEntry {
233 measurement: Arc::clone(&measurement),
234 tags,
235 fields: point
236 .fields()
237 .iter()
238 .map(|(k, v)| (Arc::clone(k), v.clone()))
239 .collect(),
240 };
241
242 let entry_size =
243 entry.estimated_size() + std::mem::size_of::<MemtableKey>() + NODE_OVERHEAD;
244
245 let cur_hash = point.series_key().hash_fnv();
250 let cur_ts = point.timestamp();
251 let may_overwrite = cur_hash == prev_hash && cur_ts == prev_ts;
252
253 if may_overwrite {
254 if let Some(existing) = self.data.get(&key) {
255 let old_size = existing.value().estimated_size()
256 + std::mem::size_of::<MemtableKey>()
257 + NODE_OVERHEAD;
258 total_size_delta -= old_size.min(i64::MAX as usize) as i64;
259 }
260 }
261
262 prev_hash = cur_hash;
263 prev_ts = cur_ts;
264
265 self.data.insert(key, entry);
266 total_size_delta += entry_size.min(i64::MAX as usize) as i64;
268
269 new_index_entries.push((measurement, cur_hash));
270 }
271
272 if total_size_delta >= 0 {
274 self.estimated_size
275 .fetch_add(total_size_delta as usize, Ordering::Relaxed);
276 } else {
277 let _ =
278 self.estimated_size
279 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
280 Some(current.saturating_sub(total_size_delta.unsigned_abs() as usize))
281 });
282 }
283
284 let mut index_delta = 0usize;
286 for (measurement, hash) in new_index_entries {
287 index_delta += self.index_insert(measurement, hash);
288 }
289 if index_delta > 0 {
290 self.index_size.fetch_add(index_delta, Ordering::Relaxed);
291 }
292
293 metrics::gauge!("chronix_memtable_memory_bytes")
295 .set(self.estimated_size.load(Ordering::Relaxed) as f64);
296 metrics::gauge!("chronix_memtable_entries").set(self.data.len() as f64);
297
298 Ok(points.len())
299 }
300
301 pub fn insert_batch_with_wal_seq(&self, points: &[Point], wal_seq: u64) -> Result<usize> {
310 let count = self.insert_batch(points)?;
311 self.update_wal_seq(wal_seq);
312 Ok(count)
313 }
314
315 fn update_wal_seq(&self, seq: u64) {
317 let mut current = self.min_wal_seq.load(Ordering::Relaxed);
319 while seq < current {
320 match self.min_wal_seq.compare_exchange_weak(
321 current,
322 seq,
323 Ordering::Release,
324 Ordering::Relaxed,
325 ) {
326 Ok(_) => break,
327 Err(actual) => current = actual,
328 }
329 }
330
331 let mut current = self.max_wal_seq.load(Ordering::Relaxed);
333 while seq > current {
334 match self.max_wal_seq.compare_exchange_weak(
335 current,
336 seq,
337 Ordering::Release,
338 Ordering::Relaxed,
339 ) {
340 Ok(_) => break,
341 Err(actual) => current = actual,
342 }
343 }
344 }
345
346 #[must_use]
353 pub fn scan(&self, series_key: &SeriesKey, min_ts: Timestamp, max_ts: Timestamp) -> Vec<Point> {
354 let hash = series_key.hash_fnv();
355 let canonical = series_key.canonical_form();
356 let start = MemtableKey::new(hash, min_ts, Arc::from(canonical));
359 let end = MemtableKey::new(hash, max_ts, Arc::from(canonical));
360
361 self.data
362 .range(start..=end)
363 .filter(|entry| &*entry.key().series_canonical == canonical)
364 .filter_map(|entry| Self::entry_to_point(entry.key(), entry.value()))
365 .collect()
366 }
367
368 #[must_use]
377 pub fn scan_range(&self, min_ts: Timestamp, max_ts: Timestamp) -> Vec<Point> {
378 self.data
381 .iter()
382 .filter(|entry| {
383 let ts = entry.key().timestamp;
384 ts >= min_ts && ts <= max_ts
385 })
386 .filter_map(|entry| Self::entry_to_point(entry.key(), entry.value()))
387 .collect()
388 }
389
390 #[must_use]
393 pub fn scan_all(&self) -> Vec<Point> {
394 self.data
395 .iter()
396 .filter_map(|entry| Self::entry_to_point(entry.key(), entry.value()))
397 .collect()
398 }
399
400 #[must_use]
407 pub fn scan_measurement(
408 &self,
409 measurement: &str,
410 min_ts: Timestamp,
411 max_ts: Timestamp,
412 ) -> Vec<Point> {
413 let hashes = match self.measurement_index.get(measurement) {
414 Some(set) => set.iter().copied().collect::<Vec<_>>(),
415 None => return Vec::new(),
416 };
417
418 let mut results = Vec::new();
419 for hash in hashes {
420 let start = MemtableKey::lower_bound(hash, min_ts);
422 let end = MemtableKey::upper_bound(hash, max_ts);
423 results.extend(
424 self.data
425 .range(start..=end)
426 .filter(|entry| {
427 let ts = entry.key().timestamp;
428 ts >= min_ts && ts <= max_ts && &*entry.value().measurement == measurement
429 })
430 .filter_map(|entry| Self::entry_to_point(entry.key(), entry.value())),
431 );
432 }
433 results
434 }
435
436 #[must_use]
438 pub fn len(&self) -> usize {
439 self.data.len()
440 }
441
442 #[must_use]
444 pub fn is_empty(&self) -> bool {
445 self.data.is_empty()
446 }
447
448 #[must_use]
458 pub fn interner_bytes(&self) -> usize {
459 self.interner.memory_bytes()
460 }
461
462 #[must_use]
464 pub fn estimated_size(&self) -> usize {
465 self.estimated_size.load(Ordering::Relaxed) + self.index_size.load(Ordering::Relaxed)
466 }
467
468 fn series_tag_set(
474 &self,
475 canonical: &Arc<str>,
476 series_key: &SeriesKey,
477 ) -> Arc<[(Arc<str>, Arc<str>)]> {
478 if let Some(existing) = self.series_tags.get(canonical) {
479 return Arc::clone(existing.value());
480 }
481 let tags: Arc<[(Arc<str>, Arc<str>)]> = series_key
482 .tags()
483 .iter()
484 .map(|(k, v)| (self.interner.intern(k), self.interner.intern(v)))
485 .collect();
486 let mut charged = 0usize;
487 let stored = self
488 .series_tags
489 .entry(Arc::clone(canonical))
490 .or_insert_with(|| {
491 charged = canonical.len()
495 + tags.len() * std::mem::size_of::<(Arc<str>, Arc<str>)>()
496 + tags.iter().map(|(k, v)| k.len() + v.len()).sum::<usize>()
497 + 2 * std::mem::size_of::<usize>()
498 + 64;
499 tags
500 })
501 .clone();
502 if charged > 0 {
503 self.estimated_size.fetch_add(charged, Ordering::Relaxed);
504 }
505 stored
506 }
507
508 fn index_insert(&self, measurement: Arc<str>, hash: u64) -> usize {
517 const SET_OVERHEAD: usize = 56;
518 match self.measurement_index.entry(measurement) {
519 Entry::Occupied(mut occupied) => {
520 let before = occupied.get().capacity();
521 occupied.get_mut().insert(hash);
522 (occupied.get().capacity() - before) * std::mem::size_of::<u64>()
523 }
524 Entry::Vacant(vacant) => {
525 let key_bytes = std::mem::size_of::<usize>() * 2 + vacant.key().len();
526 let mut set = HashSet::new();
527 set.insert(hash);
528 let set_bytes = set.capacity() * std::mem::size_of::<u64>() + SET_OVERHEAD;
529 vacant.insert(set);
530 key_bytes + set_bytes
531 }
532 }
533 }
534
535 #[must_use]
539 pub fn min_wal_seq(&self) -> Option<u64> {
540 let val = self.min_wal_seq.load(Ordering::Acquire);
541 if val == u64::MAX {
542 None
543 } else {
544 Some(val)
545 }
546 }
547
548 #[must_use]
552 pub fn max_wal_seq(&self) -> Option<u64> {
553 let val = self.max_wal_seq.load(Ordering::Acquire);
554 if val == 0 && self.min_wal_seq.load(Ordering::Acquire) == u64::MAX {
555 None
556 } else {
557 Some(val)
558 }
559 }
560
561 #[must_use]
563 pub fn is_frozen(&self) -> bool {
564 self.frozen.load(Ordering::Acquire)
565 }
566
567 pub fn freeze(&self) {
572 self.frozen.store(true, Ordering::Release);
573 }
574
575 #[must_use]
577 pub fn to_points(&self) -> Vec<Point> {
578 self.scan_all()
579 }
580
581 #[must_use]
583 #[deprecated(
584 since = "0.1.0",
585 note = "renamed to `to_points`; this method does not drain"
586 )]
587 pub fn drain_to_points(&self) -> Vec<Point> {
588 self.to_points()
589 }
590
591 fn entry_to_point(key: &MemtableKey, entry: &MemtableEntry) -> Option<Point> {
596 let tags = entry
597 .tags
598 .iter()
599 .map(|(k, v)| (k.to_string(), v.to_string()))
600 .collect();
601 let series_key = SeriesKey::new(entry.measurement.to_string(), tags)
602 .map_err(|e| {
603 tracing::error!(
606 hash = key.series_key_hash,
607 error = %e,
608 "DATA LOSS: failed to reconstruct series key during flush"
609 );
610 metrics::counter!("chronix_memtable_dropped_points_total").increment(1);
611 e
612 })
613 .ok()?;
614 Point::new(
615 series_key,
616 entry
617 .fields
618 .iter()
619 .map(|(k, v)| (k.to_string(), v.clone()))
620 .collect(),
621 key.timestamp,
622 )
623 .map_err(|e| {
624 tracing::error!(
625 hash = key.series_key_hash,
626 timestamp = key.timestamp,
627 error = %e,
628 "DATA LOSS: failed to reconstruct point during flush"
629 );
630 metrics::counter!("chronix_memtable_dropped_points_total").increment(1);
631 e
632 })
633 .ok()
634 }
635}
636
637impl Default for Memtable {
638 fn default() -> Self {
639 Self::new()
640 }
641}
642
643#[cfg(test)]
648mod tests {
649 use super::*;
650 use std::collections::BTreeMap;
651
652 use chronix_core::FieldValue;
653
654 fn make_point(measurement: &str, host: &str, ts: i64, value: f64) -> Point {
655 let tags: BTreeMap<String, String> = [("host".to_string(), host.to_string())]
656 .into_iter()
657 .collect();
658 let series_key = SeriesKey::new(measurement.to_string(), tags).unwrap();
659 let fields: BTreeMap<String, FieldValue> = [("value".to_string(), FieldValue::F64(value))]
660 .into_iter()
661 .collect();
662 Point::new(series_key, fields, ts).unwrap()
663 }
664
665 #[test]
666 fn insert_and_scan_all() {
667 let mt = Memtable::new();
668 mt.insert(&make_point("cpu", "a", 100, 1.0)).unwrap();
669 mt.insert(&make_point("cpu", "a", 200, 2.0)).unwrap();
670 mt.insert(&make_point("cpu", "a", 300, 3.0)).unwrap();
671
672 let points = mt.scan_all();
673 assert_eq!(points.len(), 3);
674 assert_eq!(points[0].timestamp(), 100);
675 assert_eq!(points[1].timestamp(), 200);
676 assert_eq!(points[2].timestamp(), 300);
677 }
678
679 #[test]
680 fn insert_and_scan_series() {
681 let mt = Memtable::new();
682 mt.insert(&make_point("cpu", "a", 100, 1.0)).unwrap();
683 mt.insert(&make_point("cpu", "a", 200, 2.0)).unwrap();
684 mt.insert(&make_point("cpu", "b", 150, 9.0)).unwrap();
685
686 let key_a = SeriesKey::new(
687 "cpu".to_string(),
688 [("host".to_string(), "a".to_string())]
689 .into_iter()
690 .collect(),
691 )
692 .unwrap();
693
694 let points = mt.scan(&key_a, 0, 300);
695 assert_eq!(points.len(), 2);
696 assert!(points
697 .iter()
698 .all(|p| p.series_key().tag("host") == Some("a")));
699 }
700
701 #[test]
702 fn scan_time_range() {
703 let mt = Memtable::new();
704 mt.insert(&make_point("cpu", "a", 100, 1.0)).unwrap();
705 mt.insert(&make_point("cpu", "a", 200, 2.0)).unwrap();
706 mt.insert(&make_point("cpu", "a", 300, 3.0)).unwrap();
707
708 let key = SeriesKey::new(
709 "cpu".to_string(),
710 [("host".to_string(), "a".to_string())]
711 .into_iter()
712 .collect(),
713 )
714 .unwrap();
715
716 let points = mt.scan(&key, 150, 250);
717 assert_eq!(points.len(), 1);
718 assert_eq!(points[0].timestamp(), 200);
719 }
720
721 #[test]
722 fn last_write_wins() {
723 let mt = Memtable::new();
724 mt.insert(&make_point("cpu", "a", 100, 1.0)).unwrap();
725 mt.insert(&make_point("cpu", "a", 100, 99.0)).unwrap();
726
727 let points = mt.scan_all();
728 assert_eq!(points.len(), 1);
729 assert_eq!(points[0].field("value"), Some(&FieldValue::F64(99.0)));
730 }
731
732 #[test]
733 fn empty_scan() {
734 let mt = Memtable::new();
735 let key = SeriesKey::new("cpu".to_string(), BTreeMap::new()).unwrap();
736 let points = mt.scan(&key, 0, 1000);
737 assert!(points.is_empty());
738 }
739
740 #[test]
743 fn index_accounting_matches_a_full_walk() {
744 let mt = Memtable::new();
745 for i in 0..500u64 {
746 let measurement = format!("m{}", i % 7);
747 let series = format!("s{}", i % 53);
748 mt.insert(&make_point(&measurement, &series, i as i64, 1.0))
749 .unwrap();
750 }
751
752 let batch: Vec<_> = (500..800u64)
754 .map(|i| {
755 make_point(
756 &format!("m{}", i % 11),
757 &format!("s{}", i % 97),
758 i as i64,
759 2.0,
760 )
761 })
762 .collect();
763 mt.insert_batch(&batch).unwrap();
764
765 let walked: usize = mt
766 .measurement_index
767 .iter()
768 .map(|entry| {
769 let key_bytes = std::mem::size_of::<usize>() * 2 + entry.key().len();
770 let set_bytes = entry.value().capacity() * std::mem::size_of::<u64>() + 56;
771 key_bytes + set_bytes
772 })
773 .sum();
774
775 assert_eq!(
776 mt.index_size.load(Ordering::Relaxed),
777 walked,
778 "incremental index accounting drifted from a full walk"
779 );
780 }
781
782 #[test]
783 fn estimated_size_increases() {
784 let mt = Memtable::new();
785 assert_eq!(mt.estimated_size(), 0);
786 mt.insert(&make_point("cpu", "a", 100, 1.0)).unwrap();
787 assert!(mt.estimated_size() > 0);
788 let size_after_one = mt.estimated_size();
789 mt.insert(&make_point("cpu", "a", 200, 2.0)).unwrap();
790 assert!(mt.estimated_size() > size_after_one);
791 }
792
793 #[test]
794 fn wal_seq_tracking() {
795 let mt = Memtable::new();
796 assert_eq!(mt.min_wal_seq(), None);
797 assert_eq!(mt.max_wal_seq(), None);
798
799 mt.insert_with_wal_seq(&make_point("cpu", "a", 100, 1.0), 10)
800 .unwrap();
801 assert_eq!(mt.min_wal_seq(), Some(10));
802 assert_eq!(mt.max_wal_seq(), Some(10));
803
804 mt.insert_with_wal_seq(&make_point("cpu", "a", 200, 2.0), 5)
805 .unwrap();
806 assert_eq!(mt.min_wal_seq(), Some(5));
807 assert_eq!(mt.max_wal_seq(), Some(10));
808
809 mt.insert_with_wal_seq(&make_point("cpu", "a", 300, 3.0), 20)
810 .unwrap();
811 assert_eq!(mt.min_wal_seq(), Some(5));
812 assert_eq!(mt.max_wal_seq(), Some(20));
813 }
814
815 #[test]
816 fn freeze_prevents_inserts() {
817 let mt = Memtable::new();
818 mt.insert(&make_point("cpu", "a", 100, 1.0)).unwrap();
819 mt.freeze();
820 assert!(mt.is_frozen());
821
822 let result = mt.insert(&make_point("cpu", "a", 200, 2.0));
823 assert!(result.is_err());
824
825 let points = mt.scan_all();
827 assert_eq!(points.len(), 1);
828 }
829
830 #[test]
831 fn len_and_is_empty() {
832 let mt = Memtable::new();
833 assert!(mt.is_empty());
834 assert_eq!(mt.len(), 0);
835
836 mt.insert(&make_point("cpu", "a", 100, 1.0)).unwrap();
837 assert!(!mt.is_empty());
838 assert_eq!(mt.len(), 1);
839 }
840
841 #[test]
842 fn multiple_series() {
843 let mt = Memtable::new();
844 mt.insert(&make_point("cpu", "a", 100, 1.0)).unwrap();
845 mt.insert(&make_point("cpu", "b", 100, 2.0)).unwrap();
846 mt.insert(&make_point("mem", "a", 100, 3.0)).unwrap();
847
848 assert_eq!(mt.len(), 3);
849
850 let all = mt.scan_all();
851 assert_eq!(all.len(), 3);
852 }
853
854 #[test]
855 fn concurrent_inserts() {
856 use std::sync::Arc;
857 use std::thread;
858
859 let mt = Arc::new(Memtable::new());
860 let mut handles = Vec::new();
861
862 for thread_id in 0..4 {
863 let mt = Arc::clone(&mt);
864 handles.push(thread::spawn(move || {
865 for i in 0..100 {
866 let ts = i64::from(thread_id * 1000 + i);
867 let host = format!("host-{thread_id}");
868 mt.insert(&make_point("cpu", &host, ts, ts as f64)).unwrap();
869 }
870 }));
871 }
872
873 for h in handles {
874 h.join().unwrap();
875 }
876
877 assert_eq!(mt.len(), 400);
878 let all = mt.scan_all();
879 assert_eq!(all.len(), 400);
880
881 let mut prev_key: Option<MemtableKey> = None;
883 for p in &all {
884 let key = MemtableKey::new(
885 p.series_key().hash_fnv(),
886 p.timestamp(),
887 Arc::from(p.series_key().canonical_form()),
888 );
889 if let Some(prev) = prev_key {
890 assert!(key >= prev, "entries should be ordered");
891 }
892 prev_key = Some(key);
893 }
894 }
895
896 #[allow(deprecated)]
897 #[test]
898 fn to_points_is_scan_all() {
899 let mt = Memtable::new();
900 mt.insert(&make_point("cpu", "a", 100, 1.0)).unwrap();
901 mt.insert(&make_point("cpu", "a", 200, 2.0)).unwrap();
902
903 let scan = mt.scan_all();
904 let points = mt.to_points();
905 assert_eq!(scan.len(), points.len());
906 for (s, d) in scan.iter().zip(points.iter()) {
907 assert_eq!(s.timestamp(), d.timestamp());
908 assert_eq!(s.fields(), d.fields());
909 }
910
911 let drain = mt.drain_to_points();
913 assert_eq!(drain.len(), points.len());
914 }
915
916 #[test]
917 fn scan_range_across_series() {
918 let mt = Memtable::new();
919 mt.insert(&make_point("cpu", "a", 50, 1.0)).unwrap();
920 mt.insert(&make_point("cpu", "a", 100, 2.0)).unwrap();
921 mt.insert(&make_point("cpu", "b", 75, 3.0)).unwrap();
922 mt.insert(&make_point("cpu", "b", 200, 4.0)).unwrap();
923
924 let points = mt.scan_range(60, 150);
925 assert_eq!(points.len(), 2);
927 for p in &points {
928 assert!(
929 p.timestamp() >= 60 && p.timestamp() <= 150,
930 "timestamp {} should be in [60, 150]",
931 p.timestamp()
932 );
933 }
934 }
935
936 #[test]
937 fn scan_measurement_filters_by_name() {
938 let mt = Memtable::new();
939 mt.insert(&make_point("cpu", "a", 100, 1.0)).unwrap();
940 mt.insert(&make_point("cpu", "b", 200, 2.0)).unwrap();
941 mt.insert(&make_point("mem", "a", 150, 3.0)).unwrap();
942 mt.insert(&make_point("mem", "a", 300, 4.0)).unwrap();
943 mt.insert(&make_point("disk", "c", 250, 5.0)).unwrap();
944
945 let cpu = mt.scan_measurement("cpu", 0, i64::MAX);
947 assert_eq!(cpu.len(), 2);
948 assert!(cpu.iter().all(|p| p.series_key().measurement() == "cpu"));
949
950 let mem = mt.scan_measurement("mem", 0, i64::MAX);
952 assert_eq!(mem.len(), 2);
953 assert!(mem.iter().all(|p| p.series_key().measurement() == "mem"));
954
955 let disk = mt.scan_measurement("disk", 0, i64::MAX);
957 assert_eq!(disk.len(), 1);
958
959 let none = mt.scan_measurement("net", 0, i64::MAX);
961 assert!(none.is_empty());
962 }
963
964 #[test]
965 fn scan_measurement_respects_time_range() {
966 let mt = Memtable::new();
967 mt.insert(&make_point("cpu", "a", 100, 1.0)).unwrap();
968 mt.insert(&make_point("cpu", "a", 200, 2.0)).unwrap();
969 mt.insert(&make_point("cpu", "a", 300, 3.0)).unwrap();
970 mt.insert(&make_point("mem", "a", 200, 4.0)).unwrap();
971
972 let result = mt.scan_measurement("cpu", 150, 250);
973 assert_eq!(result.len(), 1);
974 assert_eq!(result[0].timestamp(), 200);
975 }
976
977 #[test]
981 fn hash_collision_preserves_both_series() {
982 let mt = Memtable::new();
983
984 let p1 = make_point("cpu", "host-a", 1000, 42.0);
988 let p2 = make_point("cpu", "host-b", 1000, 99.0);
989
990 mt.insert(&p1).unwrap();
991 mt.insert(&p2).unwrap();
992
993 assert_eq!(
995 mt.len(),
996 2,
997 "two distinct series at same timestamp must both survive"
998 );
999
1000 let s1 = mt.scan(p1.series_key(), 0, i64::MAX);
1002 assert_eq!(s1.len(), 1);
1003 assert_eq!(
1004 *s1[0].field("value").unwrap(),
1005 chronix_core::FieldValue::F64(42.0)
1006 );
1007
1008 let s2 = mt.scan(p2.series_key(), 0, i64::MAX);
1009 assert_eq!(s2.len(), 1);
1010 assert_eq!(
1011 *s2[0].field("value").unwrap(),
1012 chronix_core::FieldValue::F64(99.0)
1013 );
1014 }
1015
1016 #[test]
1017 fn insert_batch_basic() {
1018 let mt = Memtable::new();
1019 let points = vec![
1020 make_point("cpu", "a", 100, 1.0),
1021 make_point("cpu", "a", 200, 2.0),
1022 make_point("cpu", "b", 300, 3.0),
1023 make_point("mem", "a", 400, 4.0),
1024 ];
1025 let count = mt.insert_batch(&points).unwrap();
1026 assert_eq!(count, 4);
1027
1028 let all = mt.scan_all();
1029 assert_eq!(all.len(), 4);
1030
1031 let cpu = mt.scan_measurement("cpu", 0, i64::MAX);
1033 assert_eq!(cpu.len(), 3);
1034 let mem = mt.scan_measurement("mem", 0, i64::MAX);
1035 assert_eq!(mem.len(), 1);
1036 }
1037
1038 #[test]
1039 fn insert_batch_overwrite() {
1040 let mt = Memtable::new();
1041 let initial = vec![
1042 make_point("cpu", "a", 100, 1.0),
1043 make_point("cpu", "a", 200, 2.0),
1044 ];
1045 mt.insert_batch(&initial).unwrap();
1046
1047 let overwrite = vec![
1049 make_point("cpu", "a", 100, 10.0),
1050 make_point("cpu", "a", 200, 20.0),
1051 ];
1052 mt.insert_batch(&overwrite).unwrap();
1053
1054 let all = mt.scan_all();
1055 assert_eq!(all.len(), 2);
1056 assert_eq!(
1057 *all[0].field("value").unwrap(),
1058 chronix_core::FieldValue::F64(10.0)
1059 );
1060 assert_eq!(
1061 *all[1].field("value").unwrap(),
1062 chronix_core::FieldValue::F64(20.0)
1063 );
1064 }
1065
1066 #[test]
1067 fn insert_batch_frozen_fails() {
1068 let mt = Memtable::new();
1069 mt.freeze();
1070 let points = vec![make_point("cpu", "a", 100, 1.0)];
1071 assert!(mt.insert_batch(&points).is_err());
1072 }
1073
1074 #[test]
1075 fn insert_batch_with_wal_seq_updates_range() {
1076 let mt = Memtable::new();
1077 let points = vec![
1078 make_point("cpu", "a", 100, 1.0),
1079 make_point("cpu", "a", 200, 2.0),
1080 ];
1081 mt.insert_batch_with_wal_seq(&points, 42).unwrap();
1082 assert_eq!(mt.min_wal_seq(), Some(42));
1083 assert_eq!(mt.max_wal_seq(), Some(42));
1084 }
1085}
1086
1087#[cfg(test)]
1093mod calibration {
1094 use std::alloc::{GlobalAlloc, Layout, System};
1095 use std::collections::BTreeMap;
1096 use std::sync::atomic::{AtomicUsize, Ordering};
1097
1098 use chronix_core::{FieldValue, Point, SeriesKey};
1099
1100 use super::Memtable;
1101
1102 struct Counting;
1103 static LIVE: AtomicUsize = AtomicUsize::new(0);
1104
1105 std::thread_local! {
1108 static THREAD_LIVE: std::cell::Cell<isize> = const { std::cell::Cell::new(0) };
1109 }
1110
1111 fn thread_live() -> isize {
1112 THREAD_LIVE.with(std::cell::Cell::get)
1113 }
1114
1115 #[allow(unsafe_code)]
1119 unsafe impl GlobalAlloc for Counting {
1120 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
1121 LIVE.fetch_add(layout.size(), Ordering::Relaxed);
1122 let _ = THREAD_LIVE.try_with(|c| c.set(c.get() + layout.size() as isize));
1123 unsafe { System.alloc(layout) }
1124 }
1125 unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
1126 LIVE.fetch_sub(layout.size(), Ordering::Relaxed);
1127 let _ = THREAD_LIVE.try_with(|c| c.set(c.get() - layout.size() as isize));
1128 unsafe { System.dealloc(ptr, layout) }
1129 }
1130 }
1131
1132 #[global_allocator]
1133 static ALLOC: Counting = Counting;
1134
1135 #[test]
1139 fn memtable_estimate_tracks_real_allocation() {
1140 let keys: Vec<SeriesKey> = (0..50)
1141 .map(|m| {
1142 SeriesKey::new(
1143 "power",
1144 BTreeMap::from([
1145 ("meter".to_string(), format!("m{m:02}")),
1146 ("site".to_string(), "home".to_string()),
1147 ]),
1148 )
1149 .unwrap()
1150 })
1151 .collect();
1152 let points: Vec<Point> = (0..400i64)
1154 .flat_map(|s| {
1155 keys.iter().map(move |k| {
1156 Point::new(
1157 k.clone(),
1158 BTreeMap::from([
1159 ("w".to_string(), FieldValue::F64(230.0 + s as f64)),
1160 ("kwh".to_string(), FieldValue::F64(s as f64 / 1000.0)),
1161 ]),
1162 s * 1_000_000_000,
1163 )
1164 .unwrap()
1165 })
1166 })
1167 .collect();
1168
1169 let before = thread_live();
1170 let mt = Memtable::new();
1171 for p in &points {
1172 mt.insert(p).unwrap();
1173 }
1174 let measured = usize::try_from(thread_live() - before).unwrap_or(0);
1175 let estimated = mt.estimated_size();
1176 let ratio = estimated as f64 / measured as f64;
1177 assert!(
1178 (0.75..=1.25).contains(&ratio),
1179 "estimate {estimated} B vs allocator {measured} B for {} points: ratio {ratio:.2} \
1180 ({:.0} B/point measured, {:.0} B/point estimated)",
1181 points.len(),
1182 measured as f64 / points.len() as f64,
1183 estimated as f64 / points.len() as f64,
1184 );
1185 drop(mt);
1186 }
1187}
1188
1189#[derive(Debug)]
1191pub struct MeasurementBatch {
1192 pub measurement: String,
1194 pub batch: arrow::record_batch::RecordBatch,
1198 pub tag_columns: Vec<String>,
1200}
1201
1202enum FieldBuilder {
1204 F64(arrow::array::Float64Builder),
1205 I64(arrow::array::Int64Builder),
1206 U64(arrow::array::UInt64Builder),
1207 Bool(arrow::array::BooleanBuilder),
1208 Str(arrow::array::StringBuilder),
1209}
1210
1211impl FieldBuilder {
1212 fn for_value(v: &chronix_core::FieldValue, rows_before: usize) -> Self {
1213 use chronix_core::FieldValue as V;
1214 let mut b = match v {
1215 V::F64(_) => Self::F64(arrow::array::Float64Builder::new()),
1216 V::I64(_) => Self::I64(arrow::array::Int64Builder::new()),
1217 V::U64(_) => Self::U64(arrow::array::UInt64Builder::new()),
1218 V::Bool(_) => Self::Bool(arrow::array::BooleanBuilder::new()),
1219 V::String(_) => Self::Str(arrow::array::StringBuilder::new()),
1220 };
1221 for _ in 0..rows_before {
1222 b.append_null();
1223 }
1224 b
1225 }
1226
1227 fn append_null(&mut self) {
1228 match self {
1229 Self::F64(b) => b.append_null(),
1230 Self::I64(b) => b.append_null(),
1231 Self::U64(b) => b.append_null(),
1232 Self::Bool(b) => b.append_null(),
1233 Self::Str(b) => b.append_null(),
1234 }
1235 }
1236
1237 fn append(&mut self, v: &chronix_core::FieldValue) {
1241 use chronix_core::FieldValue as V;
1242 match (self, v) {
1243 (Self::F64(b), V::F64(x)) => b.append_value(*x),
1244 (Self::I64(b), V::I64(x)) => b.append_value(*x),
1245 (Self::U64(b), V::U64(x)) => b.append_value(*x),
1246 (Self::Bool(b), V::Bool(x)) => b.append_value(*x),
1247 (Self::Str(b), V::String(x)) => b.append_value(x),
1248 (this, _) => this.append_null(),
1249 }
1250 }
1251
1252 fn finish(self) -> (arrow::datatypes::DataType, arrow::array::ArrayRef) {
1253 use arrow::datatypes::DataType;
1254 match self {
1255 Self::F64(mut b) => (DataType::Float64, Arc::new(b.finish())),
1256 Self::I64(mut b) => (DataType::Int64, Arc::new(b.finish())),
1257 Self::U64(mut b) => (DataType::UInt64, Arc::new(b.finish())),
1258 Self::Bool(mut b) => (DataType::Boolean, Arc::new(b.finish())),
1259 Self::Str(mut b) => (DataType::Utf8, Arc::new(b.finish())),
1260 }
1261 }
1262}
1263
1264struct MeasurementBuilder {
1266 rows: usize,
1267 timestamps: arrow::array::Int64Builder,
1268 tags: std::collections::BTreeMap<Arc<str>, arrow::array::StringBuilder>,
1269 fields: std::collections::BTreeMap<Arc<str>, FieldBuilder>,
1270}
1271
1272impl MeasurementBuilder {
1273 fn new() -> Self {
1274 Self {
1275 rows: 0,
1276 timestamps: arrow::array::Int64Builder::new(),
1277 tags: std::collections::BTreeMap::new(),
1278 fields: std::collections::BTreeMap::new(),
1279 }
1280 }
1281
1282 fn push(&mut self, key: &MemtableKey, entry: &MemtableEntry) {
1283 self.timestamps.append_value(key.timestamp);
1284 for (k, v) in entry.tags.iter() {
1287 let rows = self.rows;
1288 self.tags.entry(Arc::clone(k)).or_insert_with(|| {
1289 let mut b = arrow::array::StringBuilder::new();
1290 for _ in 0..rows {
1291 b.append_null();
1292 }
1293 b
1294 });
1295 let _ = v;
1296 }
1297 for (name, b) in &mut self.tags {
1298 match entry.tags.iter().find(|(k, _)| k == name) {
1299 Some((_, v)) => b.append_value(v),
1300 None => b.append_null(),
1301 }
1302 }
1303 for (k, v) in entry.fields.iter() {
1304 let rows = self.rows;
1305 self.fields
1306 .entry(Arc::clone(k))
1307 .or_insert_with(|| FieldBuilder::for_value(v, rows));
1308 }
1309 for (name, b) in &mut self.fields {
1310 match entry.fields.iter().find(|(k, _)| k == name) {
1311 Some((_, v)) => b.append(v),
1312 None => b.append_null(),
1313 }
1314 }
1315 self.rows += 1;
1316 }
1317
1318 fn finish(mut self, measurement: &str) -> Option<MeasurementBatch> {
1319 use arrow::datatypes::{DataType, Field, Schema};
1320 if self.rows == 0 {
1321 return None;
1322 }
1323 use crate::segment::metadata::roles;
1324 let mut fields = vec![
1327 Field::new(chronix_core::TIME_COLUMN, DataType::Int64, false)
1328 .with_metadata(roles::arrow_metadata(roles::TIMESTAMP)),
1329 ];
1330 let mut columns: Vec<arrow::array::ArrayRef> = vec![Arc::new(self.timestamps.finish())];
1331 let tag_columns: Vec<String> = self.tags.keys().map(ToString::to_string).collect();
1332 for (name, mut b) in self.tags {
1333 fields.push(
1334 Field::new(name.as_ref(), DataType::Utf8, true)
1335 .with_metadata(roles::arrow_metadata(roles::TAG)),
1336 );
1337 columns.push(Arc::new(b.finish()));
1338 }
1339 for (name, b) in self.fields {
1340 let (dt, arr) = b.finish();
1341 fields.push(
1342 Field::new(name.as_ref(), dt, true)
1343 .with_metadata(roles::arrow_metadata(roles::FIELD)),
1344 );
1345 columns.push(arr);
1346 }
1347 let batch =
1348 arrow::record_batch::RecordBatch::try_new(Arc::new(Schema::new(fields)), columns)
1349 .ok()?;
1350 Some(MeasurementBatch {
1351 measurement: measurement.to_string(),
1352 batch,
1353 tag_columns,
1354 })
1355 }
1356}
1357
1358impl Memtable {
1359 #[must_use]
1368 pub fn to_record_batches(&self) -> Vec<MeasurementBatch> {
1369 let mut builders: std::collections::BTreeMap<Arc<str>, MeasurementBuilder> =
1370 std::collections::BTreeMap::new();
1371 for entry in self.data.iter() {
1372 let value = entry.value();
1373 builders
1374 .entry(Arc::clone(&value.measurement))
1375 .or_insert_with(MeasurementBuilder::new)
1376 .push(entry.key(), value);
1377 }
1378 builders
1379 .into_iter()
1380 .filter_map(|(m, b)| b.finish(&m))
1381 .collect()
1382 }
1383}