1use std::collections::BTreeMap;
17use std::sync::Arc;
18use std::time::Duration;
19
20use parking_lot::RwLock;
21
22use chronix_core::types::{Point, ShardId, Timestamp};
23
24use crate::memtable::error::{MemtableError, Result};
25use crate::memtable::flush::{FlushConfig, FlushController, FlushResult};
26
27#[derive(Debug, Clone)]
29pub struct ShardRouterConfig {
30 pub shard_duration: Duration,
32 pub ooo_shard_tolerance: i64,
35 pub flush_config: FlushConfig,
37}
38
39impl Default for ShardRouterConfig {
40 fn default() -> Self {
41 Self {
42 shard_duration: Duration::from_secs(3600), ooo_shard_tolerance: 1,
44 flush_config: FlushConfig::default(),
45 }
46 }
47}
48
49struct ShardEntry {
51 controller: Arc<FlushController>,
53}
54
55pub struct ShardRouter {
61 shards: Arc<RwLock<BTreeMap<ShardId, ShardEntry>>>,
63 config: ShardRouterConfig,
65 active_shard: Arc<RwLock<Option<ShardId>>>,
67}
68
69impl std::fmt::Debug for ShardRouter {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 f.debug_struct("ShardRouter")
72 .field("config", &self.config)
73 .field("active_shard", &*self.active_shard.read())
74 .field("shard_count", &self.shards.read().len())
75 .finish()
76 }
77}
78
79impl ShardRouter {
80 #[must_use]
82 pub fn new(config: ShardRouterConfig) -> Self {
83 Self {
84 shards: Arc::new(RwLock::new(BTreeMap::new())),
85 config,
86 active_shard: Arc::new(RwLock::new(None)),
87 }
88 }
89
90 pub fn insert(&self, point: &Point) -> Result<()> {
97 let shard_id = ShardId::from_timestamp(point.timestamp(), self.config.shard_duration);
98
99 self.ensure_shard_allowed(shard_id, point.timestamp())?;
100 self.insert_into_shard(shard_id, point)
101 }
102
103 pub fn insert_with_wal_seq(&self, point: &Point, wal_seq: u64) -> Result<()> {
110 let shard_id = ShardId::from_timestamp(point.timestamp(), self.config.shard_duration);
111
112 self.ensure_shard_allowed(shard_id, point.timestamp())?;
113 self.insert_into_shard_with_wal_seq(shard_id, point, wal_seq)
114 }
115
116 pub fn admit(&self, timestamp: Timestamp) -> Result<ShardId> {
131 let shard_id = ShardId::from_timestamp(timestamp, self.config.shard_duration);
132 self.ensure_shard_allowed(shard_id, timestamp)?;
133 Ok(shard_id)
134 }
135
136 pub fn insert_admitted(&self, point: &Point, wal_seq: u64) -> Result<()> {
149 let shard_id = ShardId::from_timestamp(point.timestamp(), self.config.shard_duration);
150 self.insert_into_shard_with_wal_seq(shard_id, point, wal_seq)
151 }
152
153 pub fn insert_replay(&self, point: &Point, wal_seq: u64) -> Result<()> {
167 let shard_id = ShardId::from_timestamp(point.timestamp(), self.config.shard_duration);
168
169 {
171 let mut active_guard = self.active_shard.write();
172 match *active_guard {
173 Some(active) if shard_id.0 > active.0 => {
174 *active_guard = Some(shard_id);
175 }
176 None => {
177 *active_guard = Some(shard_id);
178 }
179 _ => {}
180 }
181 }
182
183 self.insert_into_shard_with_wal_seq(shard_id, point, wal_seq)
184 }
185
186 pub fn flush_shard(&self, shard_id: ShardId) -> Result<Vec<FlushResult>> {
195 self.flush_shard_with(shard_id, |_| Ok(()))
196 }
197
198 pub fn flush_shard_with<F>(
212 &self,
213 shard_id: ShardId,
214 mut register: F,
215 ) -> Result<Vec<FlushResult>>
216 where
217 F: FnMut(&[FlushResult]) -> Result<()>,
218 {
219 let controller = {
220 let shards = self.shards.read();
221 let entry = shards
222 .get(&shard_id)
223 .ok_or(MemtableError::NoSuchShard(shard_id.0))?;
224 Arc::clone(&entry.controller)
225 };
226
227 match controller.freeze_and_swap() {
230 Ok(()) | Err(MemtableError::CapacityExceeded { .. }) => {}
231 Err(e) => return Err(e),
232 }
233 let mut all = Vec::new();
235 while controller.frozen_count() > 0 {
236 match controller.flush_frozen_with(&mut register) {
237 Ok(results) => all.extend(results),
238 Err(MemtableError::NoFrozenMemtable) => continue,
239 Err(e) => {
240 if all.is_empty() {
241 return Err(e);
242 }
243 tracing::warn!(shard = %shard_id, error = %e, "flush stopped early");
246 return Ok(all);
247 }
248 }
249 }
250 if all.is_empty() {
251 return Err(MemtableError::NoFrozenMemtable);
252 }
253 Ok(all)
254 }
255
256 #[must_use]
262 pub fn active_shard(&self) -> Option<ShardId> {
263 *self.active_shard.read()
264 }
265
266 pub fn seed_active_shard(&self, shard: ShardId) {
274 let mut active = self.active_shard.write();
275 if active.is_none_or(|a| shard.0 > a.0) {
276 *active = Some(shard);
277 }
278 }
279
280 pub fn retire_idle_shards(&self) {
287 let Some(active) = self.active_shard() else {
288 return;
289 };
290 let oldest_live = active.0.saturating_sub(self.config.ooo_shard_tolerance);
291 let mut shards = self.shards.write();
292 shards.retain(|id, entry| id.0 >= oldest_live || !entry.controller.is_idle());
293 }
294
295 #[must_use]
297 pub fn shard_ids(&self) -> Vec<ShardId> {
298 self.shards.read().keys().copied().collect()
299 }
300
301 #[must_use]
309 pub fn live_series(&self) -> std::collections::HashSet<String> {
310 let mut out = std::collections::HashSet::new();
311 for entry in self.shards.read().values() {
312 for key in entry.controller.all_series_canonical_forms() {
313 out.insert(key.to_string());
314 }
315 }
316 out
317 }
318
319 #[must_use]
324 pub fn live_tag_pairs(&self) -> std::collections::BTreeSet<(String, String)> {
325 let mut out = std::collections::BTreeSet::new();
326 let mut visit = |key: &str, value: &str| {
327 out.insert((key.to_owned(), value.to_owned()));
328 };
329 for entry in self.shards.read().values() {
330 entry.controller.visit_tags(&mut visit);
331 }
332 out
333 }
334
335 #[must_use]
337 pub fn total_memory(&self) -> usize {
338 self.shards
339 .read()
340 .values()
341 .map(|e| e.controller.total_memory())
342 .sum()
343 }
344
345 #[must_use]
351 pub fn total_interner_memory(&self) -> usize {
352 self.shards
353 .read()
354 .values()
355 .map(|e| e.controller.total_interner_memory())
356 .sum()
357 }
358
359 #[must_use]
363 pub fn min_unflushed_wal_seq(&self) -> Option<u64> {
364 self.shards
365 .read()
366 .values()
367 .filter_map(|e| e.controller.min_unflushed_wal_seq())
368 .min()
369 }
370
371 #[must_use]
373 pub fn scan(
374 &self,
375 series_key: &chronix_core::SeriesKey,
376 min_ts: Timestamp,
377 max_ts: Timestamp,
378 ) -> Vec<Point> {
379 let controllers: Vec<Arc<FlushController>> = self
381 .shards
382 .read()
383 .values()
384 .map(|e| Arc::clone(&e.controller))
385 .collect();
386
387 let mut all_points = Vec::new();
388 for ctrl in &controllers {
389 all_points.extend(ctrl.scan(series_key, min_ts, max_ts));
390 }
391
392 all_points.sort_by_key(Point::timestamp);
394 all_points
395 }
396
397 #[must_use]
402 pub fn scan_measurement(
403 &self,
404 measurement: &str,
405 min_ts: Timestamp,
406 max_ts: Timestamp,
407 ) -> Vec<Point> {
408 let controllers: Vec<Arc<FlushController>> = self
410 .shards
411 .read()
412 .values()
413 .map(|e| Arc::clone(&e.controller))
414 .collect();
415
416 let mut all_points = Vec::new();
417 for ctrl in &controllers {
418 all_points.extend(ctrl.scan_measurement(measurement, min_ts, max_ts));
419 }
420
421 all_points.sort_by_key(Point::timestamp);
423 all_points
424 }
425
426 #[must_use]
428 pub fn shard_for_timestamp(&self, timestamp: Timestamp) -> ShardId {
429 ShardId::from_timestamp(timestamp, self.config.shard_duration)
430 }
431
432 fn ensure_shard_allowed(&self, shard_id: ShardId, timestamp: Timestamp) -> Result<()> {
437 let mut active_guard = self.active_shard.write();
438
439 let active = if let Some(active) = *active_guard {
440 if shard_id.0 > active.0 {
442 *active_guard = Some(shard_id);
443 shard_id
444 } else {
445 active
446 }
447 } else {
448 *active_guard = Some(shard_id);
449 shard_id
450 };
451
452 let min_allowed = active.0.saturating_sub(self.config.ooo_shard_tolerance);
453 let max_allowed = active.0.saturating_add(self.config.ooo_shard_tolerance);
454
455 if shard_id.0 < min_allowed || shard_id.0 > max_allowed {
456 return Err(MemtableError::ShardOutOfRange {
457 timestamp,
458 earliest: ShardId(min_allowed).start_timestamp(self.config.shard_duration),
459 latest: ShardId(max_allowed).end_timestamp(self.config.shard_duration),
460 tolerance: self.config.ooo_shard_tolerance,
461 shard_secs: self.config.shard_duration.as_secs(),
462 });
463 }
464
465 Ok(())
466 }
467
468 fn with_shard<F, R>(&self, shard_id: ShardId, f: F) -> Result<R>
471 where
472 F: FnOnce(&FlushController) -> Result<R>,
473 {
474 {
476 let shards = self.shards.read();
477 if let Some(entry) = shards.get(&shard_id) {
478 return f(&entry.controller);
479 }
480 }
481
482 let mut shards = self.shards.write();
484 let entry = shards.entry(shard_id).or_insert_with(|| {
485 let mut config = self.config.flush_config.clone();
486 config.segment_dir = config.segment_dir.join(format!("shard_{}", shard_id.0));
487 ShardEntry {
488 controller: Arc::new(FlushController::new(config)),
489 }
490 });
491 f(&entry.controller)
492 }
493
494 fn insert_into_shard(&self, shard_id: ShardId, point: &Point) -> Result<()> {
496 self.with_shard(shard_id, |ctrl| ctrl.insert(point))
497 }
498
499 fn insert_into_shard_with_wal_seq(
501 &self,
502 shard_id: ShardId,
503 point: &Point,
504 wal_seq: u64,
505 ) -> Result<()> {
506 self.with_shard(shard_id, |ctrl| ctrl.insert_with_wal_seq(point, wal_seq))
507 }
508}
509#[cfg(test)]
510mod tests {
511 use super::*;
512 use std::collections::BTreeMap as StdBTreeMap;
513
514 use crate::segment::writer::SegmentWriterConfig;
515 use chronix_core::types::{FieldValue, SeriesKey};
516
517 fn make_point(ts: Timestamp, value: f64) -> Point {
518 make_point_with_measurement("cpu", ts, value)
519 }
520
521 fn make_point_with_measurement(measurement: &str, ts: Timestamp, value: f64) -> Point {
522 let tags: StdBTreeMap<String, String> = [("host".to_string(), "a".to_string())]
523 .into_iter()
524 .collect();
525 let series_key = SeriesKey::new(measurement.to_string(), tags).unwrap();
526 let fields: StdBTreeMap<String, FieldValue> =
527 [("value".to_string(), FieldValue::F64(value))]
528 .into_iter()
529 .collect();
530 Point::new(series_key, fields, ts).unwrap()
531 }
532
533 fn test_router_config(dir: &std::path::Path) -> ShardRouterConfig {
534 ShardRouterConfig {
535 shard_duration: Duration::from_secs(3600),
536 ooo_shard_tolerance: 1,
537 flush_config: FlushConfig {
538 flush_threshold: 64 * 1024 * 1024,
539 max_memory: 256 * 1024 * 1024,
540 segment_dir: dir.to_path_buf(),
541 segment_writer_config: SegmentWriterConfig {
542 compress: false,
543 ..Default::default()
544 },
545 ..Default::default()
546 },
547 }
548 }
549
550 const NS_PER_HOUR: i64 = 3_600_000_000_000;
552
553 #[test]
554 fn routes_to_correct_shard() {
555 let dir = tempfile::tempdir().unwrap();
556 let router = ShardRouter::new(test_router_config(dir.path()));
557
558 let p1 = make_point(100, 1.0);
560 router.insert(&p1).unwrap();
561
562 assert_eq!(router.shard_ids().len(), 1);
563 assert_eq!(router.shard_ids()[0], ShardId(0));
564 }
565
566 #[test]
567 fn routes_to_multiple_shards() {
568 let dir = tempfile::tempdir().unwrap();
569 let router = ShardRouter::new(test_router_config(dir.path()));
570
571 router.insert(&make_point(100, 1.0)).unwrap();
573 router.insert(&make_point(NS_PER_HOUR + 100, 2.0)).unwrap();
575
576 let shard_ids = router.shard_ids();
577 assert_eq!(shard_ids.len(), 2);
578 }
579
580 #[test]
581 fn ooo_within_tolerance_accepted() {
582 let dir = tempfile::tempdir().unwrap();
583 let router = ShardRouter::new(test_router_config(dir.path()));
584
585 router.insert(&make_point(NS_PER_HOUR + 100, 1.0)).unwrap();
587
588 router.insert(&make_point(100, 2.0)).unwrap();
590
591 assert_eq!(router.shard_ids().len(), 2);
592 }
593
594 #[test]
595 fn ooo_beyond_tolerance_rejected() {
596 let dir = tempfile::tempdir().unwrap();
597 let mut config = test_router_config(dir.path());
598 config.ooo_shard_tolerance = 0; let router = ShardRouter::new(config);
600
601 router.insert(&make_point(NS_PER_HOUR + 100, 1.0)).unwrap();
603
604 let result = router.insert(&make_point(100, 2.0));
606 assert!(result.is_err());
607 }
608
609 #[test]
610 fn flush_shard() {
611 let dir = tempfile::tempdir().unwrap();
612 let router = ShardRouter::new(test_router_config(dir.path()));
613
614 for i in 0..10 {
615 router.insert(&make_point(i * 100, i as f64)).unwrap();
616 }
617
618 let shard_id = ShardId(0);
619 let results = router.flush_shard(shard_id).unwrap();
620 assert_eq!(results.len(), 1);
621 assert_eq!(results[0].measurement, "cpu");
622 assert_eq!(results[0].points_flushed, 10);
623 }
624
625 #[test]
626 fn scan_across_shards() {
627 let dir = tempfile::tempdir().unwrap();
628 let router = ShardRouter::new(test_router_config(dir.path()));
629
630 router.insert(&make_point(100, 1.0)).unwrap();
632 router.insert(&make_point(NS_PER_HOUR + 100, 2.0)).unwrap();
634
635 let key = SeriesKey::new(
636 "cpu".to_string(),
637 [("host".to_string(), "a".to_string())]
638 .into_iter()
639 .collect(),
640 )
641 .unwrap();
642
643 let points = router.scan(&key, 0, NS_PER_HOUR * 2);
644 assert_eq!(points.len(), 2);
645 assert!(points[0].timestamp() < points[1].timestamp());
646 }
647
648 #[test]
649 fn total_memory_across_shards() {
650 let dir = tempfile::tempdir().unwrap();
651 let router = ShardRouter::new(test_router_config(dir.path()));
652
653 router.insert(&make_point(100, 1.0)).unwrap();
654 router.insert(&make_point(NS_PER_HOUR + 100, 2.0)).unwrap();
655
656 assert!(router.total_memory() > 0);
657 }
658
659 #[test]
660 fn shard_for_timestamp_computation() {
661 let dir = tempfile::tempdir().unwrap();
662 let router = ShardRouter::new(test_router_config(dir.path()));
663
664 assert_eq!(router.shard_for_timestamp(0), ShardId(0));
665 assert_eq!(router.shard_for_timestamp(NS_PER_HOUR), ShardId(1));
666 assert_eq!(router.shard_for_timestamp(NS_PER_HOUR - 1), ShardId(0));
667 }
668
669 #[test]
670 fn scan_measurement_across_shards() {
671 let dir = tempfile::tempdir().unwrap();
672 let router = ShardRouter::new(test_router_config(dir.path()));
673
674 router
676 .insert(&make_point_with_measurement("cpu", 100, 1.0))
677 .unwrap();
678 router
679 .insert(&make_point_with_measurement("mem", 200, 2.0))
680 .unwrap();
681 router
683 .insert(&make_point_with_measurement("cpu", NS_PER_HOUR + 100, 3.0))
684 .unwrap();
685
686 let cpu = router.scan_measurement("cpu", 0, i64::MAX);
688 assert_eq!(cpu.len(), 2);
689 assert!(cpu.iter().all(|p| p.series_key().measurement() == "cpu"));
690 assert!(cpu[0].timestamp() < cpu[1].timestamp()); let mem = router.scan_measurement("mem", 0, i64::MAX);
694 assert_eq!(mem.len(), 1);
695
696 let none = router.scan_measurement("disk", 0, i64::MAX);
698 assert!(none.is_empty());
699 }
700
701 #[test]
702 fn scan_measurement_respects_time_range() {
703 let dir = tempfile::tempdir().unwrap();
704 let router = ShardRouter::new(test_router_config(dir.path()));
705
706 router
707 .insert(&make_point_with_measurement("cpu", 100, 1.0))
708 .unwrap();
709 router
710 .insert(&make_point_with_measurement("cpu", 200, 2.0))
711 .unwrap();
712 router
713 .insert(&make_point_with_measurement("cpu", 300, 3.0))
714 .unwrap();
715
716 let result = router.scan_measurement("cpu", 150, 250);
717 assert_eq!(result.len(), 1);
718 assert_eq!(result[0].timestamp(), 200);
719 }
720}