1pub mod config;
23pub mod db;
24pub mod error;
25pub mod events;
26pub mod executor;
27pub mod queue;
28pub mod types;
29
30pub use config::{QueueConfig, QueueConfigBuilder};
31pub use error::QueueError;
32pub use executor::ProcessedJob;
33pub use queue::QueueManager;
34pub use types::{
35 FailureClass, JobResult, QueueJob, QueueJobDetails, QueueJobStatus, QueuePriority, QueueStats,
36};
37
38use rusqlite::Connection;
39use std::sync::{Arc, Mutex};
40
41pub trait QueueEventEmitter: Send + Sync + 'static {
47 fn emit_job_started(&self, event: events::JobStartedEvent);
49 fn emit_job_completed(&self, event: events::JobCompletedEvent);
51 fn emit_job_failed(&self, event: events::JobFailedEvent);
53 fn emit_job_progress(&self, event: events::JobProgressEvent);
55 fn emit_job_cancelled(&self, event: events::JobCancelledEvent);
57}
58
59pub struct NoopEventEmitter;
63
64impl QueueEventEmitter for NoopEventEmitter {
65 fn emit_job_started(&self, _event: events::JobStartedEvent) {}
66 fn emit_job_completed(&self, _event: events::JobCompletedEvent) {}
67 fn emit_job_failed(&self, _event: events::JobFailedEvent) {}
68 fn emit_job_progress(&self, _event: events::JobProgressEvent) {}
69 fn emit_job_cancelled(&self, _event: events::JobCancelledEvent) {}
70}
71
72pub struct LoggingEventEmitter;
76
77#[allow(deprecated)]
78impl QueueEventEmitter for LoggingEventEmitter {
79 fn emit_job_started(&self, event: events::JobStartedEvent) {
80 let trace = event
82 .trace_ctx
83 .as_ref()
84 .map(|ctx| ctx.trace_id.as_str())
85 .or(event.trace_id.as_deref())
86 .unwrap_or("");
87 let attempt = event
88 .attempt_id
89 .as_ref()
90 .map(|a| a.to_string())
91 .unwrap_or_else(|| {
92 event
93 .attempt_count
94 .map(|c| c.to_string())
95 .unwrap_or_default()
96 });
97 tracing::info!(
98 job_id = %event.job_id,
99 worker_id = event.worker_id.as_deref().unwrap_or(""),
100 trace_id = %trace,
101 attempt = %attempt,
102 trial_id = event.trial_id.as_ref().map(|t| t.to_string()).unwrap_or_default().as_str(),
103 "Job started"
104 );
105 }
106 fn emit_job_completed(&self, event: events::JobCompletedEvent) {
107 let trace = event
108 .trace_ctx
109 .as_ref()
110 .map(|ctx| ctx.trace_id.as_str())
111 .or(event.trace_id.as_deref())
112 .unwrap_or("");
113 let attempt = event
114 .attempt_id
115 .as_ref()
116 .map(|a| a.to_string())
117 .unwrap_or_else(|| {
118 event
119 .attempt_count
120 .map(|c| c.to_string())
121 .unwrap_or_default()
122 });
123 tracing::info!(
124 job_id = %event.job_id,
125 worker_id = event.worker_id.as_deref().unwrap_or(""),
126 trace_id = %trace,
127 attempt = %attempt,
128 trial_id = event.trial_id.as_ref().map(|t| t.to_string()).unwrap_or_default().as_str(),
129 "Job completed"
130 );
131 }
132 fn emit_job_failed(&self, event: events::JobFailedEvent) {
133 let trace = event
134 .trace_ctx
135 .as_ref()
136 .map(|ctx| ctx.trace_id.as_str())
137 .or(event.trace_id.as_deref())
138 .unwrap_or("");
139 let attempt = event
140 .attempt_id
141 .as_ref()
142 .map(|a| a.to_string())
143 .unwrap_or_else(|| {
144 event
145 .attempt_count
146 .map(|c| c.to_string())
147 .unwrap_or_default()
148 });
149 tracing::warn!(
150 job_id = %event.job_id,
151 worker_id = event.worker_id.as_deref().unwrap_or(""),
152 trace_id = %trace,
153 attempt = %attempt,
154 trial_id = event.trial_id.as_ref().map(|t| t.to_string()).unwrap_or_default().as_str(),
155 failure_class = event.failure_class.as_deref().unwrap_or(""),
156 error = %event.error,
157 "Job failed"
158 );
159 }
160 fn emit_job_progress(&self, event: events::JobProgressEvent) {
161 let trace = event
162 .trace_ctx
163 .as_ref()
164 .map(|ctx| ctx.trace_id.as_str())
165 .or(event.trace_id.as_deref())
166 .unwrap_or("");
167 tracing::debug!(
168 job_id = %event.job_id,
169 worker_id = event.worker_id.as_deref().unwrap_or(""),
170 trace_id = %trace,
171 progress = %event.progress,
172 step = %event.current_step,
173 total = %event.total_steps,
174 "Job progress"
175 );
176 }
177 fn emit_job_cancelled(&self, event: events::JobCancelledEvent) {
178 let trace = event
179 .trace_ctx
180 .as_ref()
181 .map(|ctx| ctx.trace_id.as_str())
182 .or(event.trace_id.as_deref())
183 .unwrap_or("");
184 tracing::info!(
185 job_id = %event.job_id,
186 worker_id = event.worker_id.as_deref().unwrap_or(""),
187 trace_id = %trace,
188 "Job cancelled"
189 );
190 }
191}
192
193#[allow(deprecated)]
198pub struct JobContext {
199 pub job_id: String,
201 #[deprecated(note = "Use trace_ctx instead. Will be removed when all consumers migrate.")]
206 pub trace_id: Option<String>,
207 pub trace_ctx: Option<stack_ids::TraceCtx>,
209 pub attempt_id: Option<stack_ids::AttemptId>,
211 pub trial_id: Option<stack_ids::TrialId>,
213 pub worker_id: Option<String>,
215 #[deprecated(
219 note = "Use attempt_id/trial_id instead. Will be removed when all consumers migrate."
220 )]
221 pub attempt_count: u32,
222 pub(crate) event_emitter: Arc<dyn QueueEventEmitter>,
224 pub(crate) db: Arc<Mutex<Connection>>,
226}
227
228#[allow(deprecated)]
229impl JobContext {
230 pub fn new_direct(job_id: &str) -> Self {
236 let conn = Connection::open_in_memory().expect("in-memory DB");
237 let _ = conn.execute_batch(
238 "CREATE TABLE IF NOT EXISTS queue_jobs (
239 id TEXT PRIMARY KEY,
240 priority INTEGER DEFAULT 2,
241 status TEXT DEFAULT 'processing',
242 data_json TEXT NOT NULL DEFAULT '{}',
243 created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
244 started_at DATETIME,
245 completed_at DATETIME,
246 error_message TEXT
247 )",
248 );
249 Self {
250 job_id: job_id.to_string(),
251 trace_id: None,
252 trace_ctx: None,
253 attempt_id: Some(stack_ids::AttemptId::generate()),
254 trial_id: Some(stack_ids::TrialId::generate()),
255 worker_id: None,
256 attempt_count: 1,
257 event_emitter: Arc::new(NoopEventEmitter),
258 db: Arc::new(Mutex::new(conn)),
259 }
260 }
261
262 pub fn emit_progress(&self, current: u32, total: u32) {
268 self.event_emitter
269 .emit_job_progress(events::JobProgressEvent {
270 job_id: self.job_id.clone(),
271 current_step: current,
272 total_steps: total,
273 progress: if total > 0 {
274 current as f64 / total as f64
275 } else {
276 0.0
277 },
278 trace_id: self.trace_id.clone(),
279 worker_id: self.worker_id.clone(),
280 attempt_count: Some(self.attempt_count),
281 status: Some("processing".to_string()),
282 trace_ctx: self.trace_ctx.clone(),
283 attempt_id: self.attempt_id.clone(),
284 trial_id: self.trial_id.clone(),
285 });
286 }
287
288 pub fn is_cancelled(&self) -> bool {
294 match self.db.lock() {
295 Ok(conn) => db::is_cancelled(&conn, &self.job_id).unwrap_or(false),
296 Err(_) => false,
297 }
298 }
299}
300
301pub trait JobHandler: Send + Sync + serde::Serialize + serde::de::DeserializeOwned + Clone {
327 fn execute(
332 &self,
333 ctx: &JobContext,
334 ) -> impl std::future::Future<Output = Result<JobResult, QueueError>> + Send;
335
336 fn job_type(&self) -> &str {
338 std::any::type_name::<Self>()
339 }
340}