1use std::path::{Path, PathBuf};
10use std::sync::Arc;
11use std::time::SystemTime;
12
13use dashmap::DashMap;
14use serde::{Deserialize, Serialize};
15use tracing::{debug, info};
16
17use chronix_core::{NamespaceId, NamespaceQuota, NamespaceUsage};
18
19use crate::tenant::error::{Result, TenantError};
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct NamespaceInfo {
24 pub id: NamespaceId,
26 pub description: String,
28 pub owner: String,
30 pub quota: NamespaceQuota,
32 pub created_at_ms: u64,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct NamespaceState {
39 pub info: NamespaceInfo,
41 pub usage: NamespaceUsage,
43}
44
45#[derive(Debug)]
56pub struct NamespaceRegistry {
57 namespaces: DashMap<String, NamespaceState>,
59 persist_dir: Option<PathBuf>,
61}
62
63const SNAPSHOT_FILE: &str = "namespaces.json";
65
66fn write_snapshot(dir: &Path, states: &[NamespaceState]) -> Result<()> {
68 let json = serde_json::to_string_pretty(states)
69 .map_err(|e| TenantError::InvalidConfig(format!("cannot serialize namespaces: {e}")))?;
70
71 let tmp_path = dir.join("namespaces.json.tmp");
72 let final_path = dir.join(SNAPSHOT_FILE);
73 std::fs::write(&tmp_path, &json)
74 .map_err(|e| TenantError::Persist(format!("cannot write namespace snapshot: {e}")))?;
75
76 let result = (|| -> Result<()> {
79 let f = std::fs::File::open(&tmp_path).map_err(|e| {
80 TenantError::Persist(format!("cannot open tmp snapshot for fsync: {e}"))
81 })?;
82 f.sync_all()
83 .map_err(|e| TenantError::Persist(format!("cannot fsync namespace snapshot: {e}")))?;
84
85 std::fs::rename(&tmp_path, &final_path)
86 .map_err(|e| TenantError::Persist(format!("cannot rename namespace snapshot: {e}")))?;
87 Ok(())
88 })();
89
90 if result.is_err() {
91 let _ = std::fs::remove_file(&tmp_path);
92 }
93
94 result?;
95
96 debug!("namespace snapshot saved");
97 Ok(())
98}
99impl NamespaceRegistry {
100 #[must_use]
104 pub fn new() -> Self {
105 let registry = Self {
106 namespaces: DashMap::new(),
107 persist_dir: None,
108 };
109
110 registry.ensure_default();
111 registry
112 }
113
114 pub fn open(dir: impl Into<PathBuf>) -> Result<Self> {
125 let dir = dir.into();
126 std::fs::create_dir_all(&dir)
127 .map_err(|e| TenantError::InvalidConfig(format!("cannot create namespace dir: {e}")))?;
128
129 let snapshot_path = dir.join(SNAPSHOT_FILE);
130 let registry = if snapshot_path.exists() {
131 let data = std::fs::read_to_string(&snapshot_path).map_err(|e| {
132 TenantError::InvalidConfig(format!("cannot read namespace snapshot: {e}"))
133 })?;
134 let states: Vec<NamespaceState> = serde_json::from_str(&data).map_err(|e| {
135 TenantError::InvalidConfig(format!("corrupt namespace snapshot: {e}"))
136 })?;
137 let namespaces = DashMap::new();
138 for state in states {
139 namespaces.insert(state.info.id.as_str().to_string(), state);
140 }
141 info!(count = namespaces.len(), "loaded namespace snapshot");
142 Self {
143 namespaces,
144 persist_dir: Some(dir),
145 }
146 } else {
147 Self {
148 namespaces: DashMap::new(),
149 persist_dir: Some(dir),
150 }
151 };
152
153 registry.ensure_default();
154
155 if !snapshot_path.exists() {
157 registry.save_snapshot_inner()?;
158 }
159
160 Ok(registry)
161 }
162
163 fn ensure_default(&self) {
165 if !self.namespaces.contains_key(NamespaceId::DEFAULT) {
166 let default_info = NamespaceInfo {
167 id: NamespaceId::default_namespace(),
168 description: "Default namespace".to_string(),
169 owner: "system".to_string(),
170 quota: NamespaceQuota::default(),
171 created_at_ms: now_ms(),
172 };
173 self.namespaces.insert(
174 NamespaceId::DEFAULT.to_string(),
175 NamespaceState {
176 info: default_info,
177 usage: NamespaceUsage::default(),
178 },
179 );
180 }
181 }
182
183 fn save_snapshot_inner(&self) -> Result<()> {
187 let Some(dir) = &self.persist_dir else {
188 return Ok(());
189 };
190
191 let states: Vec<NamespaceState> =
192 self.namespaces.iter().map(|r| r.value().clone()).collect();
193
194 write_snapshot(dir, &states)
195 }
196
197 pub fn create_namespace(
204 &self,
205 id: NamespaceId,
206 description: impl Into<String>,
207 owner: impl Into<String>,
208 quota: NamespaceQuota,
209 ) -> Result<NamespaceInfo> {
210 let name = id.as_str().to_string();
211
212 use dashmap::mapref::entry::Entry;
215 match self.namespaces.entry(name.clone()) {
216 Entry::Occupied(_) => Err(TenantError::NamespaceAlreadyExists(name)),
217 Entry::Vacant(vacant) => {
218 let info = NamespaceInfo {
219 id,
220 description: description.into(),
221 owner: owner.into(),
222 quota,
223 created_at_ms: now_ms(),
224 };
225
226 let state = NamespaceState {
227 info: info.clone(),
228 usage: NamespaceUsage::default(),
229 };
230
231 vacant.insert(state);
232
233 info!(namespace = %name, "namespace created");
234 #[allow(clippy::cast_precision_loss)]
235 metrics::gauge!("chronix_namespace_count").set(self.namespaces.len() as f64);
236
237 if let Err(e) = self.save_snapshot_inner() {
245 self.namespaces.remove(&name);
246 #[allow(clippy::cast_precision_loss)]
247 metrics::gauge!("chronix_namespace_count").set(self.namespaces.len() as f64);
248 return Err(e);
249 }
250 Ok(info)
251 }
252 }
253 }
254
255 pub fn delete_namespace(&self, name: &str) -> Result<NamespaceInfo> {
262 if name == NamespaceId::DEFAULT {
263 return Err(TenantError::InvalidConfig(
264 "cannot delete the default namespace".to_string(),
265 ));
266 }
267
268 let (_, state) = self
269 .namespaces
270 .remove(name)
271 .ok_or_else(|| TenantError::NamespaceNotFound(name.to_string()))?;
272
273 info!(namespace = %name, "namespace deleted");
274 #[allow(clippy::cast_precision_loss)]
275 metrics::gauge!("chronix_namespace_count").set(self.namespaces.len() as f64);
276
277 self.save_snapshot_inner()?;
280 Ok(state.info)
281 }
282
283 pub fn get(&self, name: &str) -> Result<NamespaceState> {
289 self.namespaces
290 .get(name)
291 .map(|r| r.value().clone())
292 .ok_or_else(|| TenantError::NamespaceNotFound(name.to_string()))
293 }
294
295 #[must_use]
297 pub fn exists(&self, name: &str) -> bool {
298 self.namespaces.contains_key(name)
299 }
300
301 #[must_use]
303 pub fn list_names(&self) -> Vec<String> {
304 self.namespaces.iter().map(|r| r.key().clone()).collect()
305 }
306
307 #[must_use]
309 pub fn list_all(&self) -> Vec<NamespaceState> {
310 self.namespaces.iter().map(|r| r.value().clone()).collect()
311 }
312
313 #[must_use]
315 pub fn count(&self) -> usize {
316 self.namespaces.len()
317 }
318
319 pub fn update_quota(&self, name: &str, quota: NamespaceQuota) -> Result<()> {
325 quota.validate().map_err(TenantError::InvalidConfig)?;
327 {
328 let mut entry = self
329 .namespaces
330 .get_mut(name)
331 .ok_or_else(|| TenantError::NamespaceNotFound(name.to_string()))?;
332 entry.value_mut().info.quota = quota;
333 }
334 debug!(namespace = %name, "quota updated");
335 self.save_snapshot_inner()?;
339 Ok(())
340 }
341
342 #[must_use]
346 pub fn get_usage(&self, name: &str) -> Option<NamespaceUsage> {
347 self.namespaces.get(name).map(|r| r.value().usage.clone())
348 }
349
350 pub fn update_usage(&self, name: &str, usage: NamespaceUsage) -> Result<()> {
356 let mut entry = self
357 .namespaces
358 .get_mut(name)
359 .ok_or_else(|| TenantError::NamespaceNotFound(name.to_string()))?;
360
361 entry.value_mut().usage = usage;
362 Ok(())
363 }
364
365 pub fn increment_series(&self, name: &str, count: u64) -> Result<()> {
373 let mut entry = self
374 .namespaces
375 .get_mut(name)
376 .ok_or_else(|| TenantError::NamespaceNotFound(name.to_string()))?;
377
378 entry.value_mut().usage.series_count =
379 entry.value().usage.series_count.saturating_add(count);
380 Ok(())
381 }
382
383 pub fn increment_storage(&self, name: &str, bytes: u64) -> Result<()> {
391 let mut entry = self
392 .namespaces
393 .get_mut(name)
394 .ok_or_else(|| TenantError::NamespaceNotFound(name.to_string()))?;
395
396 entry.value_mut().usage.storage_bytes =
397 entry.value().usage.storage_bytes.saturating_add(bytes);
398 Ok(())
399 }
400
401 pub fn increment_measurements(&self, name: &str, count: u32) -> Result<()> {
409 let mut entry = self
410 .namespaces
411 .get_mut(name)
412 .ok_or_else(|| TenantError::NamespaceNotFound(name.to_string()))?;
413
414 entry.value_mut().usage.measurements =
415 entry.value().usage.measurements.saturating_add(count);
416 Ok(())
417 }
418
419 pub fn check_and_increment_write(
434 &self,
435 name: &str,
436 new_series: u64,
437 new_points: u64,
438 ) -> Result<()> {
439 let mut entry = self
440 .namespaces
441 .get_mut(name)
442 .ok_or_else(|| TenantError::NamespaceNotFound(name.to_string()))?;
443
444 let max_series = entry.value().info.quota.max_series_count;
447 let max_rate = entry.value().info.quota.max_ingestion_rate;
448 let max_storage = entry.value().info.quota.max_storage_bytes;
449 let current_series = entry.value().usage.series_count;
450 let current_storage = entry.value().usage.storage_bytes;
451
452 let projected_series = current_series.saturating_add(new_series);
454 if projected_series > max_series {
455 return Err(TenantError::QuotaExceeded {
456 namespace: name.to_string(),
457 resource: "series_count".to_string(),
458 current: current_series,
459 limit: max_series,
460 });
461 }
462
463 let now_s = now_ms() / 1000;
466 let last_window = entry.value().usage.ingestion_rate_window_s;
467 let current_rate = if last_window == now_s {
468 entry.value().usage.ingestion_rate
469 } else {
470 0.0
472 };
473
474 #[allow(clippy::cast_precision_loss)]
475 let projected_rate = current_rate + new_points as f64;
476 #[allow(clippy::cast_precision_loss)]
477 let rate_limit = max_rate as f64;
478 if projected_rate > rate_limit {
479 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
480 let current = current_rate as u64;
481 return Err(TenantError::QuotaExceeded {
482 namespace: name.to_string(),
483 resource: "ingestion_rate".to_string(),
484 current,
485 limit: max_rate,
486 });
487 }
488
489 if current_storage > max_storage {
491 return Err(TenantError::QuotaExceeded {
492 namespace: name.to_string(),
493 resource: "storage_bytes".to_string(),
494 current: current_storage,
495 limit: max_storage,
496 });
497 }
498
499 let state_mut = entry.value_mut();
502 state_mut.usage.series_count = projected_series;
503 state_mut.usage.ingestion_rate = projected_rate;
504 state_mut.usage.ingestion_rate_window_s = now_s;
505
506 Ok(())
507 }
508
509 pub fn check_and_increment_measurement(&self, name: &str) -> Result<()> {
519 let mut entry = self
520 .namespaces
521 .get_mut(name)
522 .ok_or_else(|| TenantError::NamespaceNotFound(name.to_string()))?;
523
524 let state = entry.value();
525 let projected = u64::from(state.usage.measurements) + 1;
526 let limit = u64::from(state.info.quota.max_measurements);
527
528 if projected > limit {
529 return Err(TenantError::QuotaExceeded {
530 namespace: name.to_string(),
531 resource: "measurements".to_string(),
532 current: u64::from(state.usage.measurements),
533 limit,
534 });
535 }
536
537 entry.value_mut().usage.measurements = state.usage.measurements.saturating_add(1);
538 Ok(())
539 }
540
541 #[must_use]
547 pub fn usage_ratios(&self) -> Vec<UsageRatio> {
548 let mut ratios = Vec::new();
549
550 for entry in &self.namespaces {
551 let ns = entry.key();
552 let state = entry.value();
553 let q = &state.info.quota;
554 let u = &state.usage;
555
556 ratios.push(UsageRatio {
557 namespace: ns.clone(),
558 resource: "series_count".to_string(),
559 current: u.series_count,
560 limit: q.max_series_count,
561 ratio: ratio(u.series_count, q.max_series_count),
562 });
563 ratios.push(UsageRatio {
564 namespace: ns.clone(),
565 resource: "storage_bytes".to_string(),
566 current: u.storage_bytes,
567 limit: q.max_storage_bytes,
568 ratio: ratio(u.storage_bytes, q.max_storage_bytes),
569 });
570 ratios.push(UsageRatio {
571 namespace: ns.clone(),
572 resource: "measurements".to_string(),
573 current: u64::from(u.measurements),
574 limit: u64::from(q.max_measurements),
575 ratio: ratio(u64::from(u.measurements), u64::from(q.max_measurements)),
576 });
577 }
578
579 ratios
580 }
581
582 pub fn emit_usage_metrics(&self) {
588 for r in self.usage_ratios() {
589 metrics::gauge!(
590 "chronix_namespace_usage_ratio",
591 "namespace" => r.namespace,
592 "resource" => r.resource,
593 )
594 .set(r.ratio);
595 }
596 }
597}
598
599#[derive(Debug, Clone, Serialize, Deserialize)]
601pub struct UsageRatio {
602 pub namespace: String,
604 pub resource: String,
606 pub current: u64,
608 pub limit: u64,
610 pub ratio: f64,
612}
613
614#[allow(clippy::cast_precision_loss)]
620fn ratio(current: u64, limit: u64) -> f64 {
621 if limit == 0 {
622 return if current > 0 { f64::MAX } else { 0.0 };
623 }
624 current as f64 / limit as f64
625}
626
627impl Default for NamespaceRegistry {
628 fn default() -> Self {
629 Self::new()
630 }
631}
632
633fn now_ms() -> u64 {
634 #[allow(clippy::cast_possible_truncation)]
635 SystemTime::now()
636 .duration_since(std::time::UNIX_EPOCH)
637 .map(|d| d.as_millis() as u64)
638 .unwrap_or(0)
639}
640
641#[must_use]
643pub fn shared_registry() -> Arc<NamespaceRegistry> {
644 Arc::new(NamespaceRegistry::new())
645}
646
647#[cfg(test)]
648mod tests {
649
650 #[test]
660 fn creating_a_namespace_is_durable_or_it_fails() {
661 let dir = tempfile::tempdir().unwrap();
662 let registry = NamespaceRegistry::open(dir.path()).unwrap();
663
664 std::fs::create_dir_all(dir.path().join("namespaces.json.tmp")).unwrap();
666
667 let err = registry.create_namespace(
668 NamespaceId::new("tenant-a").unwrap(),
669 "a tenant",
670 "ops",
671 NamespaceQuota::default(),
672 );
673 assert!(
674 err.is_err(),
675 "a namespace that cannot be persisted must not be acknowledged"
676 );
677 }
678
679 #[test]
682 fn changing_a_quota_is_durable_or_it_fails() {
683 let dir = tempfile::tempdir().unwrap();
684 let registry = NamespaceRegistry::open(dir.path()).unwrap();
685 registry
686 .create_namespace(
687 NamespaceId::new("tenant-a").unwrap(),
688 "a tenant",
689 "ops",
690 NamespaceQuota::default(),
691 )
692 .unwrap();
693
694 std::fs::create_dir_all(dir.path().join("namespaces.json.tmp")).unwrap();
695
696 let quota = NamespaceQuota {
697 max_series_count: 42,
698 ..NamespaceQuota::default()
699 };
700 assert!(
701 registry.update_quota("tenant-a", quota).is_err(),
702 "a quota that cannot be persisted reverts at the next restart, so \
703 reporting it as applied is a setting that is accepted and ignored"
704 );
705 }
706 use super::*;
707
708 #[test]
709 fn default_namespace_exists_on_creation() {
710 let registry = NamespaceRegistry::new();
711 assert!(registry.exists(NamespaceId::DEFAULT));
712 assert_eq!(registry.count(), 1);
713 }
714
715 #[test]
716 fn create_and_get_namespace() {
717 let registry = NamespaceRegistry::new();
718 let id = NamespaceId::new("team-alpha").unwrap();
719 let info = registry
720 .create_namespace(id, "Team Alpha", "admin", NamespaceQuota::default())
721 .unwrap();
722
723 assert_eq!(info.id.as_str(), "team-alpha");
724 assert_eq!(info.owner, "admin");
725 assert_eq!(registry.count(), 2);
726
727 let state = registry.get("team-alpha").unwrap();
728 assert_eq!(state.info.description, "Team Alpha");
729 }
730
731 #[test]
732 fn create_duplicate_fails() {
733 let registry = NamespaceRegistry::new();
734 let id = NamespaceId::new("my-ns").unwrap();
735 registry
736 .create_namespace(id, "first", "admin", NamespaceQuota::default())
737 .unwrap();
738
739 let id2 = NamespaceId::new("my-ns").unwrap();
740 let err = registry
741 .create_namespace(id2, "second", "admin", NamespaceQuota::default())
742 .unwrap_err();
743
744 assert!(matches!(err, TenantError::NamespaceAlreadyExists(_)));
745 }
746
747 #[test]
748 fn delete_namespace() {
749 let registry = NamespaceRegistry::new();
750 let id = NamespaceId::new("temp-ns").unwrap();
751 registry
752 .create_namespace(id, "temp", "admin", NamespaceQuota::default())
753 .unwrap();
754
755 assert_eq!(registry.count(), 2);
756 registry.delete_namespace("temp-ns").unwrap();
757 assert_eq!(registry.count(), 1);
758 assert!(!registry.exists("temp-ns"));
759 }
760
761 #[test]
762 fn cannot_delete_default_namespace() {
763 let registry = NamespaceRegistry::new();
764 let err = registry.delete_namespace("default").unwrap_err();
765 assert!(matches!(err, TenantError::InvalidConfig(_)));
766 }
767
768 #[test]
769 fn delete_nonexistent_fails() {
770 let registry = NamespaceRegistry::new();
771 let err = registry.delete_namespace("nope").unwrap_err();
772 assert!(matches!(err, TenantError::NamespaceNotFound(_)));
773 }
774
775 #[test]
776 fn list_namespaces() {
777 let registry = NamespaceRegistry::new();
778 let id = NamespaceId::new("ns-a").unwrap();
779 registry
780 .create_namespace(id, "A", "admin", NamespaceQuota::default())
781 .unwrap();
782 let id = NamespaceId::new("ns-b").unwrap();
783 registry
784 .create_namespace(id, "B", "admin", NamespaceQuota::default())
785 .unwrap();
786
787 let mut names = registry.list_names();
788 names.sort();
789 assert_eq!(names, vec!["default", "ns-a", "ns-b"]);
790 }
791
792 #[test]
793 fn update_quota() {
794 let registry = NamespaceRegistry::new();
795 let id = NamespaceId::new("quota-ns").unwrap();
796 registry
797 .create_namespace(id, "quota test", "admin", NamespaceQuota::default())
798 .unwrap();
799
800 let new_quota = NamespaceQuota {
801 max_series_count: 500,
802 max_ingestion_rate: 1000,
803 max_storage_bytes: 1024,
804 max_measurements: 10,
805 max_request_rps: 0,
806 max_request_burst: 0,
807 };
808
809 registry
810 .update_quota("quota-ns", new_quota.clone())
811 .unwrap();
812 let state = registry.get("quota-ns").unwrap();
813 assert_eq!(state.info.quota, new_quota);
814 }
815
816 #[test]
817 fn usage_tracking() {
818 let registry = NamespaceRegistry::new();
819 let id = NamespaceId::new("usage-ns").unwrap();
820 registry
821 .create_namespace(id, "usage test", "admin", NamespaceQuota::default())
822 .unwrap();
823
824 registry.increment_series("usage-ns", 100).unwrap();
825 registry.increment_storage("usage-ns", 4096).unwrap();
826 registry.increment_measurements("usage-ns", 5).unwrap();
827
828 let usage = registry.get_usage("usage-ns").unwrap();
829 assert_eq!(usage.series_count, 100);
830 assert_eq!(usage.storage_bytes, 4096);
831 assert_eq!(usage.measurements, 5);
832 }
833
834 #[test]
835 fn shared_registry_is_arc() {
836 let reg = shared_registry();
837 let reg2 = reg.clone();
838 let id = NamespaceId::new("shared-test").unwrap();
839 reg.create_namespace(id, "test", "admin", NamespaceQuota::default())
840 .unwrap();
841 assert!(reg2.exists("shared-test"));
842 }
843
844 #[test]
845 fn namespace_id_validation() {
846 assert!(NamespaceId::new("valid-name").is_ok());
847 assert!(NamespaceId::new("a123").is_ok());
848 assert!(NamespaceId::new("").is_err()); assert!(NamespaceId::new("Invalid").is_err()); assert!(NamespaceId::new("has space").is_err()); assert!(NamespaceId::new("-leading").is_err()); assert!(NamespaceId::new("trailing-").is_err()); assert!(NamespaceId::new("a".repeat(64)).is_err()); assert!(NamespaceId::new("a".repeat(63)).is_ok()); }
856
857 #[test]
860 fn usage_ratios_empty_registry() {
861 let registry = NamespaceRegistry::new();
862 let ratios = registry.usage_ratios();
863 assert_eq!(ratios.len(), 3);
865 for r in &ratios {
866 assert_eq!(r.namespace, "default");
867 assert!((r.ratio - 0.0).abs() < f64::EPSILON);
868 }
869 }
870
871 #[test]
872 fn usage_ratios_with_usage() {
873 let registry = NamespaceRegistry::new();
874 let id = NamespaceId::new("metrics-ns").unwrap();
875 let quota = NamespaceQuota {
876 max_series_count: 100,
877 max_ingestion_rate: 1000,
878 max_storage_bytes: 10_000,
879 max_measurements: 20,
880 max_request_rps: 0,
881 max_request_burst: 0,
882 };
883 registry
884 .create_namespace(id, "test", "admin", quota)
885 .unwrap();
886
887 registry.increment_series("metrics-ns", 50).unwrap();
888 registry.increment_storage("metrics-ns", 7500).unwrap();
889 registry.increment_measurements("metrics-ns", 10).unwrap();
890
891 let ratios = registry.usage_ratios();
892 let ns_ratios: Vec<_> = ratios
893 .iter()
894 .filter(|r| r.namespace == "metrics-ns")
895 .collect();
896 assert_eq!(ns_ratios.len(), 3);
897
898 let series = ns_ratios
899 .iter()
900 .find(|r| r.resource == "series_count")
901 .unwrap();
902 assert!((series.ratio - 0.5).abs() < f64::EPSILON);
903 assert_eq!(series.current, 50);
904 assert_eq!(series.limit, 100);
905
906 let storage = ns_ratios
907 .iter()
908 .find(|r| r.resource == "storage_bytes")
909 .unwrap();
910 assert!((storage.ratio - 0.75).abs() < f64::EPSILON);
911
912 let measurements = ns_ratios
913 .iter()
914 .find(|r| r.resource == "measurements")
915 .unwrap();
916 assert!((measurements.ratio - 0.5).abs() < f64::EPSILON);
917 }
918
919 #[test]
920 fn emit_usage_metrics_does_not_panic() {
921 let registry = NamespaceRegistry::new();
922 let id = NamespaceId::new("emit-ns").unwrap();
923 registry
924 .create_namespace(id, "test", "admin", NamespaceQuota::default())
925 .unwrap();
926 registry.increment_series("emit-ns", 42).unwrap();
927
928 registry.emit_usage_metrics();
930 }
931
932 #[test]
933 fn usage_ratio_zero_limit() {
934 let r = ratio(100, 0);
936 assert_eq!(r, f64::MAX);
937
938 let r = ratio(0, 0);
940 assert!((r - 0.0).abs() < f64::EPSILON);
941 }
942
943 #[test]
944 fn usage_ratio_over_quota() {
945 let r = ratio(200, 100);
946 assert!((r - 2.0).abs() < f64::EPSILON);
947 }
948
949 #[test]
950 fn open_creates_and_loads_snapshot() {
951 let tmp = tempfile::tempdir().unwrap();
952 let dir = tmp.path().join("namespaces");
953
954 {
956 let reg = NamespaceRegistry::open(&dir).unwrap();
957 let id = NamespaceId::new("test-ns").unwrap();
958 reg.create_namespace(id, "Test", "admin", NamespaceQuota::default())
959 .unwrap();
960 assert_eq!(reg.count(), 2);
961 }
962
963 {
965 let reg = NamespaceRegistry::open(&dir).unwrap();
966 assert_eq!(reg.count(), 2);
967 assert!(reg.exists("test-ns"));
968 assert!(reg.exists(NamespaceId::DEFAULT));
969 }
970 }
971
972 #[test]
973 fn open_delete_persists() {
974 let tmp = tempfile::tempdir().unwrap();
975 let dir = tmp.path().join("namespaces");
976
977 {
978 let reg = NamespaceRegistry::open(&dir).unwrap();
979 let id = NamespaceId::new("ephemeral").unwrap();
980 reg.create_namespace(id, "Ephemeral", "admin", NamespaceQuota::default())
981 .unwrap();
982 reg.delete_namespace("ephemeral").unwrap();
983 }
984
985 {
986 let reg = NamespaceRegistry::open(&dir).unwrap();
987 assert_eq!(reg.count(), 1);
988 assert!(!reg.exists("ephemeral"));
989 }
990 }
991
992 #[test]
993 fn open_quota_update_persists() {
994 let tmp = tempfile::tempdir().unwrap();
995 let dir = tmp.path().join("namespaces");
996
997 {
998 let reg = NamespaceRegistry::open(&dir).unwrap();
999 let id = NamespaceId::new("quotans").unwrap();
1000 reg.create_namespace(id, "Quota Test", "admin", NamespaceQuota::default())
1001 .unwrap();
1002 reg.update_quota(
1003 "quotans",
1004 NamespaceQuota {
1005 max_series_count: 42,
1006 ..NamespaceQuota::default()
1007 },
1008 )
1009 .unwrap();
1010 }
1011
1012 {
1013 let reg = NamespaceRegistry::open(&dir).unwrap();
1014 let state = reg.get("quotans").unwrap();
1015 assert_eq!(state.info.quota.max_series_count, 42);
1016 }
1017 }
1018}