use crate::completion::CompletionBatcher;
use crate::dispatcher::{ConcurrencyMode, Dispatcher, OverflowPool, QueueConfig};
use crate::events::{BoxedUntypedEventHandler, JobEvent, UntypedJobEvent};
use crate::executor::{BoxedWorker, JobError, JobExecutor, JobResult, Worker};
use crate::heartbeat::HeartbeatService;
use crate::maintenance::{MaintenanceService, RetentionPolicy};
use crate::runtime::{InFlightMap, InFlightRegistry};
use awa_model::admin::{
self, QueueRuntimeConfigSnapshot, QueueRuntimeMode, QueueRuntimeSnapshot, RateLimitSnapshot,
RuntimeSnapshotInput,
};
use awa_model::{JobArgs, PeriodicJob};
use chrono::{DateTime, Utc};
use serde::de::DeserializeOwned;
use sqlx::PgPool;
use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{Mutex, RwLock};
use tokio::task::JoinSet;
use tokio_util::sync::CancellationToken;
use tracing::{info, warn};
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum BuildError {
#[error("at least one queue must be configured")]
NoQueuesConfigured,
#[error("sum of min_workers ({total_min}) exceeds global_max_workers ({global_max})")]
MinWorkersExceedGlobal { total_min: u32, global_max: u32 },
#[error("rate_limit max_rate must be > 0.0")]
InvalidRateLimit,
#[error("queue weight must be > 0")]
InvalidWeight,
#[error("cleanup_batch_size must be > 0")]
InvalidBatchSize,
}
#[derive(Debug, Clone)]
pub struct HealthCheck {
pub healthy: bool,
pub postgres_connected: bool,
pub poll_loop_alive: bool,
pub heartbeat_alive: bool,
pub maintenance_alive: bool,
pub shutting_down: bool,
pub leader: bool,
pub queues: HashMap<String, QueueHealth>,
}
#[derive(Debug, Clone)]
pub struct QueueHealth {
pub in_flight: u32,
pub available: u64,
pub capacity: QueueCapacity,
}
#[derive(Debug, Clone)]
pub enum QueueCapacity {
HardReserved { max_workers: u32 },
Weighted {
min_workers: u32,
weight: u32,
overflow_held: u32,
},
}
pub struct ClientBuilder {
pool: PgPool,
queues: Vec<(String, QueueConfig)>,
workers: HashMap<String, BoxedWorker>,
lifecycle_handlers: HashMap<String, Vec<BoxedUntypedEventHandler>>,
state: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
heartbeat_interval: Duration,
promote_interval: Duration,
heartbeat_rescue_interval: Option<Duration>,
deadline_rescue_interval: Option<Duration>,
callback_rescue_interval: Option<Duration>,
periodic_jobs: Vec<PeriodicJob>,
global_max_workers: Option<u32>,
leader_election_interval: Option<Duration>,
leader_check_interval: Option<Duration>,
completed_retention: Option<Duration>,
failed_retention: Option<Duration>,
cleanup_batch_size: Option<i64>,
cleanup_interval: Option<Duration>,
queue_retention_overrides: HashMap<String, RetentionPolicy>,
runtime_snapshot_interval: Duration,
}
impl ClientBuilder {
pub fn new(pool: PgPool) -> Self {
Self {
pool,
queues: Vec::new(),
workers: HashMap::new(),
lifecycle_handlers: HashMap::new(),
state: HashMap::new(),
heartbeat_interval: Duration::from_secs(30),
promote_interval: Duration::from_millis(250),
heartbeat_rescue_interval: None,
deadline_rescue_interval: None,
callback_rescue_interval: None,
periodic_jobs: Vec::new(),
global_max_workers: None,
leader_election_interval: None,
leader_check_interval: None,
completed_retention: None,
failed_retention: None,
cleanup_batch_size: None,
cleanup_interval: None,
queue_retention_overrides: HashMap::new(),
runtime_snapshot_interval: Duration::from_secs(10),
}
}
pub fn queue(mut self, name: impl Into<String>, config: QueueConfig) -> Self {
self.queues.push((name.into(), config));
self
}
pub fn register<T, F, Fut>(mut self, handler: F) -> Self
where
T: JobArgs + DeserializeOwned + Send + Sync + 'static,
F: Fn(T, &crate::context::JobContext) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<JobResult, JobError>> + Send + Sync + 'static,
{
let kind = T::kind().to_string();
let worker = TypedWorker {
kind: T::kind(),
handler: Arc::new(handler),
_phantom: std::marker::PhantomData,
};
self.workers.insert(kind, Box::new(worker));
self
}
pub fn on_event<T, F, Fut>(mut self, handler: F) -> Self
where
T: JobArgs + DeserializeOwned + Send + Sync + 'static,
F: Fn(JobEvent<T>) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = ()> + Send + 'static,
{
let kind = T::kind().to_string();
let handler = Arc::new(handler);
let erased: BoxedUntypedEventHandler = Arc::new(move |event: UntypedJobEvent| {
let handler = handler.clone();
Box::pin(async move {
let args: T = match serde_json::from_value(event.job().args.clone()) {
Ok(args) => args,
Err(err) => {
warn!(
job_id = event.job().id,
kind = %event.job().kind,
error = %err,
"Failed to deserialize args for lifecycle event handler"
);
return;
}
};
(handler)(event.into_typed(args)).await;
})
});
self.lifecycle_handlers
.entry(kind)
.or_default()
.push(erased);
self
}
pub fn on_event_kind<F, Fut>(mut self, kind: impl Into<String>, handler: F) -> Self
where
F: Fn(UntypedJobEvent) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = ()> + Send + 'static,
{
let kind = kind.into();
let handler = Arc::new(handler);
let erased: BoxedUntypedEventHandler = Arc::new(move |event: UntypedJobEvent| {
let handler = handler.clone();
Box::pin(async move {
(handler)(event).await;
})
});
self.lifecycle_handlers
.entry(kind)
.or_default()
.push(erased);
self
}
pub fn register_worker(mut self, worker: impl Worker + 'static) -> Self {
let kind = worker.kind().to_string();
self.workers.insert(kind, Box::new(worker));
self
}
pub fn state<T: Any + Send + Sync + Clone>(mut self, value: T) -> Self {
self.state.insert(TypeId::of::<T>(), Box::new(value));
self
}
pub fn heartbeat_interval(mut self, interval: Duration) -> Self {
self.heartbeat_interval = interval;
self
}
pub fn promote_interval(mut self, interval: Duration) -> Self {
self.promote_interval = interval;
self
}
pub fn heartbeat_rescue_interval(mut self, interval: Duration) -> Self {
self.heartbeat_rescue_interval = Some(interval);
self
}
pub fn deadline_rescue_interval(mut self, interval: Duration) -> Self {
self.deadline_rescue_interval = Some(interval);
self
}
pub fn callback_rescue_interval(mut self, interval: Duration) -> Self {
self.callback_rescue_interval = Some(interval);
self
}
pub fn leader_election_interval(mut self, interval: Duration) -> Self {
self.leader_election_interval = Some(interval);
self
}
pub fn leader_check_interval(mut self, interval: Duration) -> Self {
self.leader_check_interval = Some(interval);
self
}
pub fn global_max_workers(mut self, max: u32) -> Self {
self.global_max_workers = Some(max);
self
}
pub fn completed_retention(mut self, retention: Duration) -> Self {
self.completed_retention = Some(retention);
self
}
pub fn failed_retention(mut self, retention: Duration) -> Self {
self.failed_retention = Some(retention);
self
}
pub fn cleanup_batch_size(mut self, batch_size: i64) -> Self {
self.cleanup_batch_size = Some(batch_size);
self
}
pub fn cleanup_interval(mut self, interval: Duration) -> Self {
self.cleanup_interval = Some(interval);
self
}
pub fn queue_retention(mut self, queue: impl Into<String>, policy: RetentionPolicy) -> Self {
self.queue_retention_overrides.insert(queue.into(), policy);
self
}
pub fn runtime_snapshot_interval(mut self, interval: Duration) -> Self {
self.runtime_snapshot_interval = interval;
self
}
pub fn periodic(mut self, job: PeriodicJob) -> Self {
self.periodic_jobs.push(job);
self
}
pub fn build(self) -> Result<Client, BuildError> {
if self.queues.is_empty() {
return Err(BuildError::NoQueuesConfigured);
}
for (_, config) in &self.queues {
if let Some(rl) = &config.rate_limit {
if rl.max_rate <= 0.0 {
return Err(BuildError::InvalidRateLimit);
}
}
if config.weight == 0 {
return Err(BuildError::InvalidWeight);
}
}
if let Some(bs) = self.cleanup_batch_size {
if bs <= 0 {
return Err(BuildError::InvalidBatchSize);
}
}
let overflow_pool = if let Some(global_max) = self.global_max_workers {
let total_min: u32 = self.queues.iter().map(|(_, c)| c.min_workers).sum();
if total_min > global_max {
return Err(BuildError::MinWorkersExceedGlobal {
total_min,
global_max,
});
}
let overflow_capacity = global_max - total_min;
let weights: HashMap<String, u32> = self
.queues
.iter()
.map(|(name, c)| (name.clone(), c.weight.max(1)))
.collect();
Some(Arc::new(OverflowPool::new(overflow_capacity, weights)))
} else {
None
};
let metrics = crate::metrics::AwaMetrics::from_global();
let queue_in_flight = Arc::new(
self.queues
.iter()
.map(|(name, _)| (name.clone(), Arc::new(AtomicU32::new(0))))
.collect(),
);
let dispatcher_alive = Arc::new(
self.queues
.iter()
.map(|(name, _)| (name.clone(), Arc::new(AtomicBool::new(false))))
.collect(),
);
Ok(Client {
pool: self.pool,
queues: self.queues,
workers: Arc::new(self.workers),
lifecycle_handlers: Arc::new(self.lifecycle_handlers),
state: Arc::new(self.state),
heartbeat_interval: self.heartbeat_interval,
promote_interval: self.promote_interval,
heartbeat_rescue_interval: self.heartbeat_rescue_interval,
deadline_rescue_interval: self.deadline_rescue_interval,
callback_rescue_interval: self.callback_rescue_interval,
periodic_jobs: Arc::new(self.periodic_jobs),
dispatch_cancel: CancellationToken::new(),
service_cancel: CancellationToken::new(),
dispatcher_handles: RwLock::new(Vec::new()),
service_handles: RwLock::new(Vec::new()),
job_set: Arc::new(Mutex::new(JoinSet::new())),
in_flight: Arc::new(InFlightRegistry::default()),
queue_in_flight,
dispatcher_alive,
heartbeat_alive: Arc::new(AtomicBool::new(false)),
maintenance_alive: Arc::new(AtomicBool::new(false)),
leader: Arc::new(AtomicBool::new(false)),
overflow_pool,
metrics,
leader_election_interval: self.leader_election_interval,
leader_check_interval: self.leader_check_interval,
completed_retention: self.completed_retention,
failed_retention: self.failed_retention,
cleanup_batch_size: self.cleanup_batch_size,
cleanup_interval: self.cleanup_interval,
queue_retention_overrides: self.queue_retention_overrides,
global_max_workers: self.global_max_workers,
runtime_snapshot_interval: self.runtime_snapshot_interval,
runtime_instance_id: Uuid::new_v4(),
runtime_started_at: Utc::now(),
runtime_hostname: std::env::var("HOSTNAME").ok(),
runtime_pid: std::process::id() as i32,
runtime_version: env!("CARGO_PKG_VERSION"),
})
}
}
struct TypedWorker<T, F, Fut>
where
T: JobArgs + DeserializeOwned + Send + Sync + 'static,
F: Fn(T, &crate::context::JobContext) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<JobResult, JobError>> + Send + Sync + 'static,
{
kind: &'static str,
handler: Arc<F>,
_phantom: std::marker::PhantomData<fn() -> (T, Fut)>,
}
#[async_trait::async_trait]
impl<T, F, Fut> Worker for TypedWorker<T, F, Fut>
where
T: JobArgs + DeserializeOwned + Send + Sync + 'static,
F: Fn(T, &crate::context::JobContext) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<JobResult, JobError>> + Send + Sync + 'static,
{
fn kind(&self) -> &'static str {
self.kind
}
async fn perform(&self, ctx: &crate::context::JobContext) -> Result<JobResult, JobError> {
let args: T = serde_json::from_value(ctx.job.args.clone())
.map_err(|err| JobError::Terminal(format!("failed to deserialize args: {}", err)))?;
(self.handler)(args, ctx).await
}
}
pub struct Client {
pool: PgPool,
queues: Vec<(String, QueueConfig)>,
workers: Arc<HashMap<String, BoxedWorker>>,
lifecycle_handlers: Arc<HashMap<String, Vec<BoxedUntypedEventHandler>>>,
state: Arc<HashMap<TypeId, Box<dyn Any + Send + Sync>>>,
heartbeat_interval: Duration,
promote_interval: Duration,
heartbeat_rescue_interval: Option<Duration>,
deadline_rescue_interval: Option<Duration>,
callback_rescue_interval: Option<Duration>,
periodic_jobs: Arc<Vec<PeriodicJob>>,
dispatch_cancel: CancellationToken,
service_cancel: CancellationToken,
dispatcher_handles: RwLock<Vec<tokio::task::JoinHandle<()>>>,
service_handles: RwLock<Vec<tokio::task::JoinHandle<()>>>,
job_set: Arc<Mutex<JoinSet<()>>>,
in_flight: InFlightMap,
queue_in_flight: Arc<HashMap<String, Arc<AtomicU32>>>,
dispatcher_alive: Arc<HashMap<String, Arc<AtomicBool>>>,
heartbeat_alive: Arc<AtomicBool>,
maintenance_alive: Arc<AtomicBool>,
leader: Arc<AtomicBool>,
overflow_pool: Option<Arc<OverflowPool>>,
metrics: crate::metrics::AwaMetrics,
leader_election_interval: Option<Duration>,
leader_check_interval: Option<Duration>,
completed_retention: Option<Duration>,
failed_retention: Option<Duration>,
cleanup_batch_size: Option<i64>,
cleanup_interval: Option<Duration>,
queue_retention_overrides: HashMap<String, RetentionPolicy>,
global_max_workers: Option<u32>,
runtime_snapshot_interval: Duration,
runtime_instance_id: Uuid,
runtime_started_at: DateTime<Utc>,
runtime_hostname: Option<String>,
runtime_pid: i32,
runtime_version: &'static str,
}
#[derive(Clone)]
struct RuntimeReporterState {
pool: PgPool,
queues: Vec<(String, QueueConfig)>,
queue_in_flight: Arc<HashMap<String, Arc<AtomicU32>>>,
dispatcher_alive: Arc<HashMap<String, Arc<AtomicBool>>>,
heartbeat_alive: Arc<AtomicBool>,
maintenance_alive: Arc<AtomicBool>,
leader: Arc<AtomicBool>,
dispatch_cancel: CancellationToken,
overflow_pool: Option<Arc<OverflowPool>>,
global_max_workers: Option<u32>,
instance_id: Uuid,
started_at: DateTime<Utc>,
hostname: Option<String>,
pid: i32,
version: &'static str,
snapshot_interval: Duration,
}
impl Client {
pub fn builder(pool: PgPool) -> ClientBuilder {
ClientBuilder::new(pool)
}
fn runtime_reporter_state(&self) -> RuntimeReporterState {
RuntimeReporterState {
pool: self.pool.clone(),
queues: self.queues.clone(),
queue_in_flight: self.queue_in_flight.clone(),
dispatcher_alive: self.dispatcher_alive.clone(),
heartbeat_alive: self.heartbeat_alive.clone(),
maintenance_alive: self.maintenance_alive.clone(),
leader: self.leader.clone(),
dispatch_cancel: self.dispatch_cancel.clone(),
overflow_pool: self.overflow_pool.clone(),
global_max_workers: self.global_max_workers,
instance_id: self.runtime_instance_id,
started_at: self.runtime_started_at,
hostname: self.runtime_hostname.clone(),
pid: self.runtime_pid,
version: self.runtime_version,
snapshot_interval: self.runtime_snapshot_interval,
}
}
async fn publish_runtime_snapshot(&self) {
let reporter = self.runtime_reporter_state();
reporter.publish_snapshot().await;
}
pub async fn start(&self) -> Result<(), awa_model::AwaError> {
info!(
queues = self.queues.len(),
workers = self.workers.len(),
"Starting Awa worker runtime"
);
let (completion_batcher, completion_handle) = CompletionBatcher::new(
self.pool.clone(),
self.service_cancel.clone(),
self.metrics.clone(),
);
let executor = Arc::new(JobExecutor::new(
self.pool.clone(),
self.workers.clone(),
self.lifecycle_handlers.clone(),
self.in_flight.clone(),
self.queue_in_flight.clone(),
self.state.clone(),
self.metrics.clone(),
completion_handle,
));
let mut service_handles = self.service_handles.write().await;
service_handles.extend(completion_batcher.spawn());
let heartbeat = HeartbeatService::new(
self.pool.clone(),
self.in_flight.clone(),
self.heartbeat_interval,
self.heartbeat_alive.clone(),
self.service_cancel.clone(),
self.metrics.clone(),
);
service_handles.push(tokio::spawn(async move {
heartbeat.run().await;
}));
let mut maintenance = MaintenanceService::new(
self.pool.clone(),
self.metrics.clone(),
self.leader.clone(),
self.maintenance_alive.clone(),
self.service_cancel.clone(),
self.periodic_jobs.clone(),
self.in_flight.clone(),
)
.promote_interval(self.promote_interval);
if let Some(interval) = self.heartbeat_rescue_interval {
maintenance = maintenance.heartbeat_rescue_interval(interval);
}
if let Some(interval) = self.deadline_rescue_interval {
maintenance = maintenance.deadline_rescue_interval(interval);
}
if let Some(interval) = self.callback_rescue_interval {
maintenance = maintenance.callback_rescue_interval(interval);
}
if let Some(interval) = self.leader_election_interval {
maintenance = maintenance.leader_election_interval(interval);
}
if let Some(interval) = self.leader_check_interval {
maintenance = maintenance.leader_check_interval(interval);
}
if let Some(retention) = self.completed_retention {
maintenance = maintenance.completed_retention(retention);
}
if let Some(retention) = self.failed_retention {
maintenance = maintenance.failed_retention(retention);
}
if let Some(batch_size) = self.cleanup_batch_size {
maintenance = maintenance.cleanup_batch_size(batch_size);
}
if let Some(interval) = self.cleanup_interval {
maintenance = maintenance.cleanup_interval(interval);
}
if !self.queue_retention_overrides.is_empty() {
maintenance =
maintenance.queue_retention_overrides(self.queue_retention_overrides.clone());
}
service_handles.push(tokio::spawn(async move {
maintenance.run().await;
}));
let mut dispatcher_handles = self.dispatcher_handles.write().await;
for (queue_name, config) in &self.queues {
let alive = self
.dispatcher_alive
.get(queue_name)
.cloned()
.unwrap_or_else(|| Arc::new(AtomicBool::new(false)));
let dispatcher = if let Some(overflow_pool) = &self.overflow_pool {
let concurrency = ConcurrencyMode::Weighted {
local_semaphore: Arc::new(tokio::sync::Semaphore::new(
config.min_workers as usize,
)),
overflow_pool: overflow_pool.clone(),
queue_name: queue_name.clone(),
};
Dispatcher::with_concurrency(
queue_name.clone(),
config.clone(),
self.pool.clone(),
executor.clone(),
self.metrics.clone(),
self.in_flight.clone(),
alive,
self.dispatch_cancel.clone(),
self.job_set.clone(),
concurrency,
)
} else {
Dispatcher::new(
queue_name.clone(),
config.clone(),
self.pool.clone(),
executor.clone(),
self.metrics.clone(),
self.in_flight.clone(),
alive,
self.dispatch_cancel.clone(),
self.job_set.clone(),
)
};
dispatcher_handles.push(tokio::spawn(async move {
dispatcher.run().await;
}));
}
self.publish_runtime_snapshot().await;
let reporter = self.runtime_reporter_state();
service_handles.push(tokio::spawn(async move {
reporter.run().await;
}));
info!("Awa worker runtime started");
Ok(())
}
pub async fn shutdown(&self, timeout: Duration) {
info!("Initiating graceful shutdown");
self.dispatch_cancel.cancel();
self.publish_runtime_snapshot().await;
for flag in self.in_flight.flags() {
flag.store(true, Ordering::SeqCst);
}
let dispatcher_handles: Vec<_> = {
let mut guard = self.dispatcher_handles.write().await;
std::mem::take(&mut *guard)
};
for handle in dispatcher_handles {
let _ = handle.await;
}
let drain = async {
let mut set = self.job_set.lock().await;
while set.join_next().await.is_some() {}
};
if tokio::time::timeout(timeout, drain).await.is_err() {
warn!(
timeout_secs = timeout.as_secs(),
"Shutdown drain timeout exceeded, some jobs may not have completed"
);
}
self.service_cancel.cancel();
let service_handles: Vec<_> = {
let mut guard = self.service_handles.write().await;
std::mem::take(&mut *guard)
};
for handle in service_handles {
let _ = handle.await;
}
info!("Awa worker runtime stopped");
}
pub fn pool(&self) -> &PgPool {
&self.pool
}
pub async fn health_check(&self) -> HealthCheck {
let postgres_connected = sqlx::query("SELECT 1").execute(&self.pool).await.is_ok();
let poll_loop_alive = self
.dispatcher_alive
.values()
.all(|alive| alive.load(Ordering::SeqCst));
let heartbeat_alive = self.heartbeat_alive.load(Ordering::SeqCst);
let maintenance_alive = self.maintenance_alive.load(Ordering::SeqCst);
let shutting_down = self.dispatch_cancel.is_cancelled();
let leader = self.leader.load(Ordering::SeqCst);
let available_rows = sqlx::query_as::<_, (String, i64)>(
r#"
SELECT queue, count(*)::bigint AS available
FROM awa.jobs_hot
WHERE state = 'available'
GROUP BY queue
"#,
)
.fetch_all(&self.pool)
.await
.unwrap_or_default();
let available_by_queue: HashMap<_, _> = available_rows.into_iter().collect();
let queues = self
.queues
.iter()
.map(|(queue, config)| {
let in_flight = self
.queue_in_flight
.get(queue)
.map(|counter| counter.load(Ordering::SeqCst))
.unwrap_or(0);
let available = available_by_queue.get(queue).copied().unwrap_or(0).max(0) as u64;
let capacity = if let Some(overflow_pool) = &self.overflow_pool {
QueueCapacity::Weighted {
min_workers: config.min_workers,
weight: config.weight,
overflow_held: overflow_pool.held(queue),
}
} else {
QueueCapacity::HardReserved {
max_workers: config.max_workers,
}
};
(
queue.clone(),
QueueHealth {
in_flight,
available,
capacity,
},
)
})
.collect();
HealthCheck {
healthy: postgres_connected
&& poll_loop_alive
&& heartbeat_alive
&& maintenance_alive
&& !shutting_down,
postgres_connected,
poll_loop_alive,
heartbeat_alive,
maintenance_alive,
shutting_down,
leader,
queues,
}
}
}
impl RuntimeReporterState {
fn queue_snapshot(&self, queue: &str, config: &QueueConfig) -> QueueRuntimeSnapshot {
let in_flight = self
.queue_in_flight
.get(queue)
.map(|counter| counter.load(Ordering::SeqCst))
.unwrap_or(0);
let (mode, max_workers, min_workers, weight, overflow_held) =
if let Some(overflow_pool) = &self.overflow_pool {
(
QueueRuntimeMode::Weighted,
None,
Some(config.min_workers),
Some(config.weight),
Some(overflow_pool.held(queue)),
)
} else {
(
QueueRuntimeMode::HardReserved,
Some(config.max_workers),
None,
None,
None,
)
};
QueueRuntimeSnapshot {
queue: queue.to_string(),
in_flight,
overflow_held,
config: QueueRuntimeConfigSnapshot {
mode,
max_workers,
min_workers,
weight,
global_max_workers: self.global_max_workers,
poll_interval_ms: config.poll_interval.as_millis() as u64,
deadline_duration_secs: config.deadline_duration.as_secs(),
priority_aging_interval_secs: config.priority_aging_interval.as_secs(),
rate_limit: config.rate_limit.as_ref().map(|rl| RateLimitSnapshot {
max_rate: rl.max_rate,
burst: rl.burst,
}),
},
}
}
async fn snapshot_input(&self) -> RuntimeSnapshotInput {
let postgres_connected = sqlx::query("SELECT 1").execute(&self.pool).await.is_ok();
let poll_loop_alive = self
.dispatcher_alive
.values()
.all(|alive| alive.load(Ordering::SeqCst));
let heartbeat_alive = self.heartbeat_alive.load(Ordering::SeqCst);
let maintenance_alive = self.maintenance_alive.load(Ordering::SeqCst);
let shutting_down = self.dispatch_cancel.is_cancelled();
let leader = self.leader.load(Ordering::SeqCst);
let healthy = postgres_connected
&& poll_loop_alive
&& heartbeat_alive
&& maintenance_alive
&& !shutting_down;
let queues = self
.queues
.iter()
.map(|(queue, config)| self.queue_snapshot(queue, config))
.collect();
RuntimeSnapshotInput {
instance_id: self.instance_id,
hostname: self.hostname.clone(),
pid: self.pid,
version: self.version.to_string(),
started_at: self.started_at,
snapshot_interval_ms: self.snapshot_interval.as_millis() as i64,
healthy,
postgres_connected,
poll_loop_alive,
heartbeat_alive,
maintenance_alive,
shutting_down,
leader,
global_max_workers: self.global_max_workers,
queues,
}
}
async fn publish_snapshot(&self) {
let snapshot = self.snapshot_input().await;
if let Err(err) = admin::upsert_runtime_snapshot(&self.pool, &snapshot).await {
warn!(error = %err, "Failed to publish runtime snapshot");
}
}
async fn run(self) {
let mut interval = tokio::time::interval(self.snapshot_interval);
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
interval.tick().await;
loop {
tokio::select! {
_ = self.dispatch_cancel.cancelled() => {
self.publish_snapshot().await;
break;
}
_ = interval.tick() => {
self.publish_snapshot().await;
}
}
}
}
}