pub mod config;
pub mod db;
pub mod error;
pub mod events;
pub mod executor;
pub mod queue;
pub mod types;
pub use config::{QueueConfig, QueueConfigBuilder};
pub use error::QueueError;
pub use executor::ProcessedJob;
pub use queue::QueueManager;
pub use types::{
FailureClass, JobResult, QueueJob, QueueJobDetails, QueueJobStatus, QueuePriority, QueueStats,
};
use rusqlite::Connection;
use std::sync::{Arc, Mutex};
pub trait QueueEventEmitter: Send + Sync + 'static {
fn emit_job_started(&self, event: events::JobStartedEvent);
fn emit_job_completed(&self, event: events::JobCompletedEvent);
fn emit_job_failed(&self, event: events::JobFailedEvent);
fn emit_job_progress(&self, event: events::JobProgressEvent);
fn emit_job_cancelled(&self, event: events::JobCancelledEvent);
}
pub struct NoopEventEmitter;
impl QueueEventEmitter for NoopEventEmitter {
fn emit_job_started(&self, _event: events::JobStartedEvent) {}
fn emit_job_completed(&self, _event: events::JobCompletedEvent) {}
fn emit_job_failed(&self, _event: events::JobFailedEvent) {}
fn emit_job_progress(&self, _event: events::JobProgressEvent) {}
fn emit_job_cancelled(&self, _event: events::JobCancelledEvent) {}
}
pub struct LoggingEventEmitter;
#[allow(deprecated)]
impl QueueEventEmitter for LoggingEventEmitter {
fn emit_job_started(&self, event: events::JobStartedEvent) {
let trace = event
.trace_ctx
.as_ref()
.map(|ctx| ctx.trace_id.as_str())
.or(event.trace_id.as_deref())
.unwrap_or("");
let attempt = event
.attempt_id
.as_ref()
.map(|a| a.to_string())
.unwrap_or_else(|| {
event
.attempt_count
.map(|c| c.to_string())
.unwrap_or_default()
});
tracing::info!(
job_id = %event.job_id,
worker_id = event.worker_id.as_deref().unwrap_or(""),
trace_id = %trace,
attempt = %attempt,
trial_id = event.trial_id.as_ref().map(|t| t.to_string()).unwrap_or_default().as_str(),
"Job started"
);
}
fn emit_job_completed(&self, event: events::JobCompletedEvent) {
let trace = event
.trace_ctx
.as_ref()
.map(|ctx| ctx.trace_id.as_str())
.or(event.trace_id.as_deref())
.unwrap_or("");
let attempt = event
.attempt_id
.as_ref()
.map(|a| a.to_string())
.unwrap_or_else(|| {
event
.attempt_count
.map(|c| c.to_string())
.unwrap_or_default()
});
tracing::info!(
job_id = %event.job_id,
worker_id = event.worker_id.as_deref().unwrap_or(""),
trace_id = %trace,
attempt = %attempt,
trial_id = event.trial_id.as_ref().map(|t| t.to_string()).unwrap_or_default().as_str(),
"Job completed"
);
}
fn emit_job_failed(&self, event: events::JobFailedEvent) {
let trace = event
.trace_ctx
.as_ref()
.map(|ctx| ctx.trace_id.as_str())
.or(event.trace_id.as_deref())
.unwrap_or("");
let attempt = event
.attempt_id
.as_ref()
.map(|a| a.to_string())
.unwrap_or_else(|| {
event
.attempt_count
.map(|c| c.to_string())
.unwrap_or_default()
});
tracing::warn!(
job_id = %event.job_id,
worker_id = event.worker_id.as_deref().unwrap_or(""),
trace_id = %trace,
attempt = %attempt,
trial_id = event.trial_id.as_ref().map(|t| t.to_string()).unwrap_or_default().as_str(),
failure_class = event.failure_class.as_deref().unwrap_or(""),
error = %event.error,
"Job failed"
);
}
fn emit_job_progress(&self, event: events::JobProgressEvent) {
let trace = event
.trace_ctx
.as_ref()
.map(|ctx| ctx.trace_id.as_str())
.or(event.trace_id.as_deref())
.unwrap_or("");
tracing::debug!(
job_id = %event.job_id,
worker_id = event.worker_id.as_deref().unwrap_or(""),
trace_id = %trace,
progress = %event.progress,
step = %event.current_step,
total = %event.total_steps,
"Job progress"
);
}
fn emit_job_cancelled(&self, event: events::JobCancelledEvent) {
let trace = event
.trace_ctx
.as_ref()
.map(|ctx| ctx.trace_id.as_str())
.or(event.trace_id.as_deref())
.unwrap_or("");
tracing::info!(
job_id = %event.job_id,
worker_id = event.worker_id.as_deref().unwrap_or(""),
trace_id = %trace,
"Job cancelled"
);
}
}
#[allow(deprecated)]
pub struct JobContext {
pub job_id: String,
#[deprecated(note = "Use trace_ctx instead. Will be removed when all consumers migrate.")]
pub trace_id: Option<String>,
pub trace_ctx: Option<stack_ids::TraceCtx>,
pub attempt_id: Option<stack_ids::AttemptId>,
pub trial_id: Option<stack_ids::TrialId>,
pub worker_id: Option<String>,
#[deprecated(
note = "Use attempt_id/trial_id instead. Will be removed when all consumers migrate."
)]
pub attempt_count: u32,
pub(crate) event_emitter: Arc<dyn QueueEventEmitter>,
pub(crate) db: Arc<Mutex<Connection>>,
}
#[allow(deprecated)]
impl JobContext {
pub fn new_direct(job_id: &str) -> Self {
let conn = Connection::open_in_memory().expect("in-memory DB");
let _ = conn.execute_batch(
"CREATE TABLE IF NOT EXISTS queue_jobs (
id TEXT PRIMARY KEY,
priority INTEGER DEFAULT 2,
status TEXT DEFAULT 'processing',
data_json TEXT NOT NULL DEFAULT '{}',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
started_at DATETIME,
completed_at DATETIME,
error_message TEXT
)",
);
Self {
job_id: job_id.to_string(),
trace_id: None,
trace_ctx: None,
attempt_id: Some(stack_ids::AttemptId::generate()),
trial_id: Some(stack_ids::TrialId::generate()),
worker_id: None,
attempt_count: 1,
event_emitter: Arc::new(NoopEventEmitter),
db: Arc::new(Mutex::new(conn)),
}
}
pub fn emit_progress(&self, current: u32, total: u32) {
self.event_emitter
.emit_job_progress(events::JobProgressEvent {
job_id: self.job_id.clone(),
current_step: current,
total_steps: total,
progress: if total > 0 {
current as f64 / total as f64
} else {
0.0
},
trace_id: self.trace_id.clone(),
worker_id: self.worker_id.clone(),
attempt_count: Some(self.attempt_count),
status: Some("processing".to_string()),
trace_ctx: self.trace_ctx.clone(),
attempt_id: self.attempt_id.clone(),
trial_id: self.trial_id.clone(),
});
}
pub fn is_cancelled(&self) -> bool {
match self.db.lock() {
Ok(conn) => db::is_cancelled(&conn, &self.job_id).unwrap_or(false),
Err(_) => false,
}
}
}
pub trait JobHandler: Send + Sync + serde::Serialize + serde::de::DeserializeOwned + Clone {
fn execute(
&self,
ctx: &JobContext,
) -> impl std::future::Future<Output = Result<JobResult, QueueError>> + Send;
fn job_type(&self) -> &str {
std::any::type_name::<Self>()
}
}