1use crate::completion::CompletionBatcher;
2use crate::dispatcher::{ConcurrencyMode, Dispatcher, OverflowPool, QueueConfig};
3use crate::events::{BoxedUntypedEventHandler, JobEvent, UntypedJobEvent};
4use crate::executor::{BoxedWorker, JobError, JobExecutor, JobResult, Worker};
5use crate::heartbeat::HeartbeatService;
6use crate::maintenance::{MaintenanceService, RetentionPolicy};
7use crate::runtime::{InFlightMap, InFlightRegistry};
8use awa_model::admin::{
9 self, QueueRuntimeConfigSnapshot, QueueRuntimeMode, QueueRuntimeSnapshot, RateLimitSnapshot,
10 RuntimeSnapshotInput,
11};
12use awa_model::{JobArgs, PeriodicJob};
13use chrono::{DateTime, Utc};
14use serde::de::DeserializeOwned;
15use sqlx::PgPool;
16use std::any::{Any, TypeId};
17use std::collections::HashMap;
18use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
19use std::sync::Arc;
20use std::time::Duration;
21use tokio::sync::{Mutex, RwLock};
22use tokio::task::JoinSet;
23use tokio_util::sync::CancellationToken;
24use tracing::{info, warn};
25use uuid::Uuid;
26
27#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
29pub enum BuildError {
30 #[error("at least one queue must be configured")]
31 NoQueuesConfigured,
32 #[error("sum of min_workers ({total_min}) exceeds global_max_workers ({global_max})")]
33 MinWorkersExceedGlobal { total_min: u32, global_max: u32 },
34 #[error("rate_limit max_rate must be > 0.0")]
35 InvalidRateLimit,
36 #[error("queue weight must be > 0")]
37 InvalidWeight,
38 #[error("cleanup_batch_size must be > 0")]
39 InvalidBatchSize,
40}
41
42#[derive(Debug, Clone)]
44pub struct HealthCheck {
45 pub healthy: bool,
46 pub postgres_connected: bool,
47 pub poll_loop_alive: bool,
48 pub heartbeat_alive: bool,
49 pub maintenance_alive: bool,
50 pub shutting_down: bool,
51 pub leader: bool,
52 pub queues: HashMap<String, QueueHealth>,
53}
54
55#[derive(Debug, Clone)]
57pub struct QueueHealth {
58 pub in_flight: u32,
59 pub available: u64,
60 pub capacity: QueueCapacity,
62}
63
64#[derive(Debug, Clone)]
66pub enum QueueCapacity {
67 HardReserved { max_workers: u32 },
69 Weighted {
71 min_workers: u32,
72 weight: u32,
73 overflow_held: u32,
74 },
75}
76
77pub struct ClientBuilder {
79 pool: PgPool,
80 queues: Vec<(String, QueueConfig)>,
81 workers: HashMap<String, BoxedWorker>,
82 lifecycle_handlers: HashMap<String, Vec<BoxedUntypedEventHandler>>,
83 state: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
84 heartbeat_interval: Duration,
85 promote_interval: Duration,
86 heartbeat_rescue_interval: Option<Duration>,
87 deadline_rescue_interval: Option<Duration>,
88 callback_rescue_interval: Option<Duration>,
89 periodic_jobs: Vec<PeriodicJob>,
90 global_max_workers: Option<u32>,
91 leader_election_interval: Option<Duration>,
92 leader_check_interval: Option<Duration>,
93 completed_retention: Option<Duration>,
94 failed_retention: Option<Duration>,
95 cleanup_batch_size: Option<i64>,
96 cleanup_interval: Option<Duration>,
97 queue_retention_overrides: HashMap<String, RetentionPolicy>,
98 runtime_snapshot_interval: Duration,
99}
100
101impl ClientBuilder {
102 pub fn new(pool: PgPool) -> Self {
103 Self {
104 pool,
105 queues: Vec::new(),
106 workers: HashMap::new(),
107 lifecycle_handlers: HashMap::new(),
108 state: HashMap::new(),
109 heartbeat_interval: Duration::from_secs(30),
110 promote_interval: Duration::from_millis(250),
111 heartbeat_rescue_interval: None,
112 deadline_rescue_interval: None,
113 callback_rescue_interval: None,
114 periodic_jobs: Vec::new(),
115 global_max_workers: None,
116 leader_election_interval: None,
117 leader_check_interval: None,
118 completed_retention: None,
119 failed_retention: None,
120 cleanup_batch_size: None,
121 cleanup_interval: None,
122 queue_retention_overrides: HashMap::new(),
123 runtime_snapshot_interval: Duration::from_secs(10),
124 }
125 }
126
127 pub fn queue(mut self, name: impl Into<String>, config: QueueConfig) -> Self {
129 self.queues.push((name.into(), config));
130 self
131 }
132
133 pub fn register<T, F, Fut>(mut self, handler: F) -> Self
138 where
139 T: JobArgs + DeserializeOwned + Send + Sync + 'static,
140 F: Fn(T, &crate::context::JobContext) -> Fut + Send + Sync + 'static,
141 Fut: std::future::Future<Output = Result<JobResult, JobError>> + Send + Sync + 'static,
142 {
143 let kind = T::kind().to_string();
144 let worker = TypedWorker {
145 kind: T::kind(),
146 handler: Arc::new(handler),
147 _phantom: std::marker::PhantomData,
148 };
149 self.workers.insert(kind, Box::new(worker));
150 self
151 }
152
153 pub fn on_event<T, F, Fut>(mut self, handler: F) -> Self
159 where
160 T: JobArgs + DeserializeOwned + Send + Sync + 'static,
161 F: Fn(JobEvent<T>) -> Fut + Send + Sync + 'static,
162 Fut: std::future::Future<Output = ()> + Send + 'static,
163 {
164 let kind = T::kind().to_string();
165 let handler = Arc::new(handler);
166 let erased: BoxedUntypedEventHandler = Arc::new(move |event: UntypedJobEvent| {
167 let handler = handler.clone();
168 Box::pin(async move {
169 let args: T = match serde_json::from_value(event.job().args.clone()) {
170 Ok(args) => args,
171 Err(err) => {
172 warn!(
173 job_id = event.job().id,
174 kind = %event.job().kind,
175 error = %err,
176 "Failed to deserialize args for lifecycle event handler"
177 );
178 return;
179 }
180 };
181
182 (handler)(event.into_typed(args)).await;
183 })
184 });
185 self.lifecycle_handlers
186 .entry(kind)
187 .or_default()
188 .push(erased);
189 self
190 }
191
192 pub fn on_event_kind<F, Fut>(mut self, kind: impl Into<String>, handler: F) -> Self
197 where
198 F: Fn(UntypedJobEvent) -> Fut + Send + Sync + 'static,
199 Fut: std::future::Future<Output = ()> + Send + 'static,
200 {
201 let kind = kind.into();
202 let handler = Arc::new(handler);
203 let erased: BoxedUntypedEventHandler = Arc::new(move |event: UntypedJobEvent| {
204 let handler = handler.clone();
205 Box::pin(async move {
206 (handler)(event).await;
207 })
208 });
209 self.lifecycle_handlers
210 .entry(kind)
211 .or_default()
212 .push(erased);
213 self
214 }
215
216 pub fn register_worker(mut self, worker: impl Worker + 'static) -> Self {
218 let kind = worker.kind().to_string();
219 self.workers.insert(kind, Box::new(worker));
220 self
221 }
222
223 pub fn state<T: Any + Send + Sync + Clone>(mut self, value: T) -> Self {
225 self.state.insert(TypeId::of::<T>(), Box::new(value));
226 self
227 }
228
229 pub fn heartbeat_interval(mut self, interval: Duration) -> Self {
231 self.heartbeat_interval = interval;
232 self
233 }
234
235 pub fn promote_interval(mut self, interval: Duration) -> Self {
237 self.promote_interval = interval;
238 self
239 }
240
241 pub fn heartbeat_rescue_interval(mut self, interval: Duration) -> Self {
243 self.heartbeat_rescue_interval = Some(interval);
244 self
245 }
246
247 pub fn deadline_rescue_interval(mut self, interval: Duration) -> Self {
249 self.deadline_rescue_interval = Some(interval);
250 self
251 }
252
253 pub fn callback_rescue_interval(mut self, interval: Duration) -> Self {
255 self.callback_rescue_interval = Some(interval);
256 self
257 }
258
259 pub fn leader_election_interval(mut self, interval: Duration) -> Self {
264 self.leader_election_interval = Some(interval);
265 self
266 }
267
268 pub fn leader_check_interval(mut self, interval: Duration) -> Self {
270 self.leader_check_interval = Some(interval);
271 self
272 }
273
274 pub fn global_max_workers(mut self, max: u32) -> Self {
279 self.global_max_workers = Some(max);
280 self
281 }
282
283 pub fn completed_retention(mut self, retention: Duration) -> Self {
285 self.completed_retention = Some(retention);
286 self
287 }
288
289 pub fn failed_retention(mut self, retention: Duration) -> Self {
291 self.failed_retention = Some(retention);
292 self
293 }
294
295 pub fn cleanup_batch_size(mut self, batch_size: i64) -> Self {
297 self.cleanup_batch_size = Some(batch_size);
298 self
299 }
300
301 pub fn cleanup_interval(mut self, interval: Duration) -> Self {
303 self.cleanup_interval = Some(interval);
304 self
305 }
306
307 pub fn queue_retention(mut self, queue: impl Into<String>, policy: RetentionPolicy) -> Self {
309 self.queue_retention_overrides.insert(queue.into(), policy);
310 self
311 }
312
313 pub fn runtime_snapshot_interval(mut self, interval: Duration) -> Self {
315 self.runtime_snapshot_interval = interval;
316 self
317 }
318
319 pub fn periodic(mut self, job: PeriodicJob) -> Self {
324 self.periodic_jobs.push(job);
325 self
326 }
327
328 pub fn build(self) -> Result<Client, BuildError> {
330 if self.queues.is_empty() {
331 return Err(BuildError::NoQueuesConfigured);
332 }
333
334 for (_, config) in &self.queues {
336 if let Some(rl) = &config.rate_limit {
337 if rl.max_rate <= 0.0 {
338 return Err(BuildError::InvalidRateLimit);
339 }
340 }
341 if config.weight == 0 {
342 return Err(BuildError::InvalidWeight);
343 }
344 }
345
346 if let Some(bs) = self.cleanup_batch_size {
348 if bs <= 0 {
349 return Err(BuildError::InvalidBatchSize);
350 }
351 }
352
353 let overflow_pool = if let Some(global_max) = self.global_max_workers {
355 let total_min: u32 = self.queues.iter().map(|(_, c)| c.min_workers).sum();
356 if total_min > global_max {
357 return Err(BuildError::MinWorkersExceedGlobal {
358 total_min,
359 global_max,
360 });
361 }
362 let overflow_capacity = global_max - total_min;
363 let weights: HashMap<String, u32> = self
364 .queues
365 .iter()
366 .map(|(name, c)| (name.clone(), c.weight.max(1)))
367 .collect();
368 Some(Arc::new(OverflowPool::new(overflow_capacity, weights)))
369 } else {
370 None
371 };
372
373 let metrics = crate::metrics::AwaMetrics::from_global();
374 let queue_in_flight = Arc::new(
375 self.queues
376 .iter()
377 .map(|(name, _)| (name.clone(), Arc::new(AtomicU32::new(0))))
378 .collect(),
379 );
380 let dispatcher_alive = Arc::new(
381 self.queues
382 .iter()
383 .map(|(name, _)| (name.clone(), Arc::new(AtomicBool::new(false))))
384 .collect(),
385 );
386
387 Ok(Client {
388 pool: self.pool,
389 queues: self.queues,
390 workers: Arc::new(self.workers),
391 lifecycle_handlers: Arc::new(self.lifecycle_handlers),
392 state: Arc::new(self.state),
393 heartbeat_interval: self.heartbeat_interval,
394 promote_interval: self.promote_interval,
395 heartbeat_rescue_interval: self.heartbeat_rescue_interval,
396 deadline_rescue_interval: self.deadline_rescue_interval,
397 callback_rescue_interval: self.callback_rescue_interval,
398 periodic_jobs: Arc::new(self.periodic_jobs),
399 dispatch_cancel: CancellationToken::new(),
400 service_cancel: CancellationToken::new(),
401 dispatcher_handles: RwLock::new(Vec::new()),
402 service_handles: RwLock::new(Vec::new()),
403 job_set: Arc::new(Mutex::new(JoinSet::new())),
404 in_flight: Arc::new(InFlightRegistry::default()),
405 queue_in_flight,
406 dispatcher_alive,
407 heartbeat_alive: Arc::new(AtomicBool::new(false)),
408 maintenance_alive: Arc::new(AtomicBool::new(false)),
409 leader: Arc::new(AtomicBool::new(false)),
410 overflow_pool,
411 metrics,
412 leader_election_interval: self.leader_election_interval,
413 leader_check_interval: self.leader_check_interval,
414 completed_retention: self.completed_retention,
415 failed_retention: self.failed_retention,
416 cleanup_batch_size: self.cleanup_batch_size,
417 cleanup_interval: self.cleanup_interval,
418 queue_retention_overrides: self.queue_retention_overrides,
419 global_max_workers: self.global_max_workers,
420 runtime_snapshot_interval: self.runtime_snapshot_interval,
421 runtime_instance_id: Uuid::new_v4(),
422 runtime_started_at: Utc::now(),
423 runtime_hostname: std::env::var("HOSTNAME").ok(),
424 runtime_pid: std::process::id() as i32,
425 runtime_version: env!("CARGO_PKG_VERSION"),
426 })
427 }
428}
429
430struct TypedWorker<T, F, Fut>
432where
433 T: JobArgs + DeserializeOwned + Send + Sync + 'static,
434 F: Fn(T, &crate::context::JobContext) -> Fut + Send + Sync + 'static,
435 Fut: std::future::Future<Output = Result<JobResult, JobError>> + Send + Sync + 'static,
436{
437 kind: &'static str,
438 handler: Arc<F>,
439 _phantom: std::marker::PhantomData<fn() -> (T, Fut)>,
440}
441
442#[async_trait::async_trait]
443impl<T, F, Fut> Worker for TypedWorker<T, F, Fut>
444where
445 T: JobArgs + DeserializeOwned + Send + Sync + 'static,
446 F: Fn(T, &crate::context::JobContext) -> Fut + Send + Sync + 'static,
447 Fut: std::future::Future<Output = Result<JobResult, JobError>> + Send + Sync + 'static,
448{
449 fn kind(&self) -> &'static str {
450 self.kind
451 }
452
453 async fn perform(&self, ctx: &crate::context::JobContext) -> Result<JobResult, JobError> {
454 let args: T = serde_json::from_value(ctx.job.args.clone())
455 .map_err(|err| JobError::Terminal(format!("failed to deserialize args: {}", err)))?;
456
457 (self.handler)(args, ctx).await
458 }
459}
460
461pub struct Client {
463 pool: PgPool,
464 queues: Vec<(String, QueueConfig)>,
465 workers: Arc<HashMap<String, BoxedWorker>>,
466 lifecycle_handlers: Arc<HashMap<String, Vec<BoxedUntypedEventHandler>>>,
467 state: Arc<HashMap<TypeId, Box<dyn Any + Send + Sync>>>,
468 heartbeat_interval: Duration,
469 promote_interval: Duration,
470 heartbeat_rescue_interval: Option<Duration>,
471 deadline_rescue_interval: Option<Duration>,
472 callback_rescue_interval: Option<Duration>,
473 periodic_jobs: Arc<Vec<PeriodicJob>>,
474 dispatch_cancel: CancellationToken,
476 service_cancel: CancellationToken,
478 dispatcher_handles: RwLock<Vec<tokio::task::JoinHandle<()>>>,
480 service_handles: RwLock<Vec<tokio::task::JoinHandle<()>>>,
482 job_set: Arc<Mutex<JoinSet<()>>>,
484 in_flight: InFlightMap,
485 queue_in_flight: Arc<HashMap<String, Arc<AtomicU32>>>,
486 dispatcher_alive: Arc<HashMap<String, Arc<AtomicBool>>>,
487 heartbeat_alive: Arc<AtomicBool>,
488 maintenance_alive: Arc<AtomicBool>,
489 leader: Arc<AtomicBool>,
490 overflow_pool: Option<Arc<OverflowPool>>,
492 metrics: crate::metrics::AwaMetrics,
493 leader_election_interval: Option<Duration>,
494 leader_check_interval: Option<Duration>,
495 completed_retention: Option<Duration>,
496 failed_retention: Option<Duration>,
497 cleanup_batch_size: Option<i64>,
498 cleanup_interval: Option<Duration>,
499 queue_retention_overrides: HashMap<String, RetentionPolicy>,
500 global_max_workers: Option<u32>,
501 runtime_snapshot_interval: Duration,
502 runtime_instance_id: Uuid,
503 runtime_started_at: DateTime<Utc>,
504 runtime_hostname: Option<String>,
505 runtime_pid: i32,
506 runtime_version: &'static str,
507}
508
509#[derive(Clone)]
510struct RuntimeReporterState {
511 pool: PgPool,
512 queues: Vec<(String, QueueConfig)>,
513 queue_in_flight: Arc<HashMap<String, Arc<AtomicU32>>>,
514 dispatcher_alive: Arc<HashMap<String, Arc<AtomicBool>>>,
515 heartbeat_alive: Arc<AtomicBool>,
516 maintenance_alive: Arc<AtomicBool>,
517 leader: Arc<AtomicBool>,
518 dispatch_cancel: CancellationToken,
519 overflow_pool: Option<Arc<OverflowPool>>,
520 global_max_workers: Option<u32>,
521 instance_id: Uuid,
522 started_at: DateTime<Utc>,
523 hostname: Option<String>,
524 pid: i32,
525 version: &'static str,
526 snapshot_interval: Duration,
527}
528
529impl Client {
530 pub fn builder(pool: PgPool) -> ClientBuilder {
532 ClientBuilder::new(pool)
533 }
534
535 fn runtime_reporter_state(&self) -> RuntimeReporterState {
536 RuntimeReporterState {
537 pool: self.pool.clone(),
538 queues: self.queues.clone(),
539 queue_in_flight: self.queue_in_flight.clone(),
540 dispatcher_alive: self.dispatcher_alive.clone(),
541 heartbeat_alive: self.heartbeat_alive.clone(),
542 maintenance_alive: self.maintenance_alive.clone(),
543 leader: self.leader.clone(),
544 dispatch_cancel: self.dispatch_cancel.clone(),
545 overflow_pool: self.overflow_pool.clone(),
546 global_max_workers: self.global_max_workers,
547 instance_id: self.runtime_instance_id,
548 started_at: self.runtime_started_at,
549 hostname: self.runtime_hostname.clone(),
550 pid: self.runtime_pid,
551 version: self.runtime_version,
552 snapshot_interval: self.runtime_snapshot_interval,
553 }
554 }
555
556 async fn publish_runtime_snapshot(&self) {
557 let reporter = self.runtime_reporter_state();
558 reporter.publish_snapshot().await;
559 }
560
561 pub async fn start(&self) -> Result<(), awa_model::AwaError> {
563 info!(
564 queues = self.queues.len(),
565 workers = self.workers.len(),
566 "Starting Awa worker runtime"
567 );
568
569 let (completion_batcher, completion_handle) = CompletionBatcher::new(
572 self.pool.clone(),
573 self.service_cancel.clone(),
574 self.metrics.clone(),
575 );
576
577 let executor = Arc::new(JobExecutor::new(
579 self.pool.clone(),
580 self.workers.clone(),
581 self.lifecycle_handlers.clone(),
582 self.in_flight.clone(),
583 self.queue_in_flight.clone(),
584 self.state.clone(),
585 self.metrics.clone(),
586 completion_handle,
587 ));
588
589 let mut service_handles = self.service_handles.write().await;
590
591 service_handles.extend(completion_batcher.spawn());
592
593 let heartbeat = HeartbeatService::new(
595 self.pool.clone(),
596 self.in_flight.clone(),
597 self.heartbeat_interval,
598 self.heartbeat_alive.clone(),
599 self.service_cancel.clone(),
600 self.metrics.clone(),
601 );
602 service_handles.push(tokio::spawn(async move {
603 heartbeat.run().await;
604 }));
605
606 let mut maintenance = MaintenanceService::new(
608 self.pool.clone(),
609 self.metrics.clone(),
610 self.leader.clone(),
611 self.maintenance_alive.clone(),
612 self.service_cancel.clone(),
613 self.periodic_jobs.clone(),
614 self.in_flight.clone(),
615 )
616 .promote_interval(self.promote_interval);
617 if let Some(interval) = self.heartbeat_rescue_interval {
618 maintenance = maintenance.heartbeat_rescue_interval(interval);
619 }
620 if let Some(interval) = self.deadline_rescue_interval {
621 maintenance = maintenance.deadline_rescue_interval(interval);
622 }
623 if let Some(interval) = self.callback_rescue_interval {
624 maintenance = maintenance.callback_rescue_interval(interval);
625 }
626 if let Some(interval) = self.leader_election_interval {
627 maintenance = maintenance.leader_election_interval(interval);
628 }
629 if let Some(interval) = self.leader_check_interval {
630 maintenance = maintenance.leader_check_interval(interval);
631 }
632 if let Some(retention) = self.completed_retention {
633 maintenance = maintenance.completed_retention(retention);
634 }
635 if let Some(retention) = self.failed_retention {
636 maintenance = maintenance.failed_retention(retention);
637 }
638 if let Some(batch_size) = self.cleanup_batch_size {
639 maintenance = maintenance.cleanup_batch_size(batch_size);
640 }
641 if let Some(interval) = self.cleanup_interval {
642 maintenance = maintenance.cleanup_interval(interval);
643 }
644 if !self.queue_retention_overrides.is_empty() {
645 maintenance =
646 maintenance.queue_retention_overrides(self.queue_retention_overrides.clone());
647 }
648 service_handles.push(tokio::spawn(async move {
649 maintenance.run().await;
650 }));
651
652 let mut dispatcher_handles = self.dispatcher_handles.write().await;
654 for (queue_name, config) in &self.queues {
655 let alive = self
656 .dispatcher_alive
657 .get(queue_name)
658 .cloned()
659 .unwrap_or_else(|| Arc::new(AtomicBool::new(false)));
660
661 let dispatcher = if let Some(overflow_pool) = &self.overflow_pool {
662 let concurrency = ConcurrencyMode::Weighted {
664 local_semaphore: Arc::new(tokio::sync::Semaphore::new(
665 config.min_workers as usize,
666 )),
667 overflow_pool: overflow_pool.clone(),
668 queue_name: queue_name.clone(),
669 };
670 Dispatcher::with_concurrency(
671 queue_name.clone(),
672 config.clone(),
673 self.pool.clone(),
674 executor.clone(),
675 self.metrics.clone(),
676 self.in_flight.clone(),
677 alive,
678 self.dispatch_cancel.clone(),
679 self.job_set.clone(),
680 concurrency,
681 )
682 } else {
683 Dispatcher::new(
685 queue_name.clone(),
686 config.clone(),
687 self.pool.clone(),
688 executor.clone(),
689 self.metrics.clone(),
690 self.in_flight.clone(),
691 alive,
692 self.dispatch_cancel.clone(),
693 self.job_set.clone(),
694 )
695 };
696 dispatcher_handles.push(tokio::spawn(async move {
697 dispatcher.run().await;
698 }));
699 }
700
701 self.publish_runtime_snapshot().await;
702
703 let reporter = self.runtime_reporter_state();
704 service_handles.push(tokio::spawn(async move {
705 reporter.run().await;
706 }));
707
708 info!("Awa worker runtime started");
709 Ok(())
710 }
711
712 pub async fn shutdown(&self, timeout: Duration) {
721 info!("Initiating graceful shutdown");
722
723 self.dispatch_cancel.cancel();
725
726 self.publish_runtime_snapshot().await;
727
728 for flag in self.in_flight.flags() {
730 flag.store(true, Ordering::SeqCst);
731 }
732
733 let dispatcher_handles: Vec<_> = {
735 let mut guard = self.dispatcher_handles.write().await;
736 std::mem::take(&mut *guard)
737 };
738 for handle in dispatcher_handles {
739 let _ = handle.await;
740 }
741
742 let drain = async {
744 let mut set = self.job_set.lock().await;
745 while set.join_next().await.is_some() {}
746 };
747 if tokio::time::timeout(timeout, drain).await.is_err() {
748 warn!(
749 timeout_secs = timeout.as_secs(),
750 "Shutdown drain timeout exceeded, some jobs may not have completed"
751 );
752 }
753
754 self.service_cancel.cancel();
756 let service_handles: Vec<_> = {
757 let mut guard = self.service_handles.write().await;
758 std::mem::take(&mut *guard)
759 };
760 for handle in service_handles {
761 let _ = handle.await;
762 }
763
764 info!("Awa worker runtime stopped");
765 }
766
767 pub fn pool(&self) -> &PgPool {
769 &self.pool
770 }
771
772 pub async fn health_check(&self) -> HealthCheck {
774 let postgres_connected = sqlx::query("SELECT 1").execute(&self.pool).await.is_ok();
775 let poll_loop_alive = self
776 .dispatcher_alive
777 .values()
778 .all(|alive| alive.load(Ordering::SeqCst));
779 let heartbeat_alive = self.heartbeat_alive.load(Ordering::SeqCst);
780 let maintenance_alive = self.maintenance_alive.load(Ordering::SeqCst);
781 let shutting_down = self.dispatch_cancel.is_cancelled();
782 let leader = self.leader.load(Ordering::SeqCst);
783 let available_rows = sqlx::query_as::<_, (String, i64)>(
784 r#"
785 SELECT queue, count(*)::bigint AS available
786 FROM awa.jobs_hot
787 WHERE state = 'available'
788 GROUP BY queue
789 "#,
790 )
791 .fetch_all(&self.pool)
792 .await
793 .unwrap_or_default();
794 let available_by_queue: HashMap<_, _> = available_rows.into_iter().collect();
795 let queues = self
796 .queues
797 .iter()
798 .map(|(queue, config)| {
799 let in_flight = self
800 .queue_in_flight
801 .get(queue)
802 .map(|counter| counter.load(Ordering::SeqCst))
803 .unwrap_or(0);
804 let available = available_by_queue.get(queue).copied().unwrap_or(0).max(0) as u64;
805 let capacity = if let Some(overflow_pool) = &self.overflow_pool {
806 QueueCapacity::Weighted {
807 min_workers: config.min_workers,
808 weight: config.weight,
809 overflow_held: overflow_pool.held(queue),
810 }
811 } else {
812 QueueCapacity::HardReserved {
813 max_workers: config.max_workers,
814 }
815 };
816 (
817 queue.clone(),
818 QueueHealth {
819 in_flight,
820 available,
821 capacity,
822 },
823 )
824 })
825 .collect();
826
827 HealthCheck {
828 healthy: postgres_connected
829 && poll_loop_alive
830 && heartbeat_alive
831 && maintenance_alive
832 && !shutting_down,
833 postgres_connected,
834 poll_loop_alive,
835 heartbeat_alive,
836 maintenance_alive,
837 shutting_down,
838 leader,
839 queues,
840 }
841 }
842}
843
844impl RuntimeReporterState {
845 fn queue_snapshot(&self, queue: &str, config: &QueueConfig) -> QueueRuntimeSnapshot {
846 let in_flight = self
847 .queue_in_flight
848 .get(queue)
849 .map(|counter| counter.load(Ordering::SeqCst))
850 .unwrap_or(0);
851
852 let (mode, max_workers, min_workers, weight, overflow_held) =
853 if let Some(overflow_pool) = &self.overflow_pool {
854 (
855 QueueRuntimeMode::Weighted,
856 None,
857 Some(config.min_workers),
858 Some(config.weight),
859 Some(overflow_pool.held(queue)),
860 )
861 } else {
862 (
863 QueueRuntimeMode::HardReserved,
864 Some(config.max_workers),
865 None,
866 None,
867 None,
868 )
869 };
870
871 QueueRuntimeSnapshot {
872 queue: queue.to_string(),
873 in_flight,
874 overflow_held,
875 config: QueueRuntimeConfigSnapshot {
876 mode,
877 max_workers,
878 min_workers,
879 weight,
880 global_max_workers: self.global_max_workers,
881 poll_interval_ms: config.poll_interval.as_millis() as u64,
882 deadline_duration_secs: config.deadline_duration.as_secs(),
883 priority_aging_interval_secs: config.priority_aging_interval.as_secs(),
884 rate_limit: config.rate_limit.as_ref().map(|rl| RateLimitSnapshot {
885 max_rate: rl.max_rate,
886 burst: rl.burst,
887 }),
888 },
889 }
890 }
891
892 async fn snapshot_input(&self) -> RuntimeSnapshotInput {
893 let postgres_connected = sqlx::query("SELECT 1").execute(&self.pool).await.is_ok();
894 let poll_loop_alive = self
895 .dispatcher_alive
896 .values()
897 .all(|alive| alive.load(Ordering::SeqCst));
898 let heartbeat_alive = self.heartbeat_alive.load(Ordering::SeqCst);
899 let maintenance_alive = self.maintenance_alive.load(Ordering::SeqCst);
900 let shutting_down = self.dispatch_cancel.is_cancelled();
901 let leader = self.leader.load(Ordering::SeqCst);
902 let healthy = postgres_connected
903 && poll_loop_alive
904 && heartbeat_alive
905 && maintenance_alive
906 && !shutting_down;
907 let queues = self
908 .queues
909 .iter()
910 .map(|(queue, config)| self.queue_snapshot(queue, config))
911 .collect();
912
913 RuntimeSnapshotInput {
914 instance_id: self.instance_id,
915 hostname: self.hostname.clone(),
916 pid: self.pid,
917 version: self.version.to_string(),
918 started_at: self.started_at,
919 snapshot_interval_ms: self.snapshot_interval.as_millis() as i64,
920 healthy,
921 postgres_connected,
922 poll_loop_alive,
923 heartbeat_alive,
924 maintenance_alive,
925 shutting_down,
926 leader,
927 global_max_workers: self.global_max_workers,
928 queues,
929 }
930 }
931
932 async fn publish_snapshot(&self) {
933 let snapshot = self.snapshot_input().await;
934 if let Err(err) = admin::upsert_runtime_snapshot(&self.pool, &snapshot).await {
935 warn!(error = %err, "Failed to publish runtime snapshot");
936 }
937 }
938
939 async fn run(self) {
940 let mut interval = tokio::time::interval(self.snapshot_interval);
941 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
942 interval.tick().await;
943 loop {
944 tokio::select! {
945 _ = self.dispatch_cancel.cancelled() => {
946 self.publish_snapshot().await;
947 break;
948 }
949 _ = interval.tick() => {
950 self.publish_snapshot().await;
951 }
952 }
953 }
954 }
955}