1use std::collections::{BTreeMap, BTreeSet, HashMap};
4use std::sync::{Arc, Mutex, MutexGuard};
5
6use aion_proto::{ProtoActivityTask, ProtoRegisterWorker};
7use tokio::sync::mpsc;
8
9use crate::error::ServerError;
10use crate::namespace::{CallerIdentity, NamespaceGuard, NamespaceOperation};
11use crate::observability::Metrics;
12
13pub type WorkerTaskSender = mpsc::Sender<WorkerMessage>;
15
16#[derive(Clone, Debug, Eq, PartialEq)]
18pub enum WorkerMessage {
19 ActivityTask(ProtoActivityTask),
21 DrainRequest,
23}
24
25type ActivityKey = (String, String);
26type WorkerMap = HashMap<WorkerId, WorkerHandle>;
27type RegistryMap = HashMap<ActivityKey, WorkerMap>;
28
29#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
31pub struct WorkerId(u64);
32
33impl WorkerId {
34 #[must_use]
37 pub const fn value(self) -> u64 {
38 self.0
39 }
40}
41
42#[derive(Clone, Debug)]
44pub struct WorkerHandle {
45 id: WorkerId,
46 namespace: String,
47 activity_types: BTreeSet<String>,
48 sender: WorkerTaskSender,
49}
50
51impl WorkerHandle {
52 #[must_use]
54 pub const fn id(&self) -> WorkerId {
55 self.id
56 }
57
58 #[must_use]
60 pub fn namespace(&self) -> &str {
61 &self.namespace
62 }
63
64 #[must_use]
66 pub fn activity_types(&self) -> &BTreeSet<String> {
67 &self.activity_types
68 }
69
70 #[must_use]
72 pub fn sender(&self) -> &WorkerTaskSender {
73 &self.sender
74 }
75}
76
77#[derive(Debug)]
78struct RegistryState {
79 next_worker_id: u64,
80 workers: BTreeMap<WorkerId, WorkerHandle>,
81 by_activity: RegistryMap,
82}
83
84impl Default for RegistryState {
85 fn default() -> Self {
86 Self {
87 next_worker_id: 1,
88 workers: BTreeMap::new(),
89 by_activity: HashMap::new(),
90 }
91 }
92}
93
94#[derive(Clone, Debug, Default)]
96pub struct ConnectedWorkerRegistry {
97 inner: Arc<Mutex<RegistryState>>,
98 metrics: Option<Metrics>,
99}
100
101impl ConnectedWorkerRegistry {
102 #[must_use]
104 pub fn with_metrics(metrics: Metrics) -> Self {
105 Self {
106 inner: Arc::new(Mutex::new(RegistryState::default())),
107 metrics: Some(metrics),
108 }
109 }
110
111 pub async fn accept_registration(
117 &self,
118 guard: &NamespaceGuard,
119 caller: &CallerIdentity,
120 registration: &ProtoRegisterWorker,
121 sender: WorkerTaskSender,
122 ) -> Result<WorkerRegistration, ServerError> {
123 let scoped = guard
124 .scope(caller, &NamespaceOperation::register_worker(registration))
125 .await?;
126 self.register(
127 scoped.namespace(),
128 registration.activity_types.iter(),
129 sender,
130 )
131 }
132
133 pub fn register<'a>(
139 &self,
140 namespace: impl Into<String>,
141 activity_types: impl IntoIterator<Item = &'a String>,
142 sender: WorkerTaskSender,
143 ) -> Result<WorkerRegistration, ServerError> {
144 let namespace = namespace.into();
145 let activity_types = activity_types.into_iter().cloned().collect::<BTreeSet<_>>();
146 let mut state = self.state()?;
147 let worker_id = WorkerId(state.next_worker_id);
148 state.next_worker_id = state.next_worker_id.saturating_add(1);
149
150 let handle = WorkerHandle {
151 id: worker_id,
152 namespace: namespace.clone(),
153 activity_types: activity_types.clone(),
154 sender,
155 };
156
157 for activity_type in &activity_types {
158 state
159 .by_activity
160 .entry((namespace.clone(), activity_type.clone()))
161 .or_default()
162 .insert(worker_id, handle.clone());
163 }
164 state.workers.insert(worker_id, handle);
165 drop(state);
166
167 if let Some(metrics) = &self.metrics {
168 metrics.worker_connected(&namespace);
169 }
170
171 Ok(WorkerRegistration {
172 registry: self.clone(),
173 parts: Some(WorkerRegistrationParts {
174 worker_id,
175 namespace,
176 activity_types,
177 }),
178 })
179 }
180
181 pub fn workers_for(
187 &self,
188 namespace: &str,
189 activity_type: &str,
190 ) -> Result<Vec<WorkerHandle>, ServerError> {
191 let state = self.state()?;
192 let key = (namespace.to_owned(), activity_type.to_owned());
193 Ok(state
194 .by_activity
195 .get(&key)
196 .map(|workers| workers.values().cloned().collect())
197 .unwrap_or_default())
198 }
199
200 pub fn all_workers(&self) -> Result<Vec<WorkerHandle>, ServerError> {
206 let state = self.state()?;
207 Ok(state.workers.values().cloned().collect())
208 }
209
210 pub fn broadcast_drain(&self) -> Result<usize, ServerError> {
216 let workers = self.all_workers()?;
217 let mut delivered = 0usize;
218 for worker in workers {
219 if worker
220 .sender()
221 .try_send(WorkerMessage::DrainRequest)
222 .is_ok()
223 {
224 delivered = delivered.saturating_add(1);
225 } else {
226 self.deregister(worker.id())?;
227 }
228 }
229 Ok(delivered)
230 }
231
232 pub fn select_worker(
238 &self,
239 namespace: &str,
240 activity_type: &str,
241 ) -> Result<Option<WorkerHandle>, ServerError> {
242 let state = self.state()?;
243 let key = (namespace.to_owned(), activity_type.to_owned());
244 Ok(state
245 .by_activity
246 .get(&key)
247 .and_then(|workers| workers.values().min_by_key(|worker| worker.id).cloned()))
248 }
249
250 pub fn is_registered(&self, worker_id: WorkerId) -> Result<bool, ServerError> {
261 Ok(self.state()?.workers.contains_key(&worker_id))
262 }
263
264 pub fn deregister(&self, worker_id: WorkerId) -> Result<(), ServerError> {
270 let mut state = self.state()?;
271 let removed_namespace = Self::remove_worker(&mut state, worker_id);
272 drop(state);
273
274 if let (Some(namespace), Some(metrics)) = (removed_namespace, &self.metrics) {
275 metrics.worker_disconnected(&namespace);
276 }
277
278 Ok(())
279 }
280
281 fn remove_worker(state: &mut RegistryState, worker_id: WorkerId) -> Option<String> {
282 let handle = state.workers.remove(&worker_id)?;
283 let namespace = handle.namespace.clone();
284
285 for activity_type in handle.activity_types {
286 let key = (handle.namespace.clone(), activity_type);
287 if let Some(workers) = state.by_activity.get_mut(&key) {
288 workers.remove(&worker_id);
289 if workers.is_empty() {
290 state.by_activity.remove(&key);
291 }
292 }
293 }
294
295 Some(namespace)
296 }
297
298 fn state(&self) -> Result<MutexGuard<'_, RegistryState>, ServerError> {
299 self.inner
300 .lock()
301 .map_err(|_| ServerError::lock_poisoned("connected worker registry"))
302 }
303}
304
305#[derive(Clone, Debug)]
306struct WorkerRegistrationParts {
307 worker_id: WorkerId,
308 namespace: String,
309 activity_types: BTreeSet<String>,
310}
311
312#[derive(Debug)]
317pub struct WorkerRegistration {
318 registry: ConnectedWorkerRegistry,
319 parts: Option<WorkerRegistrationParts>,
320}
321
322impl WorkerRegistration {
323 #[must_use]
325 pub fn worker_id(&self) -> Option<WorkerId> {
326 self.parts.as_ref().map(|parts| parts.worker_id)
327 }
328
329 #[must_use]
331 pub fn namespace(&self) -> Option<&str> {
332 self.parts.as_ref().map(|parts| parts.namespace.as_str())
333 }
334
335 #[must_use]
337 pub fn activity_types(&self) -> Option<&BTreeSet<String>> {
338 self.parts.as_ref().map(|parts| &parts.activity_types)
339 }
340
341 pub fn deregister(mut self) -> Result<(), ServerError> {
347 let Some(parts) = self.parts.take() else {
348 return Ok(());
349 };
350 self.registry.deregister(parts.worker_id)
351 }
352}
353
354impl Drop for WorkerRegistration {
355 fn drop(&mut self) {
356 let Some(parts) = self.parts.take() else {
357 return;
358 };
359 if let Ok(mut state) = self.registry.inner.lock() {
360 let removed_namespace =
361 ConnectedWorkerRegistry::remove_worker(&mut state, parts.worker_id);
362 if let (Some(namespace), Some(metrics)) = (removed_namespace, &self.registry.metrics) {
363 metrics.worker_disconnected(&namespace);
364 }
365 }
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use crate::config::NamespaceMode;
372 use crate::namespace::{NamespaceResolver, StaticScheduleNamespaces, StaticWorkflowNamespaces};
373
374 use super::*;
375
376 fn guard() -> NamespaceGuard {
377 NamespaceGuard::new(NamespaceResolver::authorization_only(
378 NamespaceMode::SharedEngine,
379 StaticWorkflowNamespaces::default(),
380 StaticScheduleNamespaces::default(),
381 ))
382 }
383
384 fn caller(namespace: &str) -> CallerIdentity {
385 CallerIdentity::new("worker", [namespace.to_owned()])
386 }
387
388 fn registration(namespace: &str, activity_types: &[&str]) -> ProtoRegisterWorker {
389 ProtoRegisterWorker {
390 namespace: namespace.to_owned(),
391 activity_types: activity_types
392 .iter()
393 .map(|value| (*value).to_owned())
394 .collect(),
395 }
396 }
397
398 #[tokio::test]
399 async fn register_and_deregister_are_namespace_isolated() -> Result<(), ServerError> {
400 let registry = ConnectedWorkerRegistry::default();
401 let (tenant_a_tx, _tenant_a_rx) = mpsc::channel(1);
402 let (tenant_b_tx, _tenant_b_rx) = mpsc::channel(1);
403
404 let tenant_a = registry
405 .accept_registration(
406 &guard(),
407 &caller("tenant-a"),
408 ®istration("tenant-a", &["charge", "charge"]),
409 tenant_a_tx,
410 )
411 .await?;
412 let tenant_b = registry
413 .accept_registration(
414 &guard(),
415 &caller("tenant-b"),
416 ®istration("tenant-b", &["charge"]),
417 tenant_b_tx,
418 )
419 .await?;
420
421 assert_eq!(registry.workers_for("tenant-a", "charge")?.len(), 1);
422 assert_eq!(registry.workers_for("tenant-b", "charge")?.len(), 1);
423 assert!(registry.workers_for("tenant-a", "missing")?.is_empty());
424
425 let tenant_a_id = tenant_a.worker_id();
426 tenant_a.deregister()?;
427
428 assert!(registry.workers_for("tenant-a", "charge")?.is_empty());
429 assert_eq!(registry.workers_for("tenant-b", "charge")?.len(), 1);
430 assert_ne!(tenant_a_id, tenant_b.worker_id());
431
432 tenant_b.deregister()?;
433 assert!(registry.workers_for("tenant-b", "charge")?.is_empty());
434 Ok(())
435 }
436
437 #[tokio::test]
438 async fn denied_namespace_is_not_registered() -> Result<(), ServerError> {
439 let registry = ConnectedWorkerRegistry::default();
440 let (tx, _rx) = mpsc::channel(1);
441 let denied = registry
442 .accept_registration(
443 &guard(),
444 &caller("tenant-a"),
445 ®istration("tenant-b", &["charge"]),
446 tx,
447 )
448 .await;
449
450 assert!(denied.is_err());
451 assert!(registry.workers_for("tenant-b", "charge")?.is_empty());
452 Ok(())
453 }
454}