1use std::collections::BTreeMap;
22use std::fmt;
23use std::str::FromStr;
24
25use mongreldb_types::ids::{NodeId, QueryId};
26use serde::{Deserialize, Serialize};
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
34pub enum WorkloadClass {
35 Control,
37 Replication,
39 Oltp,
41 InteractiveSql,
43 AiRetrieval,
45 Analytics,
47 Maintenance,
49 Backup,
51}
52
53impl WorkloadClass {
54 pub const ALL: [WorkloadClass; 8] = [
56 WorkloadClass::Control,
57 WorkloadClass::Replication,
58 WorkloadClass::Oltp,
59 WorkloadClass::InteractiveSql,
60 WorkloadClass::AiRetrieval,
61 WorkloadClass::Analytics,
62 WorkloadClass::Maintenance,
63 WorkloadClass::Backup,
64 ];
65
66 pub fn name(self) -> &'static str {
68 match self {
69 WorkloadClass::Control => "control",
70 WorkloadClass::Replication => "replication",
71 WorkloadClass::Oltp => "oltp",
72 WorkloadClass::InteractiveSql => "interactive_sql",
73 WorkloadClass::AiRetrieval => "ai_retrieval",
74 WorkloadClass::Analytics => "analytics",
75 WorkloadClass::Maintenance => "maintenance",
76 WorkloadClass::Backup => "backup",
77 }
78 }
79
80 pub fn has_reserved_capacity(self) -> bool {
83 matches!(self, WorkloadClass::Control | WorkloadClass::Replication)
84 }
85
86 pub fn is_low_priority(self) -> bool {
90 matches!(
91 self,
92 WorkloadClass::Analytics | WorkloadClass::Maintenance | WorkloadClass::Backup
93 )
94 }
95}
96
97impl fmt::Display for WorkloadClass {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 f.write_str(self.name())
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
105pub enum ResourceError {
106 #[error("resource group name must not be empty")]
108 EmptyName,
109 #[error("resource group {name:?}: cpu_weight must be nonzero (§4.9 bounded CPU/work)")]
111 ZeroCpuWeight {
112 name: String,
114 },
115 #[error("resource group {name:?}: work_units must be nonzero (§4.9 bounded CPU/work)")]
117 ZeroWorkUnits {
118 name: String,
120 },
121 #[error(
124 "resource group {name:?} is pinned: control and replication capacity is reserved (§13.1)"
125 )]
126 Pinned {
127 name: String,
129 },
130 #[error("resource group {name:?} not found")]
132 NotFound {
133 name: String,
135 },
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
145pub struct ResourceGroup {
146 pub name: String,
148 pub max_concurrency: usize,
150 pub max_queue: usize,
152 pub memory_bytes: u64,
154 pub temporary_disk_bytes: u64,
156 pub work_units: u64,
158 pub cpu_weight: u32,
160 pub priority: u8,
162 pub max_result_bytes: u64,
164}
165
166impl ResourceGroup {
167 pub fn validate(&self) -> Result<(), ResourceError> {
170 if self.name.is_empty() {
171 return Err(ResourceError::EmptyName);
172 }
173 if self.cpu_weight == 0 {
174 return Err(ResourceError::ZeroCpuWeight {
175 name: self.name.clone(),
176 });
177 }
178 if self.work_units == 0 {
179 return Err(ResourceError::ZeroWorkUnits {
180 name: self.name.clone(),
181 });
182 }
183 Ok(())
184 }
185
186 pub fn for_class(class: WorkloadClass) -> Self {
192 let (name, max_concurrency, max_queue, memory_mib, work_units, cpu_weight, priority) =
193 match class {
194 WorkloadClass::Control => {
195 ("control", 8usize, 64usize, 256u64, 1 << 20, 256u32, 255u8)
196 }
197 WorkloadClass::Replication => ("replication", 8, 64, 512, 1 << 20, 256, 254),
198 WorkloadClass::Oltp => ("oltp", 64, 256, 1024, 1 << 20, 128, 200),
199 WorkloadClass::InteractiveSql => {
200 ("interactive_sql", 16, 64, 1024, 1 << 19, 64, 180)
201 }
202 WorkloadClass::AiRetrieval => ("ai_retrieval", 8, 32, 1024, 1 << 18, 32, 150),
203 WorkloadClass::Analytics => ("analytics", 4, 16, 2048, 1 << 17, 16, 100),
204 WorkloadClass::Maintenance => ("maintenance", 2, 8, 512, 1 << 16, 8, 50),
205 WorkloadClass::Backup => ("backup", 1, 4, 256, 1 << 16, 4, 40),
206 };
207 Self {
208 name: name.to_string(),
209 max_concurrency,
210 max_queue,
211 memory_bytes: memory_mib * 1024 * 1024,
212 temporary_disk_bytes: 1024 * 1024 * 1024,
213 work_units,
214 cpu_weight,
215 priority,
216 max_result_bytes: 1024 * 1024 * 1024,
217 }
218 }
219}
220
221#[repr(transparent)]
227#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
228pub struct TenantId(pub u64);
229
230impl TenantId {
231 pub const ZERO: Self = Self(0);
233
234 pub const fn new(value: u64) -> Self {
236 Self(value)
237 }
238
239 pub const fn get(self) -> u64 {
241 self.0
242 }
243}
244
245impl fmt::Display for TenantId {
246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247 write!(f, "{}", self.0)
248 }
249}
250
251impl fmt::Debug for TenantId {
252 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253 write!(f, "TenantId({})", self.0)
254 }
255}
256
257impl FromStr for TenantId {
258 type Err = std::num::ParseIntError;
259
260 fn from_str(text: &str) -> Result<Self, Self::Err> {
261 text.parse::<u64>().map(Self)
262 }
263}
264
265#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
267pub struct NodeHandle {
268 id: NodeId,
269}
270
271impl NodeHandle {
272 pub fn new(id: NodeId) -> Self {
274 Self { id }
275 }
276
277 pub fn id(self) -> NodeId {
279 self.id
280 }
281
282 pub fn tenant(self, tenant: TenantId) -> TenantHandle {
284 TenantHandle {
285 node: self,
286 id: tenant,
287 }
288 }
289}
290
291#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
293pub struct TenantHandle {
294 node: NodeHandle,
295 id: TenantId,
296}
297
298impl TenantHandle {
299 pub fn node(self) -> NodeHandle {
301 self.node
302 }
303
304 pub fn id(self) -> TenantId {
306 self.id
307 }
308
309 pub fn group(self, name: impl Into<String>) -> ResourceGroupHandle {
311 ResourceGroupHandle {
312 tenant: self,
313 name: name.into(),
314 }
315 }
316}
317
318#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
320pub struct ResourceGroupHandle {
321 tenant: TenantHandle,
322 name: String,
323}
324
325impl ResourceGroupHandle {
326 pub fn tenant(&self) -> TenantHandle {
328 self.tenant
329 }
330
331 pub fn node(&self) -> NodeHandle {
333 self.tenant.node()
334 }
335
336 pub fn name(&self) -> &str {
338 &self.name
339 }
340
341 pub fn query(&self, id: QueryId, class: WorkloadClass) -> QueryHandle {
343 QueryHandle {
344 group: self.clone(),
345 id,
346 class,
347 }
348 }
349}
350
351#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
353pub struct QueryHandle {
354 group: ResourceGroupHandle,
355 id: QueryId,
356 class: WorkloadClass,
357}
358
359impl QueryHandle {
360 pub fn group(&self) -> &ResourceGroupHandle {
362 &self.group
363 }
364
365 pub fn id(&self) -> QueryId {
367 self.id
368 }
369
370 pub fn class(&self) -> WorkloadClass {
372 self.class
373 }
374
375 pub fn tenant(&self) -> TenantHandle {
377 self.group.tenant()
378 }
379
380 pub fn node(&self) -> NodeHandle {
382 self.group.node()
383 }
384}
385
386pub struct ResourceGroupRegistry {
400 groups: parking_lot::RwLock<BTreeMap<String, ResourceGroup>>,
401}
402
403const PINNED_GROUP_NAMES: [&str; 2] = ["control", "replication"];
405
406impl ResourceGroupRegistry {
407 pub fn new() -> Self {
410 Self {
411 groups: parking_lot::RwLock::new(BTreeMap::new()),
412 }
413 }
414
415 pub fn with_defaults() -> Self {
418 let registry = Self::new();
419 for class in WorkloadClass::ALL {
420 let group = ResourceGroup::for_class(class);
421 registry.groups.write().insert(group.name.clone(), group);
422 }
423 registry
424 }
425
426 pub fn register(&self, group: ResourceGroup) -> Result<(), ResourceError> {
430 group.validate()?;
431 if PINNED_GROUP_NAMES.contains(&group.name.as_str())
432 && (group.max_concurrency == 0 || group.memory_bytes == 0)
433 {
434 return Err(ResourceError::Pinned {
435 name: group.name.clone(),
436 });
437 }
438 self.groups.write().insert(group.name.clone(), group);
439 Ok(())
440 }
441
442 pub fn get(&self, name: &str) -> Option<ResourceGroup> {
444 self.groups.read().get(name).cloned()
445 }
446
447 pub fn lookup(&self, name: &str) -> Result<ResourceGroup, ResourceError> {
449 self.get(name).ok_or_else(|| ResourceError::NotFound {
450 name: name.to_string(),
451 })
452 }
453
454 pub fn remove(&self, name: &str) -> Result<bool, ResourceError> {
458 if PINNED_GROUP_NAMES.contains(&name) {
459 return Err(ResourceError::Pinned {
460 name: name.to_string(),
461 });
462 }
463 Ok(self.groups.write().remove(name).is_some())
464 }
465
466 pub fn names(&self) -> Vec<String> {
468 self.groups.read().keys().cloned().collect()
469 }
470
471 pub fn len(&self) -> usize {
473 self.groups.read().len()
474 }
475
476 pub fn is_empty(&self) -> bool {
478 self.len() == 0
479 }
480}
481
482impl Default for ResourceGroupRegistry {
483 fn default() -> Self {
484 Self::with_defaults()
485 }
486}
487
488impl fmt::Debug for ResourceGroupRegistry {
489 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
490 f.debug_struct("ResourceGroupRegistry")
491 .field("groups", &*self.groups.read())
492 .finish()
493 }
494}
495
496impl Serialize for ResourceGroupRegistry {
497 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
499 where
500 S: serde::Serializer,
501 {
502 self.groups.read().serialize(serializer)
503 }
504}
505
506impl<'de> Deserialize<'de> for ResourceGroupRegistry {
507 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
511 where
512 D: serde::Deserializer<'de>,
513 {
514 let groups = BTreeMap::<String, ResourceGroup>::deserialize(deserializer)?;
515 let registry = Self::new();
516 for (_, group) in groups {
517 registry.register(group).map_err(serde::de::Error::custom)?;
518 }
519 for pinned in PINNED_GROUP_NAMES {
520 if registry.get(pinned).is_none() {
521 return Err(serde::de::Error::custom(format!(
522 "pinned resource group {pinned:?} missing (§13.1 reserved capacity)"
523 )));
524 }
525 }
526 Ok(registry)
527 }
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533
534 fn valid_group(name: &str) -> ResourceGroup {
535 ResourceGroup {
536 name: name.to_string(),
537 max_concurrency: 4,
538 max_queue: 16,
539 memory_bytes: 1 << 20,
540 temporary_disk_bytes: 1 << 20,
541 work_units: 100,
542 cpu_weight: 1,
543 priority: 128,
544 max_result_bytes: 1 << 20,
545 }
546 }
547
548 #[test]
549 fn workload_class_set_matches_spec() {
550 assert_eq!(WorkloadClass::ALL.len(), 8);
552 let names: Vec<_> = WorkloadClass::ALL.iter().map(|c| c.name()).collect();
553 assert_eq!(
554 names,
555 vec![
556 "control",
557 "replication",
558 "oltp",
559 "interactive_sql",
560 "ai_retrieval",
561 "analytics",
562 "maintenance",
563 "backup"
564 ]
565 );
566 let mut deduped = names.clone();
568 deduped.sort_unstable();
569 deduped.dedup();
570 assert_eq!(deduped.len(), 8);
571 }
572
573 #[test]
574 fn control_and_replication_are_the_reserved_classes() {
575 for class in WorkloadClass::ALL {
576 assert_eq!(
577 class.has_reserved_capacity(),
578 matches!(class, WorkloadClass::Control | WorkloadClass::Replication),
579 "reserved capacity: {class}"
580 );
581 }
582 }
583
584 #[test]
585 fn low_priority_classes_are_deferrable() {
586 for class in WorkloadClass::ALL {
587 assert_eq!(
588 class.is_low_priority(),
589 matches!(
590 class,
591 WorkloadClass::Analytics | WorkloadClass::Maintenance | WorkloadClass::Backup
592 ),
593 "low priority: {class}"
594 );
595 }
596 }
597
598 #[test]
599 fn workload_class_serde_round_trip() {
600 for class in WorkloadClass::ALL {
601 let json = serde_json::to_string(&class).unwrap();
602 let back: WorkloadClass = serde_json::from_str(&json).unwrap();
603 assert_eq!(back, class);
604 }
605 assert_eq!(
606 serde_json::to_string(&WorkloadClass::AiRetrieval).unwrap(),
607 "\"AiRetrieval\""
608 );
609 }
610
611 #[test]
612 fn validation_accepts_a_valid_group() {
613 assert!(valid_group("g").validate().is_ok());
614 for priority in [0u8, 255] {
616 let mut g = valid_group("g");
617 g.priority = priority;
618 assert!(g.validate().is_ok());
619 }
620 }
621
622 #[test]
623 fn validation_rejects_empty_name() {
624 let mut g = valid_group("");
625 g.name.clear();
626 assert_eq!(g.validate(), Err(ResourceError::EmptyName));
627 }
628
629 #[test]
630 fn validation_rejects_zero_weights() {
631 let mut g = valid_group("g");
632 g.cpu_weight = 0;
633 assert_eq!(
634 g.validate(),
635 Err(ResourceError::ZeroCpuWeight { name: "g".into() })
636 );
637 let mut g = valid_group("g");
638 g.work_units = 0;
639 assert_eq!(
640 g.validate(),
641 Err(ResourceError::ZeroWorkUnits { name: "g".into() })
642 );
643 }
644
645 #[test]
646 fn resource_group_serde_round_trip_and_priority_range() {
647 let g = valid_group("oltp");
648 let json = serde_json::to_string(&g).unwrap();
649 let back: ResourceGroup = serde_json::from_str(&json).unwrap();
650 assert_eq!(back, g);
651 let too_big = json.replace("\"priority\":128", "\"priority\":256");
654 assert!(serde_json::from_str::<ResourceGroup>(&too_big).is_err());
655 }
656
657 #[test]
658 fn registry_defaults_cover_every_class() {
659 let registry = ResourceGroupRegistry::with_defaults();
660 assert_eq!(registry.len(), WorkloadClass::ALL.len());
661 for class in WorkloadClass::ALL {
662 let group = registry
663 .get(ResourceGroup::for_class(class).name.as_str())
664 .unwrap_or_else(|| panic!("default group for {class}"));
665 assert!(group.validate().is_ok());
666 assert!(group.max_concurrency > 0);
667 assert!(group.memory_bytes > 0);
668 }
669 assert!(registry.lookup("no-such-group").is_err());
670 }
671
672 #[test]
673 fn control_and_replication_defaults_have_top_priority() {
674 let registry = ResourceGroupRegistry::with_defaults();
675 let control = registry.lookup("control").unwrap();
676 let replication = registry.lookup("replication").unwrap();
677 assert_eq!(control.priority, 255);
678 assert_eq!(replication.priority, 254);
679 for name in registry.names() {
680 if name != "control" && name != "replication" {
681 let g = registry.lookup(&name).unwrap();
682 assert!(
683 g.priority < replication.priority,
684 "{name} outranks replication"
685 );
686 }
687 }
688 }
689
690 #[test]
691 fn pinned_groups_cannot_be_removed_or_starved() {
692 let registry = ResourceGroupRegistry::with_defaults();
693 for pinned in ["control", "replication"] {
694 assert_eq!(
695 registry.remove(pinned),
696 Err(ResourceError::Pinned {
697 name: pinned.into()
698 })
699 );
700 let mut starved = registry.lookup(pinned).unwrap();
702 starved.max_concurrency = 0;
703 assert_eq!(
704 registry.register(starved),
705 Err(ResourceError::Pinned {
706 name: pinned.into()
707 })
708 );
709 let mut starved = registry.lookup(pinned).unwrap();
710 starved.memory_bytes = 0;
711 assert_eq!(
712 registry.register(starved),
713 Err(ResourceError::Pinned {
714 name: pinned.into()
715 })
716 );
717 let mut kept = registry.lookup(pinned).unwrap();
719 kept.max_concurrency += 1;
720 assert!(registry.register(kept.clone()).is_ok());
721 assert_eq!(registry.lookup(pinned).unwrap(), kept);
722 }
723 registry.register(valid_group("scratch")).unwrap();
725 assert_eq!(registry.remove("scratch"), Ok(true));
726 assert_eq!(registry.remove("scratch"), Ok(false));
727 }
728
729 #[test]
730 fn register_validates_groups() {
731 let registry = ResourceGroupRegistry::new();
732 let mut bad = valid_group("bad");
733 bad.cpu_weight = 0;
734 assert!(matches!(
735 registry.register(bad),
736 Err(ResourceError::ZeroCpuWeight { .. })
737 ));
738 assert!(registry.get("bad").is_none());
739 }
740
741 #[test]
742 fn registry_serde_round_trip_preserves_groups() {
743 let registry = ResourceGroupRegistry::with_defaults();
744 registry.register(valid_group("tenant_a")).unwrap();
745 let json = serde_json::to_string(®istry).unwrap();
746 let back: ResourceGroupRegistry = serde_json::from_str(&json).unwrap();
747 assert_eq!(back.names(), registry.names());
748 assert_eq!(back.lookup("tenant_a").unwrap(), valid_group("tenant_a"));
749 assert_eq!(serde_json::to_string(&back).unwrap(), json);
751 }
752
753 #[test]
754 fn registry_deserialization_re_enforces_pinned_defaults() {
755 let bare = serde_json::json!({ "oltp": valid_group("oltp") });
757 assert!(serde_json::from_value::<ResourceGroupRegistry>(bare).is_err());
758 let mut starved = valid_group("control");
760 starved.memory_bytes = 0;
761 let doc =
762 serde_json::json!({ "control": starved, "replication": valid_group("replication") });
763 assert!(serde_json::from_value::<ResourceGroupRegistry>(doc).is_err());
764 }
765
766 #[test]
767 fn hierarchy_handles_walk_up_the_chain() {
768 let node = NodeHandle::new(NodeId::new_random());
769 let tenant = node.tenant(TenantId::new(7));
770 let group = tenant.group("oltp");
771 let query_id = QueryId::new_random();
772 let query = group.query(query_id, WorkloadClass::Oltp);
773
774 assert_eq!(tenant.node(), node);
776 assert_eq!(tenant.id(), TenantId::new(7));
777 assert_eq!(group.tenant(), tenant);
778 assert_eq!(group.node(), node);
779 assert_eq!(group.name(), "oltp");
780 assert_eq!(query.group(), &group);
781 assert_eq!(query.id(), query_id);
782 assert_eq!(query.class(), WorkloadClass::Oltp);
783 assert_eq!(query.tenant(), tenant);
784 assert_eq!(query.node(), node);
785 assert_eq!(node.id(), node.id());
786 }
787
788 #[test]
789 fn hierarchy_handles_serde_round_trip() {
790 let node = NodeHandle::new(NodeId::from_bytes([0xAB; 16]));
791 let query = node
792 .tenant(TenantId::new(42))
793 .group("analytics")
794 .query(QueryId::from_bytes([0xCD; 16]), WorkloadClass::Analytics);
795 let json = serde_json::to_string(&query).unwrap();
796 let back: QueryHandle = serde_json::from_str(&json).unwrap();
797 assert_eq!(back, query);
798 assert_eq!(back.node().id(), NodeId::from_bytes([0xAB; 16]));
799 assert_eq!(back.tenant().id(), TenantId::new(42));
800 }
801
802 #[test]
803 fn tenant_id_forms() {
804 let id = TenantId::new(123);
805 assert_eq!(id.get(), 123);
806 assert_eq!(id.to_string(), "123");
807 assert_eq!(format!("{id:?}"), "TenantId(123)");
808 assert_eq!("123".parse::<TenantId>().unwrap(), id);
809 assert_eq!(TenantId::ZERO.get(), 0);
810 }
811}