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 heartbeat_staleness: Option<Duration>,
88 deadline_rescue_interval: Option<Duration>,
89 callback_rescue_interval: Option<Duration>,
90 periodic_jobs: Vec<PeriodicJob>,
91 global_max_workers: Option<u32>,
92 leader_election_interval: Option<Duration>,
93 leader_check_interval: Option<Duration>,
94 completed_retention: Option<Duration>,
95 failed_retention: Option<Duration>,
96 cleanup_batch_size: Option<i64>,
97 cleanup_interval: Option<Duration>,
98 queue_retention_overrides: HashMap<String, RetentionPolicy>,
99 runtime_snapshot_interval: Duration,
100 queue_stats_interval: Option<Duration>,
101}
102
103impl ClientBuilder {
104 pub fn new(pool: PgPool) -> Self {
105 Self {
106 pool,
107 queues: Vec::new(),
108 workers: HashMap::new(),
109 lifecycle_handlers: HashMap::new(),
110 state: HashMap::new(),
111 heartbeat_interval: Duration::from_secs(30),
112 promote_interval: Duration::from_millis(250),
113 heartbeat_rescue_interval: None,
114 heartbeat_staleness: None,
115 deadline_rescue_interval: None,
116 callback_rescue_interval: None,
117 periodic_jobs: Vec::new(),
118 global_max_workers: None,
119 leader_election_interval: None,
120 leader_check_interval: None,
121 completed_retention: None,
122 failed_retention: None,
123 cleanup_batch_size: None,
124 cleanup_interval: None,
125 queue_retention_overrides: HashMap::new(),
126 runtime_snapshot_interval: Duration::from_secs(10),
127 queue_stats_interval: None,
128 }
129 }
130
131 pub fn queue(mut self, name: impl Into<String>, config: QueueConfig) -> Self {
133 self.queues.push((name.into(), config));
134 self
135 }
136
137 pub fn register<T, F, Fut>(mut self, handler: F) -> Self
142 where
143 T: JobArgs + DeserializeOwned + Send + Sync + 'static,
144 F: Fn(T, &crate::context::JobContext) -> Fut + Send + Sync + 'static,
145 Fut: std::future::Future<Output = Result<JobResult, JobError>> + Send + Sync + 'static,
146 {
147 let kind = T::kind().to_string();
148 let worker = TypedWorker {
149 kind: T::kind(),
150 handler: Arc::new(handler),
151 _phantom: std::marker::PhantomData,
152 };
153 self.workers.insert(kind, Box::new(worker));
154 self
155 }
156
157 pub fn on_event<T, F, Fut>(mut self, handler: F) -> Self
163 where
164 T: JobArgs + DeserializeOwned + Send + Sync + 'static,
165 F: Fn(JobEvent<T>) -> Fut + Send + Sync + 'static,
166 Fut: std::future::Future<Output = ()> + Send + 'static,
167 {
168 let kind = T::kind().to_string();
169 let handler = Arc::new(handler);
170 let erased: BoxedUntypedEventHandler = Arc::new(move |event: UntypedJobEvent| {
171 let handler = handler.clone();
172 Box::pin(async move {
173 let args: T = match serde_json::from_value(event.job().args.clone()) {
174 Ok(args) => args,
175 Err(err) => {
176 warn!(
177 job_id = event.job().id,
178 kind = %event.job().kind,
179 error = %err,
180 "Failed to deserialize args for lifecycle event handler"
181 );
182 return;
183 }
184 };
185
186 (handler)(event.into_typed(args)).await;
187 })
188 });
189 self.lifecycle_handlers
190 .entry(kind)
191 .or_default()
192 .push(erased);
193 self
194 }
195
196 pub fn on_event_kind<F, Fut>(mut self, kind: impl Into<String>, handler: F) -> Self
201 where
202 F: Fn(UntypedJobEvent) -> Fut + Send + Sync + 'static,
203 Fut: std::future::Future<Output = ()> + Send + 'static,
204 {
205 let kind = kind.into();
206 let handler = Arc::new(handler);
207 let erased: BoxedUntypedEventHandler = Arc::new(move |event: UntypedJobEvent| {
208 let handler = handler.clone();
209 Box::pin(async move {
210 (handler)(event).await;
211 })
212 });
213 self.lifecycle_handlers
214 .entry(kind)
215 .or_default()
216 .push(erased);
217 self
218 }
219
220 pub fn register_worker(mut self, worker: impl Worker + 'static) -> Self {
222 let kind = worker.kind().to_string();
223 self.workers.insert(kind, Box::new(worker));
224 self
225 }
226
227 #[cfg(feature = "http-worker")]
234 pub fn http_worker(
235 self,
236 kind: impl Into<String>,
237 config: crate::http_worker::HttpWorkerConfig,
238 ) -> Self {
239 let worker = crate::http_worker::HttpWorker::new(kind.into(), config);
240 self.register_worker(worker)
241 }
242
243 pub fn state<T: Any + Send + Sync + Clone>(mut self, value: T) -> Self {
245 self.state.insert(TypeId::of::<T>(), Box::new(value));
246 self
247 }
248
249 pub fn heartbeat_interval(mut self, interval: Duration) -> Self {
251 self.heartbeat_interval = interval;
252 self
253 }
254
255 pub fn promote_interval(mut self, interval: Duration) -> Self {
257 self.promote_interval = interval;
258 self
259 }
260
261 pub fn heartbeat_rescue_interval(mut self, interval: Duration) -> Self {
263 self.heartbeat_rescue_interval = Some(interval);
264 self
265 }
266
267 pub fn heartbeat_staleness(mut self, staleness: Duration) -> Self {
271 self.heartbeat_staleness = Some(staleness);
272 self
273 }
274
275 pub fn deadline_rescue_interval(mut self, interval: Duration) -> Self {
277 self.deadline_rescue_interval = Some(interval);
278 self
279 }
280
281 pub fn callback_rescue_interval(mut self, interval: Duration) -> Self {
283 self.callback_rescue_interval = Some(interval);
284 self
285 }
286
287 pub fn leader_election_interval(mut self, interval: Duration) -> Self {
292 self.leader_election_interval = Some(interval);
293 self
294 }
295
296 pub fn leader_check_interval(mut self, interval: Duration) -> Self {
298 self.leader_check_interval = Some(interval);
299 self
300 }
301
302 pub fn global_max_workers(mut self, max: u32) -> Self {
307 self.global_max_workers = Some(max);
308 self
309 }
310
311 pub fn completed_retention(mut self, retention: Duration) -> Self {
313 self.completed_retention = Some(retention);
314 self
315 }
316
317 pub fn failed_retention(mut self, retention: Duration) -> Self {
319 self.failed_retention = Some(retention);
320 self
321 }
322
323 pub fn cleanup_batch_size(mut self, batch_size: i64) -> Self {
325 self.cleanup_batch_size = Some(batch_size);
326 self
327 }
328
329 pub fn cleanup_interval(mut self, interval: Duration) -> Self {
331 self.cleanup_interval = Some(interval);
332 self
333 }
334
335 pub fn queue_retention(mut self, queue: impl Into<String>, policy: RetentionPolicy) -> Self {
337 self.queue_retention_overrides.insert(queue.into(), policy);
338 self
339 }
340
341 pub fn runtime_snapshot_interval(mut self, interval: Duration) -> Self {
343 self.runtime_snapshot_interval = interval;
344 self
345 }
346
347 pub fn queue_stats_interval(mut self, interval: Duration) -> Self {
349 self.queue_stats_interval = Some(interval);
350 self
351 }
352
353 pub fn periodic(mut self, job: PeriodicJob) -> Self {
358 self.periodic_jobs.push(job);
359 self
360 }
361
362 pub fn build(self) -> Result<Client, BuildError> {
364 if self.queues.is_empty() {
365 return Err(BuildError::NoQueuesConfigured);
366 }
367
368 for (_, config) in &self.queues {
370 if let Some(rl) = &config.rate_limit {
371 if rl.max_rate <= 0.0 {
372 return Err(BuildError::InvalidRateLimit);
373 }
374 }
375 if config.weight == 0 {
376 return Err(BuildError::InvalidWeight);
377 }
378 }
379
380 if let Some(bs) = self.cleanup_batch_size {
382 if bs <= 0 {
383 return Err(BuildError::InvalidBatchSize);
384 }
385 }
386
387 let overflow_pool = if let Some(global_max) = self.global_max_workers {
389 let total_min: u32 = self.queues.iter().map(|(_, c)| c.min_workers).sum();
390 if total_min > global_max {
391 return Err(BuildError::MinWorkersExceedGlobal {
392 total_min,
393 global_max,
394 });
395 }
396 let overflow_capacity = global_max - total_min;
397 let weights: HashMap<String, u32> = self
398 .queues
399 .iter()
400 .map(|(name, c)| (name.clone(), c.weight.max(1)))
401 .collect();
402 Some(Arc::new(OverflowPool::new(overflow_capacity, weights)))
403 } else {
404 None
405 };
406
407 if let Some(staleness) = self.heartbeat_staleness {
409 let min_safe = self.heartbeat_interval * 3;
410 if staleness < min_safe {
411 tracing::warn!(
412 heartbeat_staleness_ms = staleness.as_millis() as u64,
413 heartbeat_interval_ms = self.heartbeat_interval.as_millis() as u64,
414 recommended_min_ms = min_safe.as_millis() as u64,
415 "heartbeat_staleness ({:?}) is less than 3× heartbeat_interval ({:?}); \
416 this may cause false rescues of jobs that are still running",
417 staleness,
418 self.heartbeat_interval,
419 );
420 }
421 }
422
423 let metrics = crate::metrics::AwaMetrics::from_global();
424 let queue_in_flight = Arc::new(
425 self.queues
426 .iter()
427 .map(|(name, _)| (name.clone(), Arc::new(AtomicU32::new(0))))
428 .collect(),
429 );
430 let dispatcher_alive = Arc::new(
431 self.queues
432 .iter()
433 .map(|(name, _)| (name.clone(), Arc::new(AtomicBool::new(false))))
434 .collect(),
435 );
436
437 Ok(Client {
438 pool: self.pool,
439 queues: self.queues,
440 workers: Arc::new(self.workers),
441 lifecycle_handlers: Arc::new(self.lifecycle_handlers),
442 state: Arc::new(self.state),
443 heartbeat_interval: self.heartbeat_interval,
444 promote_interval: self.promote_interval,
445 heartbeat_rescue_interval: self.heartbeat_rescue_interval,
446 heartbeat_staleness: self.heartbeat_staleness,
447 deadline_rescue_interval: self.deadline_rescue_interval,
448 callback_rescue_interval: self.callback_rescue_interval,
449 periodic_jobs: Arc::new(self.periodic_jobs),
450 dispatch_cancel: CancellationToken::new(),
451 service_cancel: CancellationToken::new(),
452 dispatcher_handles: RwLock::new(Vec::new()),
453 service_handles: RwLock::new(Vec::new()),
454 job_set: Arc::new(Mutex::new(JoinSet::new())),
455 in_flight: Arc::new(InFlightRegistry::default()),
456 queue_in_flight,
457 dispatcher_alive,
458 heartbeat_alive: Arc::new(AtomicBool::new(false)),
459 maintenance_alive: Arc::new(AtomicBool::new(false)),
460 leader: Arc::new(AtomicBool::new(false)),
461 overflow_pool,
462 metrics,
463 leader_election_interval: self.leader_election_interval,
464 leader_check_interval: self.leader_check_interval,
465 completed_retention: self.completed_retention,
466 failed_retention: self.failed_retention,
467 cleanup_batch_size: self.cleanup_batch_size,
468 cleanup_interval: self.cleanup_interval,
469 queue_retention_overrides: self.queue_retention_overrides,
470 queue_stats_interval: self.queue_stats_interval,
471 global_max_workers: self.global_max_workers,
472 runtime_snapshot_interval: self.runtime_snapshot_interval,
473 runtime_instance_id: Uuid::new_v4(),
474 runtime_started_at: Utc::now(),
475 runtime_hostname: std::env::var("HOSTNAME").ok(),
476 runtime_pid: std::process::id() as i32,
477 runtime_version: env!("CARGO_PKG_VERSION"),
478 })
479 }
480}
481
482struct TypedWorker<T, F, Fut>
484where
485 T: JobArgs + DeserializeOwned + Send + Sync + 'static,
486 F: Fn(T, &crate::context::JobContext) -> Fut + Send + Sync + 'static,
487 Fut: std::future::Future<Output = Result<JobResult, JobError>> + Send + Sync + 'static,
488{
489 kind: &'static str,
490 handler: Arc<F>,
491 _phantom: std::marker::PhantomData<fn() -> (T, Fut)>,
492}
493
494#[async_trait::async_trait]
495impl<T, F, Fut> Worker for TypedWorker<T, F, Fut>
496where
497 T: JobArgs + DeserializeOwned + Send + Sync + 'static,
498 F: Fn(T, &crate::context::JobContext) -> Fut + Send + Sync + 'static,
499 Fut: std::future::Future<Output = Result<JobResult, JobError>> + Send + Sync + 'static,
500{
501 fn kind(&self) -> &'static str {
502 self.kind
503 }
504
505 async fn perform(&self, ctx: &crate::context::JobContext) -> Result<JobResult, JobError> {
506 let args: T = serde_json::from_value(ctx.job.args.clone())
507 .map_err(|err| JobError::Terminal(format!("failed to deserialize args: {}", err)))?;
508
509 (self.handler)(args, ctx).await
510 }
511}
512
513pub struct Client {
515 pool: PgPool,
516 queues: Vec<(String, QueueConfig)>,
517 workers: Arc<HashMap<String, BoxedWorker>>,
518 lifecycle_handlers: Arc<HashMap<String, Vec<BoxedUntypedEventHandler>>>,
519 state: Arc<HashMap<TypeId, Box<dyn Any + Send + Sync>>>,
520 heartbeat_interval: Duration,
521 promote_interval: Duration,
522 heartbeat_rescue_interval: Option<Duration>,
523 heartbeat_staleness: Option<Duration>,
524 deadline_rescue_interval: Option<Duration>,
525 callback_rescue_interval: Option<Duration>,
526 periodic_jobs: Arc<Vec<PeriodicJob>>,
527 dispatch_cancel: CancellationToken,
529 service_cancel: CancellationToken,
531 dispatcher_handles: RwLock<Vec<tokio::task::JoinHandle<()>>>,
533 service_handles: RwLock<Vec<tokio::task::JoinHandle<()>>>,
535 job_set: Arc<Mutex<JoinSet<()>>>,
537 in_flight: InFlightMap,
538 queue_in_flight: Arc<HashMap<String, Arc<AtomicU32>>>,
539 dispatcher_alive: Arc<HashMap<String, Arc<AtomicBool>>>,
540 heartbeat_alive: Arc<AtomicBool>,
541 maintenance_alive: Arc<AtomicBool>,
542 leader: Arc<AtomicBool>,
543 overflow_pool: Option<Arc<OverflowPool>>,
545 metrics: crate::metrics::AwaMetrics,
546 leader_election_interval: Option<Duration>,
547 leader_check_interval: Option<Duration>,
548 completed_retention: Option<Duration>,
549 failed_retention: Option<Duration>,
550 cleanup_batch_size: Option<i64>,
551 cleanup_interval: Option<Duration>,
552 queue_retention_overrides: HashMap<String, RetentionPolicy>,
553 queue_stats_interval: Option<Duration>,
554 global_max_workers: Option<u32>,
555 runtime_snapshot_interval: Duration,
556 runtime_instance_id: Uuid,
557 runtime_started_at: DateTime<Utc>,
558 runtime_hostname: Option<String>,
559 runtime_pid: i32,
560 runtime_version: &'static str,
561}
562
563#[derive(Clone)]
564struct RuntimeReporterState {
565 pool: PgPool,
566 queues: Vec<(String, QueueConfig)>,
567 queue_in_flight: Arc<HashMap<String, Arc<AtomicU32>>>,
568 dispatcher_alive: Arc<HashMap<String, Arc<AtomicBool>>>,
569 heartbeat_alive: Arc<AtomicBool>,
570 maintenance_alive: Arc<AtomicBool>,
571 leader: Arc<AtomicBool>,
572 dispatch_cancel: CancellationToken,
573 overflow_pool: Option<Arc<OverflowPool>>,
574 global_max_workers: Option<u32>,
575 instance_id: Uuid,
576 started_at: DateTime<Utc>,
577 hostname: Option<String>,
578 pid: i32,
579 version: &'static str,
580 snapshot_interval: Duration,
581}
582
583impl Client {
584 pub fn builder(pool: PgPool) -> ClientBuilder {
586 ClientBuilder::new(pool)
587 }
588
589 fn runtime_reporter_state(&self) -> RuntimeReporterState {
590 RuntimeReporterState {
591 pool: self.pool.clone(),
592 queues: self.queues.clone(),
593 queue_in_flight: self.queue_in_flight.clone(),
594 dispatcher_alive: self.dispatcher_alive.clone(),
595 heartbeat_alive: self.heartbeat_alive.clone(),
596 maintenance_alive: self.maintenance_alive.clone(),
597 leader: self.leader.clone(),
598 dispatch_cancel: self.dispatch_cancel.clone(),
599 overflow_pool: self.overflow_pool.clone(),
600 global_max_workers: self.global_max_workers,
601 instance_id: self.runtime_instance_id,
602 started_at: self.runtime_started_at,
603 hostname: self.runtime_hostname.clone(),
604 pid: self.runtime_pid,
605 version: self.runtime_version,
606 snapshot_interval: self.runtime_snapshot_interval,
607 }
608 }
609
610 async fn publish_runtime_snapshot(&self) {
611 let reporter = self.runtime_reporter_state();
612 reporter.publish_snapshot().await;
613 }
614
615 pub async fn start(&self) -> Result<(), awa_model::AwaError> {
617 info!(
618 queues = self.queues.len(),
619 workers = self.workers.len(),
620 "Starting Awa worker runtime"
621 );
622
623 let (completion_batcher, completion_handle) = CompletionBatcher::new(
626 self.pool.clone(),
627 self.service_cancel.clone(),
628 self.metrics.clone(),
629 );
630
631 let executor = Arc::new(JobExecutor::new(
633 self.pool.clone(),
634 self.workers.clone(),
635 self.lifecycle_handlers.clone(),
636 self.in_flight.clone(),
637 self.queue_in_flight.clone(),
638 self.state.clone(),
639 self.metrics.clone(),
640 completion_handle,
641 ));
642
643 let mut service_handles = self.service_handles.write().await;
644
645 service_handles.extend(completion_batcher.spawn());
646
647 let heartbeat = HeartbeatService::new(
649 self.pool.clone(),
650 self.in_flight.clone(),
651 self.heartbeat_interval,
652 self.heartbeat_alive.clone(),
653 self.service_cancel.clone(),
654 self.metrics.clone(),
655 );
656 service_handles.push(tokio::spawn(async move {
657 heartbeat.run().await;
658 }));
659
660 let mut maintenance = MaintenanceService::new(
662 self.pool.clone(),
663 self.metrics.clone(),
664 self.leader.clone(),
665 self.maintenance_alive.clone(),
666 self.service_cancel.clone(),
667 self.periodic_jobs.clone(),
668 self.in_flight.clone(),
669 )
670 .promote_interval(self.promote_interval);
671 if let Some(interval) = self.heartbeat_rescue_interval {
672 maintenance = maintenance.heartbeat_rescue_interval(interval);
673 }
674 if let Some(staleness) = self.heartbeat_staleness {
675 maintenance = maintenance.heartbeat_staleness(staleness);
676 }
677 if let Some(interval) = self.deadline_rescue_interval {
678 maintenance = maintenance.deadline_rescue_interval(interval);
679 }
680 if let Some(interval) = self.callback_rescue_interval {
681 maintenance = maintenance.callback_rescue_interval(interval);
682 }
683 if let Some(interval) = self.leader_election_interval {
684 maintenance = maintenance.leader_election_interval(interval);
685 }
686 if let Some(interval) = self.leader_check_interval {
687 maintenance = maintenance.leader_check_interval(interval);
688 }
689 if let Some(retention) = self.completed_retention {
690 maintenance = maintenance.completed_retention(retention);
691 }
692 if let Some(retention) = self.failed_retention {
693 maintenance = maintenance.failed_retention(retention);
694 }
695 if let Some(batch_size) = self.cleanup_batch_size {
696 maintenance = maintenance.cleanup_batch_size(batch_size);
697 }
698 if let Some(interval) = self.cleanup_interval {
699 maintenance = maintenance.cleanup_interval(interval);
700 }
701 if !self.queue_retention_overrides.is_empty() {
702 maintenance =
703 maintenance.queue_retention_overrides(self.queue_retention_overrides.clone());
704 }
705 if let Some(interval) = self.queue_stats_interval {
706 maintenance = maintenance.queue_stats_interval(interval);
707 }
708 service_handles.push(tokio::spawn(async move {
709 maintenance.run().await;
710 }));
711
712 let mut dispatcher_handles = self.dispatcher_handles.write().await;
714 for (queue_name, config) in &self.queues {
715 let alive = self
716 .dispatcher_alive
717 .get(queue_name)
718 .cloned()
719 .unwrap_or_else(|| Arc::new(AtomicBool::new(false)));
720
721 let dispatcher = if let Some(overflow_pool) = &self.overflow_pool {
722 let concurrency = ConcurrencyMode::Weighted {
724 local_semaphore: Arc::new(tokio::sync::Semaphore::new(
725 config.min_workers as usize,
726 )),
727 overflow_pool: overflow_pool.clone(),
728 queue_name: queue_name.clone(),
729 };
730 Dispatcher::with_concurrency(
731 queue_name.clone(),
732 config.clone(),
733 self.pool.clone(),
734 executor.clone(),
735 self.metrics.clone(),
736 self.in_flight.clone(),
737 alive,
738 self.dispatch_cancel.clone(),
739 self.job_set.clone(),
740 concurrency,
741 )
742 } else {
743 Dispatcher::new(
745 queue_name.clone(),
746 config.clone(),
747 self.pool.clone(),
748 executor.clone(),
749 self.metrics.clone(),
750 self.in_flight.clone(),
751 alive,
752 self.dispatch_cancel.clone(),
753 self.job_set.clone(),
754 )
755 };
756 dispatcher_handles.push(tokio::spawn(async move {
757 dispatcher.run().await;
758 }));
759 }
760
761 self.publish_runtime_snapshot().await;
762
763 let reporter = self.runtime_reporter_state();
764 service_handles.push(tokio::spawn(async move {
765 reporter.run().await;
766 }));
767
768 info!("Awa worker runtime started");
769 Ok(())
770 }
771
772 pub async fn shutdown(&self, timeout: Duration) {
781 info!("Initiating graceful shutdown");
782
783 self.dispatch_cancel.cancel();
785
786 self.publish_runtime_snapshot().await;
787
788 for flag in self.in_flight.flags() {
790 flag.store(true, Ordering::SeqCst);
791 }
792
793 let dispatcher_handles: Vec<_> = {
795 let mut guard = self.dispatcher_handles.write().await;
796 std::mem::take(&mut *guard)
797 };
798 for handle in dispatcher_handles {
799 let _ = handle.await;
800 }
801
802 let drain = async {
804 let mut set = self.job_set.lock().await;
805 while set.join_next().await.is_some() {}
806 };
807 if tokio::time::timeout(timeout, drain).await.is_err() {
808 warn!(
809 timeout_secs = timeout.as_secs(),
810 "Shutdown drain timeout exceeded, some jobs may not have completed"
811 );
812 }
813
814 self.service_cancel.cancel();
816 let service_handles: Vec<_> = {
817 let mut guard = self.service_handles.write().await;
818 std::mem::take(&mut *guard)
819 };
820 for handle in service_handles {
821 let _ = handle.await;
822 }
823
824 info!("Awa worker runtime stopped");
825 }
826
827 pub fn pool(&self) -> &PgPool {
829 &self.pool
830 }
831
832 pub async fn health_check(&self) -> HealthCheck {
834 let postgres_connected = sqlx::query("SELECT 1").execute(&self.pool).await.is_ok();
835 let poll_loop_alive = self
836 .dispatcher_alive
837 .values()
838 .all(|alive| alive.load(Ordering::SeqCst));
839 let heartbeat_alive = self.heartbeat_alive.load(Ordering::SeqCst);
840 let maintenance_alive = self.maintenance_alive.load(Ordering::SeqCst);
841 let shutting_down = self.dispatch_cancel.is_cancelled();
842 let leader = self.leader.load(Ordering::SeqCst);
843 let available_rows = sqlx::query_as::<_, (String, i64)>(
844 r#"
845 SELECT queue, count(*)::bigint AS available
846 FROM awa.jobs_hot
847 WHERE state = 'available'
848 GROUP BY queue
849 "#,
850 )
851 .fetch_all(&self.pool)
852 .await
853 .unwrap_or_default();
854 let available_by_queue: HashMap<_, _> = available_rows.into_iter().collect();
855 let queues = self
856 .queues
857 .iter()
858 .map(|(queue, config)| {
859 let in_flight = self
860 .queue_in_flight
861 .get(queue)
862 .map(|counter| counter.load(Ordering::SeqCst))
863 .unwrap_or(0);
864 let available = available_by_queue.get(queue).copied().unwrap_or(0).max(0) as u64;
865 let capacity = if let Some(overflow_pool) = &self.overflow_pool {
866 QueueCapacity::Weighted {
867 min_workers: config.min_workers,
868 weight: config.weight,
869 overflow_held: overflow_pool.held(queue),
870 }
871 } else {
872 QueueCapacity::HardReserved {
873 max_workers: config.max_workers,
874 }
875 };
876 (
877 queue.clone(),
878 QueueHealth {
879 in_flight,
880 available,
881 capacity,
882 },
883 )
884 })
885 .collect();
886
887 HealthCheck {
888 healthy: postgres_connected
889 && poll_loop_alive
890 && heartbeat_alive
891 && maintenance_alive
892 && !shutting_down,
893 postgres_connected,
894 poll_loop_alive,
895 heartbeat_alive,
896 maintenance_alive,
897 shutting_down,
898 leader,
899 queues,
900 }
901 }
902}
903
904impl RuntimeReporterState {
905 fn queue_snapshot(&self, queue: &str, config: &QueueConfig) -> QueueRuntimeSnapshot {
906 let in_flight = self
907 .queue_in_flight
908 .get(queue)
909 .map(|counter| counter.load(Ordering::SeqCst))
910 .unwrap_or(0);
911
912 let (mode, max_workers, min_workers, weight, overflow_held) =
913 if let Some(overflow_pool) = &self.overflow_pool {
914 (
915 QueueRuntimeMode::Weighted,
916 None,
917 Some(config.min_workers),
918 Some(config.weight),
919 Some(overflow_pool.held(queue)),
920 )
921 } else {
922 (
923 QueueRuntimeMode::HardReserved,
924 Some(config.max_workers),
925 None,
926 None,
927 None,
928 )
929 };
930
931 QueueRuntimeSnapshot {
932 queue: queue.to_string(),
933 in_flight,
934 overflow_held,
935 config: QueueRuntimeConfigSnapshot {
936 mode,
937 max_workers,
938 min_workers,
939 weight,
940 global_max_workers: self.global_max_workers,
941 poll_interval_ms: config.poll_interval.as_millis() as u64,
942 deadline_duration_secs: config.deadline_duration.as_secs(),
943 priority_aging_interval_secs: config.priority_aging_interval.as_secs(),
944 rate_limit: config.rate_limit.as_ref().map(|rl| RateLimitSnapshot {
945 max_rate: rl.max_rate,
946 burst: rl.burst,
947 }),
948 },
949 }
950 }
951
952 async fn snapshot_input(&self) -> RuntimeSnapshotInput {
953 let postgres_connected = sqlx::query("SELECT 1").execute(&self.pool).await.is_ok();
954 let poll_loop_alive = self
955 .dispatcher_alive
956 .values()
957 .all(|alive| alive.load(Ordering::SeqCst));
958 let heartbeat_alive = self.heartbeat_alive.load(Ordering::SeqCst);
959 let maintenance_alive = self.maintenance_alive.load(Ordering::SeqCst);
960 let shutting_down = self.dispatch_cancel.is_cancelled();
961 let leader = self.leader.load(Ordering::SeqCst);
962 let healthy = postgres_connected
963 && poll_loop_alive
964 && heartbeat_alive
965 && maintenance_alive
966 && !shutting_down;
967 let queues = self
968 .queues
969 .iter()
970 .map(|(queue, config)| self.queue_snapshot(queue, config))
971 .collect();
972
973 RuntimeSnapshotInput {
974 instance_id: self.instance_id,
975 hostname: self.hostname.clone(),
976 pid: self.pid,
977 version: self.version.to_string(),
978 started_at: self.started_at,
979 snapshot_interval_ms: self.snapshot_interval.as_millis() as i64,
980 healthy,
981 postgres_connected,
982 poll_loop_alive,
983 heartbeat_alive,
984 maintenance_alive,
985 shutting_down,
986 leader,
987 global_max_workers: self.global_max_workers,
988 queues,
989 }
990 }
991
992 async fn publish_snapshot(&self) {
993 let snapshot = self.snapshot_input().await;
994 if let Err(err) = admin::upsert_runtime_snapshot(&self.pool, &snapshot).await {
995 warn!(error = %err, "Failed to publish runtime snapshot");
996 }
997 }
998
999 async fn run(self) {
1000 let mut interval = tokio::time::interval(self.snapshot_interval);
1001 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
1002 interval.tick().await;
1003 loop {
1004 tokio::select! {
1005 _ = self.dispatch_cancel.cancelled() => {
1006 self.publish_snapshot().await;
1007 break;
1008 }
1009 _ = interval.tick() => {
1010 self.publish_snapshot().await;
1011 }
1012 }
1013 }
1014 }
1015}