1use std::path::{Path, PathBuf};
10use std::sync::{Arc, Mutex};
11use std::thread::JoinHandle;
12use std::time::SystemTime;
13
14use dashmap::DashMap;
15use serde::{Deserialize, Serialize};
16use tracing::{debug, info, warn};
17
18use chronix_core::{NamespaceId, NamespaceQuota, NamespaceUsage};
19
20use crate::tenant::error::{Result, TenantError};
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct NamespaceInfo {
25 pub id: NamespaceId,
27 pub description: String,
29 pub owner: String,
31 pub quota: NamespaceQuota,
33 pub created_at_ms: u64,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct NamespaceState {
40 pub info: NamespaceInfo,
42 pub usage: NamespaceUsage,
44}
45
46#[derive(Debug)]
57pub struct NamespaceRegistry {
58 namespaces: DashMap<String, NamespaceState>,
60 persist_dir: Option<PathBuf>,
62 pending_snapshot: Mutex<Option<JoinHandle<()>>>,
64}
65
66const SNAPSHOT_FILE: &str = "namespaces.json";
68
69fn write_snapshot(dir: &Path, states: &[NamespaceState]) -> Result<()> {
71 let json = serde_json::to_string_pretty(states)
72 .map_err(|e| TenantError::InvalidConfig(format!("cannot serialize namespaces: {e}")))?;
73
74 let tmp_path = dir.join("namespaces.json.tmp");
75 let final_path = dir.join(SNAPSHOT_FILE);
76 std::fs::write(&tmp_path, &json)
77 .map_err(|e| TenantError::InvalidConfig(format!("cannot write namespace snapshot: {e}")))?;
78
79 let result = (|| -> Result<()> {
82 let f = std::fs::File::open(&tmp_path).map_err(|e| {
83 TenantError::InvalidConfig(format!("cannot open tmp snapshot for fsync: {e}"))
84 })?;
85 f.sync_all().map_err(|e| {
86 TenantError::InvalidConfig(format!("cannot fsync namespace snapshot: {e}"))
87 })?;
88
89 std::fs::rename(&tmp_path, &final_path).map_err(|e| {
90 TenantError::InvalidConfig(format!("cannot rename namespace snapshot: {e}"))
91 })?;
92 Ok(())
93 })();
94
95 if result.is_err() {
96 let _ = std::fs::remove_file(&tmp_path);
97 }
98
99 result?;
100
101 debug!("namespace snapshot saved");
102 Ok(())
103}
104impl NamespaceRegistry {
105 #[must_use]
109 pub fn new() -> Self {
110 let registry = Self {
111 namespaces: DashMap::new(),
112 persist_dir: None,
113 pending_snapshot: Mutex::new(None),
114 };
115
116 registry.ensure_default();
117 registry
118 }
119
120 pub fn open(dir: impl Into<PathBuf>) -> Result<Self> {
131 let dir = dir.into();
132 std::fs::create_dir_all(&dir)
133 .map_err(|e| TenantError::InvalidConfig(format!("cannot create namespace dir: {e}")))?;
134
135 let snapshot_path = dir.join(SNAPSHOT_FILE);
136 let registry = if snapshot_path.exists() {
137 let data = std::fs::read_to_string(&snapshot_path).map_err(|e| {
138 TenantError::InvalidConfig(format!("cannot read namespace snapshot: {e}"))
139 })?;
140 let states: Vec<NamespaceState> = serde_json::from_str(&data).map_err(|e| {
141 TenantError::InvalidConfig(format!("corrupt namespace snapshot: {e}"))
142 })?;
143 let namespaces = DashMap::new();
144 for state in states {
145 namespaces.insert(state.info.id.as_str().to_string(), state);
146 }
147 info!(count = namespaces.len(), "loaded namespace snapshot");
148 Self {
149 namespaces,
150 persist_dir: Some(dir),
151 pending_snapshot: Mutex::new(None),
152 }
153 } else {
154 Self {
155 namespaces: DashMap::new(),
156 persist_dir: Some(dir),
157 pending_snapshot: Mutex::new(None),
158 }
159 };
160
161 registry.ensure_default();
162
163 if !snapshot_path.exists() {
165 registry.save_snapshot_inner()?;
166 }
167
168 Ok(registry)
169 }
170
171 fn ensure_default(&self) {
173 if !self.namespaces.contains_key(NamespaceId::DEFAULT) {
174 let default_info = NamespaceInfo {
175 id: NamespaceId::default_namespace(),
176 description: "Default namespace".to_string(),
177 owner: "system".to_string(),
178 quota: NamespaceQuota::default(),
179 created_at_ms: now_ms(),
180 };
181 self.namespaces.insert(
182 NamespaceId::DEFAULT.to_string(),
183 NamespaceState {
184 info: default_info,
185 usage: NamespaceUsage::default(),
186 },
187 );
188 }
189 }
190
191 fn save_snapshot_inner(&self) -> Result<()> {
195 let Some(dir) = &self.persist_dir else {
196 return Ok(());
197 };
198
199 let states: Vec<NamespaceState> =
200 self.namespaces.iter().map(|r| r.value().clone()).collect();
201
202 write_snapshot(dir, &states)
203 }
204
205 fn save_snapshot_best_effort(&self) {
208 let dir = match &self.persist_dir {
209 Some(d) => d.clone(),
210 None => return,
211 };
212
213 let states: Vec<NamespaceState> =
216 self.namespaces.iter().map(|r| r.value().clone()).collect();
217
218 let Ok(mut guard) = self.pending_snapshot.lock() else {
223 if let Err(e) = write_snapshot(&dir, &states) {
226 warn!("failed to persist namespace snapshot: {e}");
227 }
228 return;
229 };
230 if let Some(prev) = guard.take() {
231 let _ = prev.join();
232 }
233
234 match std::thread::Builder::new()
236 .name("chronix-ns-snapshot".into())
237 .spawn(move || {
238 if let Err(e) = write_snapshot(&dir, &states) {
239 warn!("failed to persist namespace snapshot: {e}");
240 }
241 }) {
242 Ok(handle) => {
243 *guard = Some(handle);
245 }
246 Err(e) => {
247 warn!("failed to spawn namespace snapshot thread, writing synchronously: {e}");
250 drop(guard);
251 if let Err(e) = self.save_snapshot_inner() {
252 warn!("failed to persist namespace snapshot: {e}");
253 }
254 }
255 }
256 }
257
258 pub fn create_namespace(
265 &self,
266 id: NamespaceId,
267 description: impl Into<String>,
268 owner: impl Into<String>,
269 quota: NamespaceQuota,
270 ) -> Result<NamespaceInfo> {
271 let name = id.as_str().to_string();
272
273 use dashmap::mapref::entry::Entry;
276 match self.namespaces.entry(name.clone()) {
277 Entry::Occupied(_) => Err(TenantError::NamespaceAlreadyExists(name)),
278 Entry::Vacant(vacant) => {
279 let info = NamespaceInfo {
280 id,
281 description: description.into(),
282 owner: owner.into(),
283 quota,
284 created_at_ms: now_ms(),
285 };
286
287 let state = NamespaceState {
288 info: info.clone(),
289 usage: NamespaceUsage::default(),
290 };
291
292 vacant.insert(state);
293
294 info!(namespace = %name, "namespace created");
295 #[allow(clippy::cast_precision_loss)]
296 metrics::gauge!("chronix_namespace_count").set(self.namespaces.len() as f64);
297
298 self.save_snapshot_best_effort();
299 Ok(info)
300 }
301 }
302 }
303
304 pub fn delete_namespace(&self, name: &str) -> Result<NamespaceInfo> {
311 if name == NamespaceId::DEFAULT {
312 return Err(TenantError::InvalidConfig(
313 "cannot delete the default namespace".to_string(),
314 ));
315 }
316
317 let (_, state) = self
318 .namespaces
319 .remove(name)
320 .ok_or_else(|| TenantError::NamespaceNotFound(name.to_string()))?;
321
322 info!(namespace = %name, "namespace deleted");
323 #[allow(clippy::cast_precision_loss)]
324 metrics::gauge!("chronix_namespace_count").set(self.namespaces.len() as f64);
325
326 if let Ok(mut guard) = self.pending_snapshot.lock() {
329 if let Some(handle) = guard.take() {
330 let _ = handle.join();
331 }
332 }
333
334 self.save_snapshot_inner()?;
337 Ok(state.info)
338 }
339
340 pub fn get(&self, name: &str) -> Result<NamespaceState> {
346 self.namespaces
347 .get(name)
348 .map(|r| r.value().clone())
349 .ok_or_else(|| TenantError::NamespaceNotFound(name.to_string()))
350 }
351
352 #[must_use]
354 pub fn exists(&self, name: &str) -> bool {
355 self.namespaces.contains_key(name)
356 }
357
358 #[must_use]
360 pub fn list_names(&self) -> Vec<String> {
361 self.namespaces.iter().map(|r| r.key().clone()).collect()
362 }
363
364 #[must_use]
366 pub fn list_all(&self) -> Vec<NamespaceState> {
367 self.namespaces.iter().map(|r| r.value().clone()).collect()
368 }
369
370 #[must_use]
372 pub fn count(&self) -> usize {
373 self.namespaces.len()
374 }
375
376 pub fn update_quota(&self, name: &str, quota: NamespaceQuota) -> Result<()> {
382 quota.validate().map_err(TenantError::InvalidConfig)?;
384 {
385 let mut entry = self
386 .namespaces
387 .get_mut(name)
388 .ok_or_else(|| TenantError::NamespaceNotFound(name.to_string()))?;
389 entry.value_mut().info.quota = quota;
390 }
391 debug!(namespace = %name, "quota updated");
392 self.save_snapshot_best_effort();
393 Ok(())
394 }
395
396 #[must_use]
400 pub fn get_usage(&self, name: &str) -> Option<NamespaceUsage> {
401 self.namespaces.get(name).map(|r| r.value().usage.clone())
402 }
403
404 pub fn update_usage(&self, name: &str, usage: NamespaceUsage) -> Result<()> {
410 let mut entry = self
411 .namespaces
412 .get_mut(name)
413 .ok_or_else(|| TenantError::NamespaceNotFound(name.to_string()))?;
414
415 entry.value_mut().usage = usage;
416 Ok(())
417 }
418
419 pub fn increment_series(&self, name: &str, count: u64) -> Result<()> {
427 let mut entry = self
428 .namespaces
429 .get_mut(name)
430 .ok_or_else(|| TenantError::NamespaceNotFound(name.to_string()))?;
431
432 entry.value_mut().usage.series_count =
433 entry.value().usage.series_count.saturating_add(count);
434 Ok(())
435 }
436
437 pub fn increment_storage(&self, name: &str, bytes: u64) -> Result<()> {
445 let mut entry = self
446 .namespaces
447 .get_mut(name)
448 .ok_or_else(|| TenantError::NamespaceNotFound(name.to_string()))?;
449
450 entry.value_mut().usage.storage_bytes =
451 entry.value().usage.storage_bytes.saturating_add(bytes);
452 Ok(())
453 }
454
455 pub fn increment_measurements(&self, name: &str, count: u32) -> Result<()> {
463 let mut entry = self
464 .namespaces
465 .get_mut(name)
466 .ok_or_else(|| TenantError::NamespaceNotFound(name.to_string()))?;
467
468 entry.value_mut().usage.measurements =
469 entry.value().usage.measurements.saturating_add(count);
470 Ok(())
471 }
472
473 pub fn check_and_increment_write(
488 &self,
489 name: &str,
490 new_series: u64,
491 new_points: u64,
492 ) -> Result<()> {
493 let mut entry = self
494 .namespaces
495 .get_mut(name)
496 .ok_or_else(|| TenantError::NamespaceNotFound(name.to_string()))?;
497
498 let max_series = entry.value().info.quota.max_series_count;
501 let max_rate = entry.value().info.quota.max_ingestion_rate;
502 let max_storage = entry.value().info.quota.max_storage_bytes;
503 let current_series = entry.value().usage.series_count;
504 let current_storage = entry.value().usage.storage_bytes;
505
506 let projected_series = current_series.saturating_add(new_series);
508 if projected_series > max_series {
509 return Err(TenantError::QuotaExceeded {
510 namespace: name.to_string(),
511 resource: "series_count".to_string(),
512 current: current_series,
513 limit: max_series,
514 });
515 }
516
517 let now_s = now_ms() / 1000;
520 let last_window = entry.value().usage.ingestion_rate_window_s;
521 let current_rate = if last_window == now_s {
522 entry.value().usage.ingestion_rate
523 } else {
524 0.0
526 };
527
528 #[allow(clippy::cast_precision_loss)]
529 let projected_rate = current_rate + new_points as f64;
530 #[allow(clippy::cast_precision_loss)]
531 let rate_limit = max_rate as f64;
532 if projected_rate > rate_limit {
533 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
534 let current = current_rate as u64;
535 return Err(TenantError::QuotaExceeded {
536 namespace: name.to_string(),
537 resource: "ingestion_rate".to_string(),
538 current,
539 limit: max_rate,
540 });
541 }
542
543 if current_storage > max_storage {
545 return Err(TenantError::QuotaExceeded {
546 namespace: name.to_string(),
547 resource: "storage_bytes".to_string(),
548 current: current_storage,
549 limit: max_storage,
550 });
551 }
552
553 let state_mut = entry.value_mut();
556 state_mut.usage.series_count = projected_series;
557 state_mut.usage.ingestion_rate = projected_rate;
558 state_mut.usage.ingestion_rate_window_s = now_s;
559
560 Ok(())
561 }
562
563 pub fn check_and_increment_measurement(&self, name: &str) -> Result<()> {
573 let mut entry = self
574 .namespaces
575 .get_mut(name)
576 .ok_or_else(|| TenantError::NamespaceNotFound(name.to_string()))?;
577
578 let state = entry.value();
579 let projected = u64::from(state.usage.measurements) + 1;
580 let limit = u64::from(state.info.quota.max_measurements);
581
582 if projected > limit {
583 return Err(TenantError::QuotaExceeded {
584 namespace: name.to_string(),
585 resource: "measurements".to_string(),
586 current: u64::from(state.usage.measurements),
587 limit,
588 });
589 }
590
591 entry.value_mut().usage.measurements = state.usage.measurements.saturating_add(1);
592 Ok(())
593 }
594
595 #[must_use]
601 pub fn usage_ratios(&self) -> Vec<UsageRatio> {
602 let mut ratios = Vec::new();
603
604 for entry in &self.namespaces {
605 let ns = entry.key();
606 let state = entry.value();
607 let q = &state.info.quota;
608 let u = &state.usage;
609
610 ratios.push(UsageRatio {
611 namespace: ns.clone(),
612 resource: "series_count".to_string(),
613 current: u.series_count,
614 limit: q.max_series_count,
615 ratio: ratio(u.series_count, q.max_series_count),
616 });
617 ratios.push(UsageRatio {
618 namespace: ns.clone(),
619 resource: "storage_bytes".to_string(),
620 current: u.storage_bytes,
621 limit: q.max_storage_bytes,
622 ratio: ratio(u.storage_bytes, q.max_storage_bytes),
623 });
624 ratios.push(UsageRatio {
625 namespace: ns.clone(),
626 resource: "measurements".to_string(),
627 current: u64::from(u.measurements),
628 limit: u64::from(q.max_measurements),
629 ratio: ratio(u64::from(u.measurements), u64::from(q.max_measurements)),
630 });
631 }
632
633 ratios
634 }
635
636 pub fn emit_usage_metrics(&self) {
642 for r in self.usage_ratios() {
643 metrics::gauge!(
644 "chronix_namespace_usage_ratio",
645 "namespace" => r.namespace,
646 "resource" => r.resource,
647 )
648 .set(r.ratio);
649 }
650 }
651}
652
653#[derive(Debug, Clone, Serialize, Deserialize)]
655pub struct UsageRatio {
656 pub namespace: String,
658 pub resource: String,
660 pub current: u64,
662 pub limit: u64,
664 pub ratio: f64,
666}
667
668#[allow(clippy::cast_precision_loss)]
674fn ratio(current: u64, limit: u64) -> f64 {
675 if limit == 0 {
676 return if current > 0 { f64::MAX } else { 0.0 };
677 }
678 current as f64 / limit as f64
679}
680
681impl Default for NamespaceRegistry {
682 fn default() -> Self {
683 Self::new()
684 }
685}
686
687impl Drop for NamespaceRegistry {
688 fn drop(&mut self) {
689 if let Ok(mut guard) = self.pending_snapshot.lock() {
692 if let Some(handle) = guard.take() {
693 let _ = handle.join();
694 }
695 }
696 }
697}
698
699fn now_ms() -> u64 {
700 #[allow(clippy::cast_possible_truncation)]
701 SystemTime::now()
702 .duration_since(std::time::UNIX_EPOCH)
703 .map(|d| d.as_millis() as u64)
704 .unwrap_or(0)
705}
706
707#[must_use]
709pub fn shared_registry() -> Arc<NamespaceRegistry> {
710 Arc::new(NamespaceRegistry::new())
711}
712
713#[cfg(test)]
714mod tests {
715 use super::*;
716
717 #[test]
718 fn default_namespace_exists_on_creation() {
719 let registry = NamespaceRegistry::new();
720 assert!(registry.exists(NamespaceId::DEFAULT));
721 assert_eq!(registry.count(), 1);
722 }
723
724 #[test]
725 fn create_and_get_namespace() {
726 let registry = NamespaceRegistry::new();
727 let id = NamespaceId::new("team-alpha").unwrap();
728 let info = registry
729 .create_namespace(id, "Team Alpha", "admin", NamespaceQuota::default())
730 .unwrap();
731
732 assert_eq!(info.id.as_str(), "team-alpha");
733 assert_eq!(info.owner, "admin");
734 assert_eq!(registry.count(), 2);
735
736 let state = registry.get("team-alpha").unwrap();
737 assert_eq!(state.info.description, "Team Alpha");
738 }
739
740 #[test]
741 fn create_duplicate_fails() {
742 let registry = NamespaceRegistry::new();
743 let id = NamespaceId::new("my-ns").unwrap();
744 registry
745 .create_namespace(id, "first", "admin", NamespaceQuota::default())
746 .unwrap();
747
748 let id2 = NamespaceId::new("my-ns").unwrap();
749 let err = registry
750 .create_namespace(id2, "second", "admin", NamespaceQuota::default())
751 .unwrap_err();
752
753 assert!(matches!(err, TenantError::NamespaceAlreadyExists(_)));
754 }
755
756 #[test]
757 fn delete_namespace() {
758 let registry = NamespaceRegistry::new();
759 let id = NamespaceId::new("temp-ns").unwrap();
760 registry
761 .create_namespace(id, "temp", "admin", NamespaceQuota::default())
762 .unwrap();
763
764 assert_eq!(registry.count(), 2);
765 registry.delete_namespace("temp-ns").unwrap();
766 assert_eq!(registry.count(), 1);
767 assert!(!registry.exists("temp-ns"));
768 }
769
770 #[test]
771 fn cannot_delete_default_namespace() {
772 let registry = NamespaceRegistry::new();
773 let err = registry.delete_namespace("default").unwrap_err();
774 assert!(matches!(err, TenantError::InvalidConfig(_)));
775 }
776
777 #[test]
778 fn delete_nonexistent_fails() {
779 let registry = NamespaceRegistry::new();
780 let err = registry.delete_namespace("nope").unwrap_err();
781 assert!(matches!(err, TenantError::NamespaceNotFound(_)));
782 }
783
784 #[test]
785 fn list_namespaces() {
786 let registry = NamespaceRegistry::new();
787 let id = NamespaceId::new("ns-a").unwrap();
788 registry
789 .create_namespace(id, "A", "admin", NamespaceQuota::default())
790 .unwrap();
791 let id = NamespaceId::new("ns-b").unwrap();
792 registry
793 .create_namespace(id, "B", "admin", NamespaceQuota::default())
794 .unwrap();
795
796 let mut names = registry.list_names();
797 names.sort();
798 assert_eq!(names, vec!["default", "ns-a", "ns-b"]);
799 }
800
801 #[test]
802 fn update_quota() {
803 let registry = NamespaceRegistry::new();
804 let id = NamespaceId::new("quota-ns").unwrap();
805 registry
806 .create_namespace(id, "quota test", "admin", NamespaceQuota::default())
807 .unwrap();
808
809 let new_quota = NamespaceQuota {
810 max_series_count: 500,
811 max_ingestion_rate: 1000,
812 max_storage_bytes: 1024,
813 max_measurements: 10,
814 max_request_rps: 0,
815 max_request_burst: 0,
816 };
817
818 registry
819 .update_quota("quota-ns", new_quota.clone())
820 .unwrap();
821 let state = registry.get("quota-ns").unwrap();
822 assert_eq!(state.info.quota, new_quota);
823 }
824
825 #[test]
826 fn usage_tracking() {
827 let registry = NamespaceRegistry::new();
828 let id = NamespaceId::new("usage-ns").unwrap();
829 registry
830 .create_namespace(id, "usage test", "admin", NamespaceQuota::default())
831 .unwrap();
832
833 registry.increment_series("usage-ns", 100).unwrap();
834 registry.increment_storage("usage-ns", 4096).unwrap();
835 registry.increment_measurements("usage-ns", 5).unwrap();
836
837 let usage = registry.get_usage("usage-ns").unwrap();
838 assert_eq!(usage.series_count, 100);
839 assert_eq!(usage.storage_bytes, 4096);
840 assert_eq!(usage.measurements, 5);
841 }
842
843 #[test]
844 fn shared_registry_is_arc() {
845 let reg = shared_registry();
846 let reg2 = reg.clone();
847 let id = NamespaceId::new("shared-test").unwrap();
848 reg.create_namespace(id, "test", "admin", NamespaceQuota::default())
849 .unwrap();
850 assert!(reg2.exists("shared-test"));
851 }
852
853 #[test]
854 fn namespace_id_validation() {
855 assert!(NamespaceId::new("valid-name").is_ok());
856 assert!(NamespaceId::new("a123").is_ok());
857 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()); }
865
866 #[test]
869 fn usage_ratios_empty_registry() {
870 let registry = NamespaceRegistry::new();
871 let ratios = registry.usage_ratios();
872 assert_eq!(ratios.len(), 3);
874 for r in &ratios {
875 assert_eq!(r.namespace, "default");
876 assert!((r.ratio - 0.0).abs() < f64::EPSILON);
877 }
878 }
879
880 #[test]
881 fn usage_ratios_with_usage() {
882 let registry = NamespaceRegistry::new();
883 let id = NamespaceId::new("metrics-ns").unwrap();
884 let quota = NamespaceQuota {
885 max_series_count: 100,
886 max_ingestion_rate: 1000,
887 max_storage_bytes: 10_000,
888 max_measurements: 20,
889 max_request_rps: 0,
890 max_request_burst: 0,
891 };
892 registry
893 .create_namespace(id, "test", "admin", quota)
894 .unwrap();
895
896 registry.increment_series("metrics-ns", 50).unwrap();
897 registry.increment_storage("metrics-ns", 7500).unwrap();
898 registry.increment_measurements("metrics-ns", 10).unwrap();
899
900 let ratios = registry.usage_ratios();
901 let ns_ratios: Vec<_> = ratios
902 .iter()
903 .filter(|r| r.namespace == "metrics-ns")
904 .collect();
905 assert_eq!(ns_ratios.len(), 3);
906
907 let series = ns_ratios
908 .iter()
909 .find(|r| r.resource == "series_count")
910 .unwrap();
911 assert!((series.ratio - 0.5).abs() < f64::EPSILON);
912 assert_eq!(series.current, 50);
913 assert_eq!(series.limit, 100);
914
915 let storage = ns_ratios
916 .iter()
917 .find(|r| r.resource == "storage_bytes")
918 .unwrap();
919 assert!((storage.ratio - 0.75).abs() < f64::EPSILON);
920
921 let measurements = ns_ratios
922 .iter()
923 .find(|r| r.resource == "measurements")
924 .unwrap();
925 assert!((measurements.ratio - 0.5).abs() < f64::EPSILON);
926 }
927
928 #[test]
929 fn emit_usage_metrics_does_not_panic() {
930 let registry = NamespaceRegistry::new();
931 let id = NamespaceId::new("emit-ns").unwrap();
932 registry
933 .create_namespace(id, "test", "admin", NamespaceQuota::default())
934 .unwrap();
935 registry.increment_series("emit-ns", 42).unwrap();
936
937 registry.emit_usage_metrics();
939 }
940
941 #[test]
942 fn usage_ratio_zero_limit() {
943 let r = ratio(100, 0);
945 assert_eq!(r, f64::MAX);
946
947 let r = ratio(0, 0);
949 assert!((r - 0.0).abs() < f64::EPSILON);
950 }
951
952 #[test]
953 fn usage_ratio_over_quota() {
954 let r = ratio(200, 100);
955 assert!((r - 2.0).abs() < f64::EPSILON);
956 }
957
958 #[test]
959 fn open_creates_and_loads_snapshot() {
960 let tmp = tempfile::tempdir().unwrap();
961 let dir = tmp.path().join("namespaces");
962
963 {
965 let reg = NamespaceRegistry::open(&dir).unwrap();
966 let id = NamespaceId::new("test-ns").unwrap();
967 reg.create_namespace(id, "Test", "admin", NamespaceQuota::default())
968 .unwrap();
969 assert_eq!(reg.count(), 2);
970 }
971
972 {
974 let reg = NamespaceRegistry::open(&dir).unwrap();
975 assert_eq!(reg.count(), 2);
976 assert!(reg.exists("test-ns"));
977 assert!(reg.exists(NamespaceId::DEFAULT));
978 }
979 }
980
981 #[test]
982 fn open_delete_persists() {
983 let tmp = tempfile::tempdir().unwrap();
984 let dir = tmp.path().join("namespaces");
985
986 {
987 let reg = NamespaceRegistry::open(&dir).unwrap();
988 let id = NamespaceId::new("ephemeral").unwrap();
989 reg.create_namespace(id, "Ephemeral", "admin", NamespaceQuota::default())
990 .unwrap();
991 reg.delete_namespace("ephemeral").unwrap();
992 }
993
994 {
995 let reg = NamespaceRegistry::open(&dir).unwrap();
996 assert_eq!(reg.count(), 1);
997 assert!(!reg.exists("ephemeral"));
998 }
999 }
1000
1001 #[test]
1002 fn open_quota_update_persists() {
1003 let tmp = tempfile::tempdir().unwrap();
1004 let dir = tmp.path().join("namespaces");
1005
1006 {
1007 let reg = NamespaceRegistry::open(&dir).unwrap();
1008 let id = NamespaceId::new("quotans").unwrap();
1009 reg.create_namespace(id, "Quota Test", "admin", NamespaceQuota::default())
1010 .unwrap();
1011 reg.update_quota(
1012 "quotans",
1013 NamespaceQuota {
1014 max_series_count: 42,
1015 ..NamespaceQuota::default()
1016 },
1017 )
1018 .unwrap();
1019 }
1020
1021 {
1022 let reg = NamespaceRegistry::open(&dir).unwrap();
1023 let state = reg.get("quotans").unwrap();
1024 assert_eq!(state.info.quota.max_series_count, 42);
1025 }
1026 }
1027}