1use std::collections::{BTreeMap, BTreeSet, HashMap};
4use std::sync::{Arc, Mutex, MutexGuard};
5
6use aion_core::{ClusterEvent, InterventionCapabilities, WorkerDeathReason, WorkerTransport};
7use aion_proto::{ProtoActivityTask, ProtoRegisterWorker};
8use aion_store::{NamespaceOrigin, NamespacePlacement, NamespaceStore};
9use tokio::sync::{Notify, mpsc};
10
11use crate::cluster_publisher::ClusterEventPublisher;
12use crate::config::AutoCreate;
13use crate::error::ServerError;
14use crate::namespace::{CallerIdentity, NamespaceGuard, NamespaceMinter, NamespaceOperation};
15use crate::observability::Metrics;
16
17pub use aion_core::DEFAULT_TASK_QUEUE;
28
29pub type WorkerTaskSender = mpsc::Sender<WorkerMessage>;
31
32#[derive(Clone, Debug)]
44pub enum WorkerDelivery {
45 Grpc(WorkerTaskSender),
48 #[cfg(feature = "liminal-transport")]
53 Liminal(crate::worker::liminal_transport::LiminalWorkerDelivery),
54}
55
56#[derive(Clone, Debug, Eq, PartialEq)]
58pub enum WorkerMessage {
59 ActivityTask(ProtoActivityTask),
61 DrainRequest,
63}
64
65#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
77pub struct PoolAddress {
78 namespace: String,
79 task_queue: String,
80}
81
82impl PoolAddress {
83 #[must_use]
87 pub fn new(namespace: impl Into<String>, task_queue: impl Into<String>) -> Self {
88 let task_queue = task_queue.into();
89 let task_queue = if task_queue.is_empty() {
90 String::from(DEFAULT_TASK_QUEUE)
91 } else {
92 task_queue
93 };
94 Self {
95 namespace: namespace.into(),
96 task_queue,
97 }
98 }
99
100 #[must_use]
102 pub fn namespace(&self) -> &str {
103 &self.namespace
104 }
105
106 #[must_use]
108 pub fn task_queue(&self) -> &str {
109 &self.task_queue
110 }
111}
112
113#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
117struct ActivityKey {
118 pool: PoolAddress,
119 activity_type: String,
120}
121
122impl ActivityKey {
123 fn new(pool: PoolAddress, activity_type: impl Into<String>) -> Self {
124 Self {
125 pool,
126 activity_type: activity_type.into(),
127 }
128 }
129}
130
131type WorkerMap = HashMap<WorkerId, WorkerHandle>;
132type RegistryMap = HashMap<ActivityKey, WorkerMap>;
133
134#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
136pub struct WorkerId(u64);
137
138impl WorkerId {
139 #[must_use]
142 pub const fn value(self) -> u64 {
143 self.0
144 }
145}
146
147#[derive(Clone, Debug)]
155pub struct WorkerHandle {
156 id: WorkerId,
157 namespaces: BTreeSet<String>,
158 task_queue: String,
159 node: Option<String>,
160 activity_types: BTreeSet<String>,
161 delivery: WorkerDelivery,
162 intervention_capabilities: InterventionCapabilities,
168}
169
170impl WorkerHandle {
171 #[must_use]
173 pub const fn id(&self) -> WorkerId {
174 self.id
175 }
176
177 #[must_use]
180 pub const fn namespaces(&self) -> &BTreeSet<String> {
181 &self.namespaces
182 }
183
184 #[must_use]
186 pub fn task_queue(&self) -> &str {
187 &self.task_queue
188 }
189
190 #[must_use]
193 pub fn node(&self) -> Option<&str> {
194 self.node.as_deref()
195 }
196
197 #[must_use]
199 pub fn activity_types(&self) -> &BTreeSet<String> {
200 &self.activity_types
201 }
202
203 #[must_use]
205 pub const fn delivery(&self) -> &WorkerDelivery {
206 &self.delivery
207 }
208
209 #[must_use]
215 pub const fn intervention_capabilities(&self) -> &InterventionCapabilities {
216 &self.intervention_capabilities
217 }
218
219 #[must_use]
226 pub fn sender(&self) -> Option<&WorkerTaskSender> {
227 match &self.delivery {
228 WorkerDelivery::Grpc(sender) => Some(sender),
229 #[cfg(feature = "liminal-transport")]
230 WorkerDelivery::Liminal(_) => None,
231 }
232 }
233}
234
235#[derive(Debug)]
236struct RegistryState {
237 next_worker_id: u64,
238 workers: BTreeMap<WorkerId, WorkerHandle>,
239 by_activity: RegistryMap,
240 rotation: HashMap<ActivityKey, usize>,
243}
244
245impl Default for RegistryState {
246 fn default() -> Self {
247 Self {
248 next_worker_id: 1,
249 workers: BTreeMap::new(),
250 by_activity: HashMap::new(),
251 rotation: HashMap::new(),
252 }
253 }
254}
255
256#[derive(Clone, Debug)]
258pub struct ConnectedWorkerRegistry {
259 inner: Arc<Mutex<RegistryState>>,
260 metrics: Option<Metrics>,
261 cluster_publisher: Option<ClusterEventPublisher>,
265 minter: Option<NamespaceMinter>,
269 worker_arrived: Arc<Notify>,
270}
271
272impl Default for ConnectedWorkerRegistry {
273 fn default() -> Self {
274 Self {
275 inner: Arc::new(Mutex::new(RegistryState::default())),
276 metrics: None,
277 cluster_publisher: None,
278 minter: None,
279 worker_arrived: Arc::new(Notify::new()),
280 }
281 }
282}
283
284impl ConnectedWorkerRegistry {
285 #[must_use]
287 pub fn with_metrics(metrics: Metrics) -> Self {
288 Self {
289 inner: Arc::new(Mutex::new(RegistryState::default())),
290 metrics: Some(metrics),
291 cluster_publisher: None,
292 minter: None,
293 worker_arrived: Arc::new(Notify::new()),
294 }
295 }
296
297 #[must_use]
300 pub fn with_cluster_publisher(mut self, publisher: ClusterEventPublisher) -> Self {
301 self.cluster_publisher = Some(publisher);
302 self
303 }
304
305 #[must_use]
320 pub fn with_namespace_minting(
321 mut self,
322 store: Arc<dyn NamespaceStore>,
323 policy: AutoCreate,
324 ) -> Self {
325 let minter = NamespaceMinter::new(store, policy);
326 let minter = match &self.cluster_publisher {
327 Some(publisher) => minter.with_cluster_publisher(publisher.clone()),
328 None => minter,
329 };
330 self.minter = Some(minter);
331 self
332 }
333
334 pub async fn accept_registration(
340 &self,
341 guard: &NamespaceGuard,
342 caller: &CallerIdentity,
343 registration: &ProtoRegisterWorker,
344 sender: WorkerTaskSender,
345 ) -> Result<WorkerRegistration, ServerError> {
346 guard
352 .scope(caller, &NamespaceOperation::register_worker(registration))
353 .await?;
354 let namespaces = guard.scope_worker_namespaces(caller, ®istration.namespaces)?;
355 self.mint_or_gate_namespaces(&namespaces).await?;
363 let node = optional_node(®istration.node);
364 self.enforce_pinned_placement(&namespaces, node.as_deref())
374 .await?;
375 self.register_namespaces(
376 namespaces,
377 registration.task_queue.clone(),
378 node,
379 registration.activity_types.iter(),
380 sender,
381 )
382 }
383
384 async fn mint_or_gate_namespaces(&self, namespaces: &[String]) -> Result<(), ServerError> {
401 let Some(minter) = &self.minter else {
402 return Ok(());
403 };
404 minter
405 .mint_or_gate(namespaces, NamespaceOrigin::WorkerMint)
406 .await
407 }
408
409 async fn enforce_pinned_placement(
432 &self,
433 namespaces: &[String],
434 node: Option<&str>,
435 ) -> Result<(), ServerError> {
436 let Some(minter) = &self.minter else {
437 return Ok(());
438 };
439 for namespace in namespaces {
440 let NamespacePlacement::Pinned { nodes } = minter.placement_of(namespace).await? else {
441 continue;
442 };
443 let admitted = node.is_some_and(|n| nodes.contains(n));
444 if !admitted {
445 return Err(ServerError::placement_admission_denied(
446 namespace, node, &nodes,
447 ));
448 }
449 }
450 Ok(())
451 }
452
453 pub fn register<'a>(
464 &self,
465 namespace: impl Into<String>,
466 activity_types: impl IntoIterator<Item = &'a String>,
467 sender: WorkerTaskSender,
468 ) -> Result<WorkerRegistration, ServerError> {
469 self.register_namespaces(
470 [namespace.into()],
471 String::from(DEFAULT_TASK_QUEUE),
472 None,
473 activity_types,
474 sender,
475 )
476 }
477
478 pub fn register_pool<'a>(
485 &self,
486 pool: PoolAddress,
487 activity_types: impl IntoIterator<Item = &'a String>,
488 sender: WorkerTaskSender,
489 ) -> Result<WorkerRegistration, ServerError> {
490 let PoolAddress {
491 namespace,
492 task_queue,
493 } = pool;
494 self.register_namespaces([namespace], task_queue, None, activity_types, sender)
495 }
496
497 pub fn register_namespaces<'a>(
509 &self,
510 namespaces: impl IntoIterator<Item = String>,
511 task_queue: impl Into<String>,
512 node: Option<String>,
513 activity_types: impl IntoIterator<Item = &'a String>,
514 sender: WorkerTaskSender,
515 ) -> Result<WorkerRegistration, ServerError> {
516 self.register_delivery(
517 namespaces,
518 task_queue,
519 node,
520 activity_types,
521 WorkerDelivery::Grpc(sender),
522 )
523 }
524
525 pub fn register_delivery<'a>(
538 &self,
539 namespaces: impl IntoIterator<Item = String>,
540 task_queue: impl Into<String>,
541 node: Option<String>,
542 activity_types: impl IntoIterator<Item = &'a String>,
543 delivery: WorkerDelivery,
544 ) -> Result<WorkerRegistration, ServerError> {
545 self.register_delivery_with_capabilities(
546 namespaces,
547 task_queue,
548 node,
549 activity_types,
550 delivery,
551 InterventionCapabilities::none(),
552 )
553 }
554
555 pub fn register_delivery_with_capabilities<'a>(
569 &self,
570 namespaces: impl IntoIterator<Item = String>,
571 task_queue: impl Into<String>,
572 node: Option<String>,
573 activity_types: impl IntoIterator<Item = &'a String>,
574 delivery: WorkerDelivery,
575 intervention_capabilities: InterventionCapabilities,
576 ) -> Result<WorkerRegistration, ServerError> {
577 let namespaces = namespaces.into_iter().collect::<BTreeSet<_>>();
578 let task_queue = task_queue.into();
579 let activity_types = activity_types.into_iter().cloned().collect::<BTreeSet<_>>();
580 let mut state = self.state()?;
581 let worker_id = WorkerId(state.next_worker_id);
582 state.next_worker_id = state.next_worker_id.saturating_add(1);
583
584 let node_for_event = node.clone();
587 let handle = WorkerHandle {
588 id: worker_id,
589 namespaces: namespaces.clone(),
590 task_queue: task_queue.clone(),
591 node,
592 activity_types: activity_types.clone(),
593 delivery,
594 intervention_capabilities,
595 };
596
597 for namespace in &namespaces {
598 let pool = PoolAddress::new(namespace.clone(), task_queue.clone());
599 for activity_type in &activity_types {
600 state
601 .by_activity
602 .entry(ActivityKey::new(pool.clone(), activity_type.clone()))
603 .or_default()
604 .insert(worker_id, handle.clone());
605 }
606 }
607 let transport = transport_of(&handle.delivery);
608 state.workers.insert(worker_id, handle);
609 drop(state);
610
611 if let Some(metrics) = &self.metrics {
612 for namespace in &namespaces {
613 metrics.worker_connected(namespace);
614 }
615 }
616
617 if let Some(publisher) = &self.cluster_publisher {
621 let namespaces_vec: Vec<String> = namespaces.iter().cloned().collect();
622 let task_queue_owned = task_queue.clone();
623 drop(publisher.emit(|meta| ClusterEvent::WorkerConnected {
624 meta,
625 worker_id: worker_id.value().to_string(),
626 namespaces: namespaces_vec,
627 task_queue: task_queue_owned,
628 transport,
629 node: node_for_event,
630 }));
631 }
632
633 self.worker_arrived.notify_waiters();
634
635 Ok(WorkerRegistration {
636 registry: self.clone(),
637 parts: Some(WorkerRegistrationParts {
638 worker_id,
639 namespaces,
640 task_queue,
641 activity_types,
642 }),
643 })
644 }
645
646 pub async fn wait_for_worker(&self) {
652 self.worker_arrived.notified().await;
653 }
654
655 pub fn workers_for(
677 &self,
678 namespace: &str,
679 task_queue: &str,
680 activity_type: &str,
681 node: Option<&str>,
682 ) -> Result<Vec<WorkerHandle>, ServerError> {
683 let mut state = self.state()?;
684 let key = ActivityKey::new(PoolAddress::new(namespace, task_queue), activity_type);
685 let mut workers: Vec<WorkerHandle> = state
686 .by_activity
687 .get(&key)
688 .map(|workers| {
689 workers
690 .values()
691 .filter(|worker| worker_matches_node(worker, node))
692 .cloned()
693 .collect()
694 })
695 .unwrap_or_default();
696 if workers.is_empty() {
697 return Ok(workers);
698 }
699 workers.sort_by_key(WorkerHandle::id);
700 let idx = state.rotation.entry(key).or_insert(0);
701 let start = *idx % workers.len();
702 *idx = idx.wrapping_add(1);
703 let mut rotated = Vec::with_capacity(workers.len());
704 rotated.extend_from_slice(&workers[start..]);
705 rotated.extend_from_slice(&workers[..start]);
706 Ok(rotated)
707 }
708
709 pub fn all_workers(&self) -> Result<Vec<WorkerHandle>, ServerError> {
715 let state = self.state()?;
716 Ok(state.workers.values().cloned().collect())
717 }
718
719 pub fn worker_by_id(&self, worker_id: WorkerId) -> Result<Option<WorkerHandle>, ServerError> {
731 Ok(self.state()?.workers.get(&worker_id).cloned())
732 }
733
734 pub fn set_intervention_capabilities(
745 &self,
746 worker_id: WorkerId,
747 capabilities: &InterventionCapabilities,
748 ) -> Result<bool, ServerError> {
749 let mut state = self.state()?;
750 if !state.workers.contains_key(&worker_id) {
751 return Ok(false);
752 }
753 if let Some(handle) = state.workers.get_mut(&worker_id) {
754 handle.intervention_capabilities = capabilities.clone();
755 }
756 for workers in state.by_activity.values_mut() {
759 if let Some(handle) = workers.get_mut(&worker_id) {
760 handle.intervention_capabilities = capabilities.clone();
761 }
762 }
763 Ok(true)
764 }
765
766 pub fn broadcast_drain(&self) -> Result<usize, ServerError> {
772 let workers = self.all_workers()?;
773 let mut delivered = 0usize;
774 for worker in workers {
775 let Some(sender) = worker.sender() else {
779 continue;
780 };
781 if sender.try_send(WorkerMessage::DrainRequest).is_ok() {
782 delivered = delivered.saturating_add(1);
783 } else {
784 self.deregister(worker.id())?;
785 }
786 }
787 Ok(delivered)
788 }
789
790 pub fn select_worker(
800 &self,
801 namespace: &str,
802 task_queue: &str,
803 activity_type: &str,
804 node: Option<&str>,
805 ) -> Result<Option<WorkerHandle>, ServerError> {
806 let state = self.state()?;
807 let key = ActivityKey::new(PoolAddress::new(namespace, task_queue), activity_type);
808 Ok(state.by_activity.get(&key).and_then(|workers| {
809 workers
810 .values()
811 .filter(|worker| worker_matches_node(worker, node))
812 .min_by_key(|worker| worker.id)
813 .cloned()
814 }))
815 }
816
817 pub fn is_registered(&self, worker_id: WorkerId) -> Result<bool, ServerError> {
828 Ok(self.state()?.workers.contains_key(&worker_id))
829 }
830
831 pub fn deregister(&self, worker_id: WorkerId) -> Result<(), ServerError> {
843 self.deregister_with_reason(worker_id, WorkerDeathReason::Disconnect)
844 }
845
846 pub fn deregister_with_reason(
854 &self,
855 worker_id: WorkerId,
856 reason: WorkerDeathReason,
857 ) -> Result<(), ServerError> {
858 let mut state = self.state()?;
859 let removed_namespaces = Self::remove_worker(&mut state, worker_id);
860 drop(state);
861
862 let Some(namespaces) = removed_namespaces else {
863 return Ok(());
865 };
866
867 if let Some(metrics) = &self.metrics {
868 for namespace in &namespaces {
869 metrics.worker_disconnected(namespace);
870 }
871 }
872 self.emit_worker_disconnected(worker_id, &namespaces, reason);
873
874 Ok(())
875 }
876
877 fn emit_worker_disconnected(
879 &self,
880 worker_id: WorkerId,
881 namespaces: &BTreeSet<String>,
882 reason: WorkerDeathReason,
883 ) {
884 if let Some(publisher) = &self.cluster_publisher {
885 let namespaces_vec: Vec<String> = namespaces.iter().cloned().collect();
886 drop(publisher.emit(|meta| ClusterEvent::WorkerDisconnected {
887 meta,
888 worker_id: worker_id.value().to_string(),
889 namespaces: namespaces_vec,
890 reason,
891 }));
892 }
893 }
894
895 fn remove_worker(state: &mut RegistryState, worker_id: WorkerId) -> Option<BTreeSet<String>> {
899 let handle = state.workers.remove(&worker_id)?;
900
901 for namespace in &handle.namespaces {
902 let pool = PoolAddress::new(namespace.clone(), handle.task_queue.clone());
903 for activity_type in &handle.activity_types {
904 let key = ActivityKey::new(pool.clone(), activity_type.clone());
905 if let Some(workers) = state.by_activity.get_mut(&key) {
906 workers.remove(&worker_id);
907 if workers.is_empty() {
908 state.by_activity.remove(&key);
909 state.rotation.remove(&key);
916 }
917 }
918 }
919 }
920
921 Some(handle.namespaces)
922 }
923
924 fn state(&self) -> Result<MutexGuard<'_, RegistryState>, ServerError> {
925 self.inner
926 .lock()
927 .map_err(|_| ServerError::lock_poisoned("connected worker registry"))
928 }
929}
930
931const fn transport_of(delivery: &WorkerDelivery) -> WorkerTransport {
934 match delivery {
935 WorkerDelivery::Grpc(_) => WorkerTransport::Grpc,
936 #[cfg(feature = "liminal-transport")]
937 WorkerDelivery::Liminal(_) => WorkerTransport::Liminal,
938 }
939}
940
941fn optional_node(node: &str) -> Option<String> {
945 if node.is_empty() {
946 None
947 } else {
948 Some(node.to_owned())
949 }
950}
951
952fn worker_matches_node(worker: &WorkerHandle, node: Option<&str>) -> bool {
957 match node {
958 None => true,
959 Some(node) => worker.node() == Some(node),
960 }
961}
962
963#[derive(Clone, Debug)]
964struct WorkerRegistrationParts {
965 worker_id: WorkerId,
966 namespaces: BTreeSet<String>,
967 task_queue: String,
968 activity_types: BTreeSet<String>,
969}
970
971#[derive(Debug)]
976pub struct WorkerRegistration {
977 registry: ConnectedWorkerRegistry,
978 parts: Option<WorkerRegistrationParts>,
979}
980
981impl WorkerRegistration {
982 #[must_use]
984 pub fn worker_id(&self) -> Option<WorkerId> {
985 self.parts.as_ref().map(|parts| parts.worker_id)
986 }
987
988 #[must_use]
990 pub fn namespaces(&self) -> Option<&BTreeSet<String>> {
991 self.parts.as_ref().map(|parts| &parts.namespaces)
992 }
993
994 #[must_use]
996 pub fn task_queue(&self) -> Option<&str> {
997 self.parts.as_ref().map(|parts| parts.task_queue.as_str())
998 }
999
1000 #[must_use]
1002 pub fn activity_types(&self) -> Option<&BTreeSet<String>> {
1003 self.parts.as_ref().map(|parts| &parts.activity_types)
1004 }
1005
1006 pub fn deregister(mut self) -> Result<(), ServerError> {
1012 let Some(parts) = self.parts.take() else {
1013 return Ok(());
1014 };
1015 self.registry.deregister(parts.worker_id)
1016 }
1017}
1018
1019impl Drop for WorkerRegistration {
1020 fn drop(&mut self) {
1021 let Some(parts) = self.parts.take() else {
1022 return;
1023 };
1024 let removed_namespaces = self.registry.inner.lock().ok().and_then(|mut state| {
1025 ConnectedWorkerRegistry::remove_worker(&mut state, parts.worker_id)
1026 });
1027 if let Some(namespaces) = removed_namespaces {
1028 if let Some(metrics) = &self.registry.metrics {
1029 for namespace in &namespaces {
1030 metrics.worker_disconnected(namespace);
1031 }
1032 }
1033 self.registry.emit_worker_disconnected(
1037 parts.worker_id,
1038 &namespaces,
1039 WorkerDeathReason::Disconnect,
1040 );
1041 }
1042 }
1043}
1044
1045#[cfg(test)]
1046mod tests {
1047 use crate::config::NamespaceMode;
1048 use crate::namespace::{NamespaceResolver, StaticScheduleNamespaces, StaticWorkflowNamespaces};
1049
1050 use super::*;
1051
1052 fn guard() -> NamespaceGuard {
1053 NamespaceGuard::new(NamespaceResolver::authorization_only(
1054 NamespaceMode::SharedEngine,
1055 StaticWorkflowNamespaces::default(),
1056 StaticScheduleNamespaces::default(),
1057 ))
1058 }
1059
1060 fn caller(namespace: &str) -> CallerIdentity {
1061 CallerIdentity::new("worker", [namespace.to_owned()])
1062 }
1063
1064 fn test_failure(message: &str) -> ServerError {
1067 ServerError::worker_dispatch("default".to_owned(), "test".to_owned(), message.to_owned())
1068 }
1069
1070 fn registration(namespace: &str, activity_types: &[&str]) -> ProtoRegisterWorker {
1071 registration_with_queue(namespace, "", activity_types)
1072 }
1073
1074 fn registration_with_queue(
1075 namespace: &str,
1076 task_queue: &str,
1077 activity_types: &[&str],
1078 ) -> ProtoRegisterWorker {
1079 registration_full(&[namespace], task_queue, "", activity_types)
1080 }
1081
1082 fn registration_full(
1083 namespaces: &[&str],
1084 task_queue: &str,
1085 node: &str,
1086 activity_types: &[&str],
1087 ) -> ProtoRegisterWorker {
1088 ProtoRegisterWorker {
1089 namespaces: namespaces.iter().map(|value| (*value).to_owned()).collect(),
1090 activity_types: activity_types
1091 .iter()
1092 .map(|value| (*value).to_owned())
1093 .collect(),
1094 task_queue: task_queue.to_owned(),
1095 node: node.to_owned(),
1096 }
1097 }
1098
1099 fn multi_caller(namespaces: &[&str]) -> CallerIdentity {
1100 CallerIdentity::new("worker", namespaces.iter().map(|value| (*value).to_owned()))
1101 }
1102
1103 #[tokio::test]
1109 async fn set_intervention_capabilities_updates_live_worker() -> Result<(), ServerError> {
1110 let registry = ConnectedWorkerRegistry::default();
1111 let (sender, _receiver) = mpsc::channel(1);
1112 let types = ["scout".to_owned()];
1113 let guard = registry.register_delivery_with_capabilities(
1114 ["default".to_owned()],
1115 "default",
1116 None,
1117 types.iter(),
1118 WorkerDelivery::Grpc(sender),
1119 InterventionCapabilities::none(),
1120 )?;
1121 let Some(worker_id) = guard.worker_id() else {
1122 return Err(test_failure("registration carries an id"));
1123 };
1124
1125 let announced = InterventionCapabilities {
1126 supported: vec![aion_core::InterventionPrimitive::InjectMessage],
1127 };
1128 assert!(
1129 registry.set_intervention_capabilities(worker_id, &announced)?,
1130 "a live worker's capabilities must be updatable"
1131 );
1132 let Some(handle) = registry.worker_by_id(worker_id)? else {
1133 return Err(test_failure("worker stays registered"));
1134 };
1135 assert_eq!(handle.intervention_capabilities(), &announced);
1136
1137 assert!(
1138 !registry.set_intervention_capabilities(WorkerId(u64::MAX), &announced)?,
1139 "an unknown worker reports false, never an error"
1140 );
1141 Ok(())
1142 }
1143
1144 #[tokio::test]
1145 async fn register_and_deregister_are_namespace_isolated() -> Result<(), ServerError> {
1146 let registry = ConnectedWorkerRegistry::default();
1147 let (tenant_a_tx, _tenant_a_rx) = mpsc::channel(1);
1148 let (tenant_b_tx, _tenant_b_rx) = mpsc::channel(1);
1149
1150 let tenant_a = registry
1151 .accept_registration(
1152 &guard(),
1153 &caller("tenant-a"),
1154 ®istration("tenant-a", &["charge", "charge"]),
1155 tenant_a_tx,
1156 )
1157 .await?;
1158 let tenant_b = registry
1159 .accept_registration(
1160 &guard(),
1161 &caller("tenant-b"),
1162 ®istration("tenant-b", &["charge"]),
1163 tenant_b_tx,
1164 )
1165 .await?;
1166
1167 let tq = DEFAULT_TASK_QUEUE;
1168 assert_eq!(
1169 registry.workers_for("tenant-a", tq, "charge", None)?.len(),
1170 1
1171 );
1172 assert_eq!(
1173 registry.workers_for("tenant-b", tq, "charge", None)?.len(),
1174 1
1175 );
1176 assert!(
1177 registry
1178 .workers_for("tenant-a", tq, "missing", None)?
1179 .is_empty()
1180 );
1181
1182 let tenant_a_id = tenant_a.worker_id();
1183 tenant_a.deregister()?;
1184
1185 assert!(
1186 registry
1187 .workers_for("tenant-a", tq, "charge", None)?
1188 .is_empty()
1189 );
1190 assert_eq!(
1191 registry.workers_for("tenant-b", tq, "charge", None)?.len(),
1192 1
1193 );
1194 assert_ne!(tenant_a_id, tenant_b.worker_id());
1195
1196 tenant_b.deregister()?;
1197 assert!(
1198 registry
1199 .workers_for("tenant-b", tq, "charge", None)?
1200 .is_empty()
1201 );
1202 Ok(())
1203 }
1204
1205 #[tokio::test]
1206 async fn denied_namespace_is_not_registered() -> Result<(), ServerError> {
1207 let registry = ConnectedWorkerRegistry::default();
1208 let (tx, _rx) = mpsc::channel(1);
1209 let denied = registry
1210 .accept_registration(
1211 &guard(),
1212 &caller("tenant-a"),
1213 ®istration("tenant-b", &["charge"]),
1214 tx,
1215 )
1216 .await;
1217
1218 assert!(denied.is_err());
1219 assert!(
1220 registry
1221 .workers_for("tenant-b", DEFAULT_TASK_QUEUE, "charge", None)?
1222 .is_empty()
1223 );
1224 Ok(())
1225 }
1226
1227 #[tokio::test]
1228 async fn task_queues_partition_disjoint_pools_within_one_namespace() -> Result<(), ServerError>
1229 {
1230 let registry = ConnectedWorkerRegistry::default();
1234 let (norn_tx, _norn_rx) = mpsc::channel(1);
1235 let (claude_a_tx, _claude_a_rx) = mpsc::channel(1);
1236 let (claude_b_tx, _claude_b_rx) = mpsc::channel(1);
1237
1238 let norn = registry
1239 .accept_registration(
1240 &guard(),
1241 &caller("local"),
1242 ®istration_with_queue("local", "norn", &["dev"]),
1243 norn_tx,
1244 )
1245 .await?;
1246 let claude_a = registry
1248 .accept_registration(
1249 &guard(),
1250 &caller("local"),
1251 ®istration_with_queue("local", "claude", &["dev"]),
1252 claude_a_tx,
1253 )
1254 .await?;
1255 let claude_b = registry
1256 .accept_registration(
1257 &guard(),
1258 &caller("local"),
1259 ®istration_with_queue("local", "claude", &["dev"]),
1260 claude_b_tx,
1261 )
1262 .await?;
1263
1264 let norn_pool = registry.workers_for("local", "norn", "dev", None)?;
1265 assert_eq!(norn_pool.len(), 1, "norn pool has exactly its one worker");
1266 let norn_id = norn.worker_id().ok_or_else(missing_id)?;
1267 assert_eq!(norn_pool[0].id(), norn_id);
1268
1269 let claude_pool = registry.workers_for("local", "claude", "dev", None)?;
1270 assert_eq!(
1271 claude_pool.len(),
1272 2,
1273 "claude pool sees only its two workers"
1274 );
1275 let claude_ids: BTreeSet<WorkerId> = claude_pool.iter().map(WorkerHandle::id).collect();
1276 assert!(
1277 !claude_ids.contains(&norn_id),
1278 "the norn worker must never appear in the claude pool"
1279 );
1280
1281 assert!(
1284 !registry
1285 .workers_for("local", "norn", "dev", None)?
1286 .iter()
1287 .any(|worker| claude_ids.contains(&worker.id()))
1288 );
1289
1290 let first = registry.workers_for("local", "claude", "dev", None)?[0].id();
1294 let second = registry.workers_for("local", "claude", "dev", None)?[0].id();
1295 assert_ne!(
1296 first, second,
1297 "claude pool round-robins across both workers"
1298 );
1299 assert_eq!(
1300 registry.workers_for("local", "norn", "dev", None)?[0].id(),
1301 norn_id,
1302 "the norn pool rotation is unaffected by claude traffic"
1303 );
1304
1305 norn.deregister()?;
1306 claude_a.deregister()?;
1307 claude_b.deregister()?;
1308 Ok(())
1309 }
1310
1311 #[tokio::test]
1312 async fn same_task_queue_in_different_namespaces_is_isolated() -> Result<(), ServerError> {
1313 let registry = ConnectedWorkerRegistry::default();
1316 let (local_tx, _local_rx) = mpsc::channel(1);
1317 let (remote_tx, _remote_rx) = mpsc::channel(1);
1318
1319 let local = registry
1320 .accept_registration(
1321 &guard(),
1322 &caller("local"),
1323 ®istration_with_queue("local", "gpu", &["render"]),
1324 local_tx,
1325 )
1326 .await?;
1327 let remote = registry
1328 .accept_registration(
1329 &guard(),
1330 &caller("remote"),
1331 ®istration_with_queue("remote", "gpu", &["render"]),
1332 remote_tx,
1333 )
1334 .await?;
1335
1336 let local_pool = registry.workers_for("local", "gpu", "render", None)?;
1337 let remote_pool = registry.workers_for("remote", "gpu", "render", None)?;
1338 assert_eq!(local_pool.len(), 1);
1339 assert_eq!(remote_pool.len(), 1);
1340 assert_ne!(
1341 local_pool[0].id(),
1342 remote_pool[0].id(),
1343 "a shared task_queue string does not merge two namespaces"
1344 );
1345
1346 local.deregister()?;
1347 assert!(
1348 registry
1349 .workers_for("local", "gpu", "render", None)?
1350 .is_empty(),
1351 "deregistering the local worker leaves the remote namespace untouched"
1352 );
1353 assert_eq!(
1354 registry.workers_for("remote", "gpu", "render", None)?.len(),
1355 1
1356 );
1357
1358 remote.deregister()?;
1359 Ok(())
1360 }
1361
1362 #[tokio::test]
1363 async fn worker_serving_a_namespace_set_is_reachable_in_each() -> Result<(), ServerError> {
1364 let registry = ConnectedWorkerRegistry::default();
1367 let (ab_tx, _ab_rx) = mpsc::channel(1);
1368 let (a_tx, _a_rx) = mpsc::channel(1);
1369
1370 let worker_ab = registry
1371 .accept_registration(
1372 &guard(),
1373 &multi_caller(&["a", "b"]),
1374 ®istration_full(&["a", "b"], "default", "", &["dev"]),
1375 ab_tx,
1376 )
1377 .await?;
1378 let worker_a = registry
1379 .accept_registration(
1380 &guard(),
1381 &caller("a"),
1382 ®istration_full(&["a"], "default", "", &["dev"]),
1383 a_tx,
1384 )
1385 .await?;
1386
1387 let in_a = registry.workers_for("a", "default", "dev", None)?;
1388 let in_b = registry.workers_for("b", "default", "dev", None)?;
1389 let both_id = worker_ab.worker_id().ok_or_else(missing_id)?;
1390 let only_a_id = worker_a.worker_id().ok_or_else(missing_id)?;
1391
1392 let a_ids: BTreeSet<WorkerId> = in_a.iter().map(WorkerHandle::id).collect();
1394 assert_eq!(a_ids, BTreeSet::from([both_id, only_a_id]));
1395 assert_eq!(in_b.len(), 1, "only the {{a, b}} worker is reachable in b");
1396 assert_eq!(in_b[0].id(), both_id);
1397 assert!(
1398 !in_b.iter().any(|worker| worker.id() == only_a_id),
1399 "the {{a}}-only worker must not be reachable in b"
1400 );
1401
1402 worker_ab.deregister()?;
1404 assert!(
1405 registry
1406 .workers_for("b", "default", "dev", None)?
1407 .is_empty()
1408 );
1409 assert_eq!(registry.workers_for("a", "default", "dev", None)?.len(), 1);
1410
1411 worker_a.deregister()?;
1412 Ok(())
1413 }
1414
1415 #[tokio::test]
1416 async fn node_pin_filters_within_pool() -> Result<(), ServerError> {
1417 let registry = ConnectedWorkerRegistry::default();
1421 let (n1_tx, _n1_rx) = mpsc::channel(1);
1422 let (n2_tx, _n2_rx) = mpsc::channel(1);
1423
1424 let on_n1 = registry
1425 .accept_registration(
1426 &guard(),
1427 &caller("ns"),
1428 ®istration_full(&["ns"], "tq", "n1", &["dev"]),
1429 n1_tx,
1430 )
1431 .await?;
1432 let on_n2 = registry
1433 .accept_registration(
1434 &guard(),
1435 &caller("ns"),
1436 ®istration_full(&["ns"], "tq", "n2", &["dev"]),
1437 n2_tx,
1438 )
1439 .await?;
1440 let n1_id = on_n1.worker_id().ok_or_else(missing_id)?;
1441 let n2_id = on_n2.worker_id().ok_or_else(missing_id)?;
1442
1443 let unpinned = registry.workers_for("ns", "tq", "dev", None)?;
1445 assert_eq!(unpinned.len(), 2, "unpinned reaches the whole pool");
1446 let first = registry.workers_for("ns", "tq", "dev", None)?[0].id();
1447 let second = registry.workers_for("ns", "tq", "dev", None)?[0].id();
1448 assert_ne!(first, second, "unpinned round-robins across both nodes");
1449
1450 let pinned_n1 = registry.workers_for("ns", "tq", "dev", Some("n1"))?;
1452 assert_eq!(pinned_n1.len(), 1);
1453 assert_eq!(pinned_n1[0].id(), n1_id);
1454 let pinned_n2 = registry.workers_for("ns", "tq", "dev", Some("n2"))?;
1455 assert_eq!(pinned_n2.len(), 1);
1456 assert_eq!(pinned_n2[0].id(), n2_id);
1457
1458 assert_eq!(
1460 registry
1461 .select_worker("ns", "tq", "dev", Some("n1"))?
1462 .map(|worker| worker.id()),
1463 Some(n1_id)
1464 );
1465
1466 assert!(
1469 registry
1470 .workers_for("ns", "tq", "dev", Some("absent"))?
1471 .is_empty(),
1472 "a pin to a node with no worker yields no candidate"
1473 );
1474 assert!(
1475 registry
1476 .select_worker("ns", "tq", "dev", Some("absent"))?
1477 .is_none()
1478 );
1479
1480 on_n1.deregister()?;
1481 on_n2.deregister()?;
1482 Ok(())
1483 }
1484
1485 #[tokio::test]
1486 async fn shared_node_id_round_robins_across_workers() -> Result<(), ServerError> {
1487 let registry = ConnectedWorkerRegistry::default();
1490 let (a_tx, _a_rx) = mpsc::channel(1);
1491 let (b_tx, _b_rx) = mpsc::channel(1);
1492
1493 let worker_a = registry
1494 .accept_registration(
1495 &guard(),
1496 &caller("ns"),
1497 ®istration_full(&["ns"], "tq", "shared", &["dev"]),
1498 a_tx,
1499 )
1500 .await?;
1501 let worker_b = registry
1502 .accept_registration(
1503 &guard(),
1504 &caller("ns"),
1505 ®istration_full(&["ns"], "tq", "shared", &["dev"]),
1506 b_tx,
1507 )
1508 .await?;
1509 let a_id = worker_a.worker_id().ok_or_else(missing_id)?;
1510 let b_id = worker_b.worker_id().ok_or_else(missing_id)?;
1511
1512 let pinned = registry.workers_for("ns", "tq", "dev", Some("shared"))?;
1513 assert_eq!(
1514 pinned.len(),
1515 2,
1516 "both workers on the shared node are candidates"
1517 );
1518 let pinned_ids: BTreeSet<WorkerId> = pinned.iter().map(WorkerHandle::id).collect();
1519 assert_eq!(pinned_ids, BTreeSet::from([a_id, b_id]));
1520
1521 let first = registry.workers_for("ns", "tq", "dev", Some("shared"))?[0].id();
1522 let second = registry.workers_for("ns", "tq", "dev", Some("shared"))?[0].id();
1523 assert_ne!(
1524 first, second,
1525 "a pin to a shared node round-robins across both workers on it"
1526 );
1527
1528 worker_a.deregister()?;
1529 worker_b.deregister()?;
1530 Ok(())
1531 }
1532
1533 #[tokio::test]
1534 async fn rotation_cursor_is_pruned_when_last_worker_leaves() -> Result<(), ServerError> {
1535 let registry = ConnectedWorkerRegistry::default();
1540 let (tx, _rx) = mpsc::channel(1);
1541 let worker = registry
1542 .accept_registration(
1543 &guard(),
1544 &caller("ns"),
1545 ®istration_full(&["ns"], "tq", "", &["dev"]),
1546 tx,
1547 )
1548 .await?;
1549
1550 let _ = registry.workers_for("ns", "tq", "dev", None)?;
1552 let key = ActivityKey::new(PoolAddress::new("ns", "tq"), "dev");
1553 assert!(
1554 registry.state()?.rotation.contains_key(&key),
1555 "a lookup must have created the rotation cursor"
1556 );
1557
1558 worker.deregister()?;
1559 let state = registry.state()?;
1560 assert!(
1561 !state.rotation.contains_key(&key),
1562 "the rotation cursor must be pruned once the last worker leaves"
1563 );
1564 assert!(
1565 !state.by_activity.contains_key(&key),
1566 "the activity bucket must also be gone"
1567 );
1568 Ok(())
1569 }
1570
1571 fn missing_id() -> ServerError {
1572 ServerError::lock_poisoned("registration unexpectedly missing a worker id")
1573 }
1574
1575 fn namespace_store() -> Arc<dyn NamespaceStore> {
1578 Arc::new(aion_store::InMemoryStore::default())
1579 }
1580
1581 fn minting_registry(
1582 store: &Arc<dyn NamespaceStore>,
1583 policy: AutoCreate,
1584 ) -> ConnectedWorkerRegistry {
1585 ConnectedWorkerRegistry::default().with_namespace_minting(Arc::clone(store), policy)
1586 }
1587
1588 #[tokio::test]
1589 async fn open_register_mints_durable_record_and_is_idempotent() -> Result<(), ServerError> {
1590 let store = namespace_store();
1591 let registry = minting_registry(&store, AutoCreate::Open);
1592
1593 let (tx_one, _rx_one) = mpsc::channel(1);
1595 let first = registry
1596 .accept_registration(
1597 &guard(),
1598 &caller("orders"),
1599 ®istration("orders", &["charge"]),
1600 tx_one,
1601 )
1602 .await?;
1603 let record = store
1604 .get_namespace("orders")
1605 .await?
1606 .ok_or_else(|| ServerError::namespace_denied("expected a minted record"))?;
1607 assert_eq!(record.name, "orders");
1608 assert_eq!(record.origin, NamespaceOrigin::WorkerMint);
1609
1610 let (tx_two, _rx_two) = mpsc::channel(1);
1613 registry
1614 .accept_registration(
1615 &guard(),
1616 &caller("orders"),
1617 ®istration("orders", &["refund"]),
1618 tx_two,
1619 )
1620 .await?;
1621 let all = store.list_namespaces().await?;
1622 assert_eq!(
1623 all.iter().filter(|r| r.name == "orders").count(),
1624 1,
1625 "re-register must not create a duplicate namespace row"
1626 );
1627 drop(first);
1628 Ok(())
1629 }
1630
1631 #[tokio::test]
1632 async fn open_register_mints_each_namespace_in_a_multi_namespace_worker()
1633 -> Result<(), ServerError> {
1634 let store = namespace_store();
1635 let registry = minting_registry(&store, AutoCreate::Open);
1636 let (tx, _rx) = mpsc::channel(1);
1637
1638 registry
1639 .accept_registration(
1640 &guard(),
1641 &multi_caller(&["alpha", "beta"]),
1642 ®istration_full(&["alpha", "beta"], "", "", &["charge"]),
1643 tx,
1644 )
1645 .await?;
1646
1647 assert!(store.get_namespace("alpha").await?.is_some());
1648 assert!(store.get_namespace("beta").await?.is_some());
1649 Ok(())
1650 }
1651
1652 async fn pinned_registry(
1658 store: &Arc<dyn NamespaceStore>,
1659 namespace: &str,
1660 nodes: &[&str],
1661 ) -> Result<ConnectedWorkerRegistry, ServerError> {
1662 store
1663 .register_namespace(namespace, NamespaceOrigin::Explicit)
1664 .await?;
1665 store
1666 .set_namespace_placement(
1667 namespace,
1668 NamespacePlacement::Pinned {
1669 nodes: nodes.iter().map(|n| (*n).to_owned()).collect(),
1670 },
1671 )
1672 .await?;
1673 Ok(minting_registry(store, AutoCreate::Open))
1674 }
1675
1676 #[tokio::test]
1679 async fn pinned_admits_a_worker_on_a_required_node() -> Result<(), ServerError> {
1680 let store = namespace_store();
1681 let registry = pinned_registry(&store, "iso", &["n1"]).await?;
1682 let (tx, _rx) = mpsc::channel(1);
1683
1684 let _registration = registry
1685 .accept_registration(
1686 &guard(),
1687 &caller("iso"),
1688 ®istration_full(&["iso"], "", "n1", &["charge"]),
1689 tx,
1690 )
1691 .await?;
1692
1693 assert_eq!(
1694 registry
1695 .workers_for("iso", DEFAULT_TASK_QUEUE, "charge", Some("n1"))?
1696 .len(),
1697 1,
1698 "an n1 worker must be admitted into the Pinned{{n1}} namespace's pool"
1699 );
1700 Ok(())
1701 }
1702
1703 #[tokio::test]
1707 async fn pinned_rejects_a_wrong_node_worker() -> Result<(), ServerError> {
1708 let store = namespace_store();
1709 let registry = pinned_registry(&store, "iso", &["n1"]).await?;
1710 let (tx, _rx) = mpsc::channel(1);
1711
1712 let denied = registry
1713 .accept_registration(
1714 &guard(),
1715 &caller("iso"),
1716 ®istration_full(&["iso"], "", "n2", &["charge"]),
1717 tx,
1718 )
1719 .await;
1720 assert!(
1721 matches!(denied, Err(ServerError::Namespace { .. })),
1722 "a wrong-node (n2) worker must be rejected from a Pinned{{n1}} namespace"
1723 );
1724 assert!(
1725 registry
1726 .workers_for("iso", DEFAULT_TASK_QUEUE, "charge", None)?
1727 .is_empty(),
1728 "a rejected registration must not insert a worker on any node"
1729 );
1730 Ok(())
1731 }
1732
1733 #[tokio::test]
1736 async fn pinned_rejects_a_node_less_worker() -> Result<(), ServerError> {
1737 let store = namespace_store();
1738 let registry = pinned_registry(&store, "iso", &["n1"]).await?;
1739 let (tx, _rx) = mpsc::channel(1);
1740
1741 let denied = registry
1742 .accept_registration(
1743 &guard(),
1744 &caller("iso"),
1745 ®istration_full(&["iso"], "", "", &["charge"]),
1746 tx,
1747 )
1748 .await;
1749 assert!(
1750 matches!(denied, Err(ServerError::Namespace { .. })),
1751 "a node-less worker must be rejected from a Pinned{{n1}} namespace"
1752 );
1753 assert!(
1754 registry
1755 .workers_for("iso", DEFAULT_TASK_QUEUE, "charge", None)?
1756 .is_empty(),
1757 "a rejected node-less registration must not insert a worker"
1758 );
1759 Ok(())
1760 }
1761
1762 #[tokio::test]
1766 async fn pinned_violation_rejects_the_whole_multi_namespace_registration()
1767 -> Result<(), ServerError> {
1768 let store = namespace_store();
1769 let registry = pinned_registry(&store, "iso", &["n1"]).await?;
1770 let (tx, _rx) = mpsc::channel(1);
1771
1772 let denied = registry
1773 .accept_registration(
1774 &guard(),
1775 &multi_caller(&["free", "iso"]),
1776 ®istration_full(&["free", "iso"], "", "n2", &["charge"]),
1777 tx,
1778 )
1779 .await;
1780 assert!(
1781 matches!(denied, Err(ServerError::Namespace { .. })),
1782 "a wrong-node worker serving a Pinned namespace fails the WHOLE registration"
1783 );
1784 assert!(
1785 registry
1786 .workers_for("free", DEFAULT_TASK_QUEUE, "charge", None)?
1787 .is_empty(),
1788 "the compliant namespace must NOT be partially admitted"
1789 );
1790 Ok(())
1791 }
1792
1793 #[tokio::test]
1796 async fn unplaced_and_prefer_admission_is_unaffected_by_the_pinned_gate()
1797 -> Result<(), ServerError> {
1798 let store = namespace_store();
1799 store
1802 .register_namespace("pref", NamespaceOrigin::Explicit)
1803 .await?;
1804 store
1805 .set_namespace_placement(
1806 "pref",
1807 NamespacePlacement::Prefer {
1808 nodes: ["n1".to_owned()].into_iter().collect(),
1809 },
1810 )
1811 .await?;
1812 let registry = minting_registry(&store, AutoCreate::Open);
1813
1814 let (tx_a, _rx_a) = mpsc::channel(1);
1815 let _reg_a = registry
1816 .accept_registration(
1817 &guard(),
1818 &caller("unpl"),
1819 ®istration_full(&["unpl"], "", "", &["charge"]),
1820 tx_a,
1821 )
1822 .await?;
1823 let (tx_b, _rx_b) = mpsc::channel(1);
1824 let _reg_b = registry
1825 .accept_registration(
1826 &guard(),
1827 &caller("pref"),
1828 ®istration_full(&["pref"], "", "", &["charge"]),
1829 tx_b,
1830 )
1831 .await?;
1832
1833 assert_eq!(
1834 registry
1835 .workers_for("unpl", DEFAULT_TASK_QUEUE, "charge", None)?
1836 .len(),
1837 1,
1838 "an Unplaced namespace admits a node-less worker unchanged"
1839 );
1840 assert_eq!(
1841 registry
1842 .workers_for("pref", DEFAULT_TASK_QUEUE, "charge", None)?
1843 .len(),
1844 1,
1845 "a Prefer namespace admits a node-less worker unchanged (only Pinned gates)"
1846 );
1847 Ok(())
1848 }
1849
1850 #[tokio::test]
1854 async fn no_minter_registry_skips_the_placement_gate() -> Result<(), ServerError> {
1855 let registry = ConnectedWorkerRegistry::default();
1856 let (tx, _rx) = mpsc::channel(1);
1857 let _registration = registry
1858 .accept_registration(
1859 &guard(),
1860 &caller("plain"),
1861 ®istration_full(&["plain"], "", "", &["charge"]),
1862 tx,
1863 )
1864 .await?;
1865 assert_eq!(
1866 registry
1867 .workers_for("plain", DEFAULT_TASK_QUEUE, "charge", None)?
1868 .len(),
1869 1,
1870 "with no minter the placement gate is a no-op — registration is unchanged"
1871 );
1872 Ok(())
1873 }
1874
1875 #[tokio::test]
1876 async fn concurrent_registrations_for_a_new_namespace_create_exactly_one_record()
1877 -> Result<(), ServerError> {
1878 let store = namespace_store();
1879 let registry = minting_registry(&store, AutoCreate::Open);
1880
1881 let mut handles = Vec::new();
1882 for _ in 0..8 {
1883 let registry = registry.clone();
1884 handles.push(tokio::spawn(async move {
1885 let (tx, rx) = mpsc::channel(1);
1886 let outcome = registry
1887 .accept_registration(
1888 &guard(),
1889 &caller("rush"),
1890 ®istration("rush", &["charge"]),
1891 tx,
1892 )
1893 .await;
1894 drop(rx);
1896 outcome.map(|registration| registration.worker_id())
1897 }));
1898 }
1899 for handle in handles {
1900 handle
1901 .await
1902 .map_err(|_| ServerError::lock_poisoned("registration task panicked"))??;
1903 }
1904
1905 let all = store.list_namespaces().await?;
1906 assert_eq!(
1907 all.iter().filter(|r| r.name == "rush").count(),
1908 1,
1909 "racing registrations must converge on exactly one durable record"
1910 );
1911 Ok(())
1912 }
1913
1914 #[tokio::test]
1915 async fn closed_rejects_unknown_namespace_and_does_not_create_it() -> Result<(), ServerError> {
1916 let store = namespace_store();
1917 let registry = minting_registry(&store, AutoCreate::Closed);
1918 let (tx, _rx) = mpsc::channel(1);
1919
1920 let denied = registry
1921 .accept_registration(
1922 &guard(),
1923 &caller("ghost"),
1924 ®istration("ghost", &["charge"]),
1925 tx,
1926 )
1927 .await;
1928 assert!(
1929 matches!(denied, Err(ServerError::Namespace { .. })),
1930 "closed policy must reject an unknown namespace"
1931 );
1932 assert!(
1933 store.get_namespace("ghost").await?.is_none(),
1934 "closed policy must NOT create the namespace it rejected"
1935 );
1936 let tq = DEFAULT_TASK_QUEUE;
1937 assert!(
1938 registry
1939 .workers_for("ghost", tq, "charge", None)?
1940 .is_empty(),
1941 "a rejected registration must not insert a worker"
1942 );
1943 Ok(())
1944 }
1945
1946 #[tokio::test]
1947 async fn closed_admits_a_known_namespace() -> Result<(), ServerError> {
1948 let store = namespace_store();
1949 store
1951 .register_namespace("known", NamespaceOrigin::Explicit)
1952 .await?;
1953 let registry = minting_registry(&store, AutoCreate::Closed);
1954 let (tx, _rx) = mpsc::channel(1);
1955
1956 let _registration = registry
1958 .accept_registration(
1959 &guard(),
1960 &caller("known"),
1961 ®istration("known", &["charge"]),
1962 tx,
1963 )
1964 .await?;
1965 let tq = DEFAULT_TASK_QUEUE;
1966 assert_eq!(
1967 registry.workers_for("known", tq, "charge", None)?.len(),
1968 1,
1969 "a known namespace must register under closed policy"
1970 );
1971 Ok(())
1972 }
1973
1974 #[tokio::test]
1975 async fn no_minter_leaves_registration_untouched() -> Result<(), ServerError> {
1976 let registry = ConnectedWorkerRegistry::default();
1979 let (tx, _rx) = mpsc::channel(1);
1980 let _registration = registry
1981 .accept_registration(
1982 &guard(),
1983 &caller("orders"),
1984 ®istration("orders", &["charge"]),
1985 tx,
1986 )
1987 .await?;
1988 let tq = DEFAULT_TASK_QUEUE;
1989 assert_eq!(registry.workers_for("orders", tq, "charge", None)?.len(), 1);
1990 Ok(())
1991 }
1992}