1use chrono::Utc;
2use std::collections::{HashMap, HashSet, BinaryHeap};
3use tokio::sync::Semaphore;
4use std::sync::{Arc, Mutex};
5use std::time::Duration;
6use tokio::sync::RwLock;
7use serde_json::json;
8
9use crate::file_store::FileStore;
10use crate::rate_limiter::RateLimiter;
11use crate::task::{RetryableTask, PriorityTask, ProgressMessage};
12use tokio::sync::broadcast;
13
14pub type TaskHandler = Arc<dyn Fn(String) -> Result<(), String> + Send + Sync>;
15pub type MaxRetryHandler = Arc<dyn Fn(String) -> Result<(), String> + Send + Sync>;
16
17struct ExecutingGuard {
18 executing_tasks: Arc<Mutex<HashSet<String>>>,
19 task_id: String,
20}
21
22impl Drop for ExecutingGuard {
23 fn drop(&mut self) {
24 if let Ok(mut executing) = self.executing_tasks.lock() {
25 executing.remove(&self.task_id);
26 }
27 }
28}
29
30#[derive(Clone)]
31pub struct SnerdQueue {
32 pub name: String,
33 pub file_store: FileStore,
34 pub rate_limiter: RateLimiter,
35 task_handlers: Arc<RwLock<HashMap<String, TaskHandler>>>,
36 max_retry_handlers: Arc<RwLock<HashMap<String, MaxRetryHandler>>>,
37 active_hashes: Arc<Mutex<HashSet<String>>>,
38 executing_tasks: Arc<Mutex<HashSet<String>>>,
39 queued_tasks: Arc<Mutex<HashSet<String>>>,
42 completed_tasks: Arc<Mutex<HashSet<String>>>,
45 worker_semaphore: Arc<Semaphore>,
46 pub progress_tx: broadcast::Sender<ProgressMessage>,
47 shared_pq: Arc<Mutex<BinaryHeap<PriorityTask>>>,
49 dispatcher_count: Arc<std::sync::atomic::AtomicUsize>,
51}
52
53impl SnerdQueue {
54 pub fn new(name: &str, file_store: FileStore, rate_limiter: RateLimiter) -> Self {
55 let mut initial_hashes = HashSet::new();
56 if let Ok(tasks) = file_store.read_tasks() {
57 for task in tasks {
58 if task.deleted_at.is_none() {
59 if let Some(hash) = task.payload_hash {
60 initial_hashes.insert(hash);
61 }
62 }
63 }
64 }
65
66 let (progress_tx, _) = broadcast::channel(1024);
67 Self {
68 name: name.to_string(),
69 file_store,
70 rate_limiter,
71 task_handlers: Arc::new(RwLock::new(HashMap::new())),
72 max_retry_handlers: Arc::new(RwLock::new(HashMap::new())),
73 active_hashes: Arc::new(Mutex::new(initial_hashes)),
74 executing_tasks: Arc::new(Mutex::new(HashSet::new())),
75 queued_tasks: Arc::new(Mutex::new(HashSet::new())),
76 completed_tasks: Arc::new(Mutex::new(HashSet::new())),
77 worker_semaphore: Arc::new(Semaphore::new(100)), progress_tx,
79 shared_pq: Arc::new(Mutex::new(BinaryHeap::new())),
80 dispatcher_count: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
81 }
82 }
83
84 pub fn subscribe_progress(&self) -> broadcast::Receiver<ProgressMessage> {
85 self.progress_tx.subscribe()
86 }
87
88 pub fn yield_progress(&self, task_id: &str, data: &str) {
89 let _ = self.progress_tx.send(ProgressMessage {
90 task_id: task_id.to_string(),
91 data: data.to_string(),
92 });
93 }
94
95 pub async fn register_task_handler<F>(&self, task_type: &str, handler: F)
96 where
97 F: Fn(String) -> Result<(), String> + Send + Sync + 'static,
98 {
99 self.task_handlers
100 .write()
101 .await
102 .insert(task_type.to_string(), Arc::new(handler));
103 }
104
105 pub async fn register_max_retry_handler<F>(&self, task_type: &str, handler: F)
106 where
107 F: Fn(String) -> Result<(), String> + Send + Sync + 'static,
108 {
109 self.max_retry_handlers
110 .write()
111 .await
112 .insert(task_type.to_string(), Arc::new(handler));
113 }
114
115 pub fn enqueue(&self, mut task: RetryableTask) -> std::io::Result<()> {
116 if let Some(ref hash) = task.payload_hash {
117 if let Ok(mut hashes) = self.active_hashes.lock() {
118 if hashes.contains(hash) {
119 return Ok(());
120 }
121 hashes.insert(hash.clone());
122 }
123 }
124 task.deleted_at = None;
125 self.file_store.save_task(&task)?;
126
127 Ok(())
134 }
135
136 pub async fn start_processor(&self, interval: Duration) {
137 let q = self.clone();
138 tokio::spawn(async move {
139 let mut interval_timer = tokio::time::interval(interval);
140 loop {
141 interval_timer.tick().await;
142 q.process_due_tasks().await;
143 }
144 });
145 }
146
147 pub async fn process_due_tasks(&self) {
148 let tasks = match self.file_store.read_tasks() {
149 Ok(t) => t,
150 Err(_) => return,
151 };
152
153 let now = Utc::now();
154
155 {
159 let mut pq = self.shared_pq.lock().unwrap();
160 let mut queued = self.queued_tasks.lock().unwrap();
161 let executing = self.executing_tasks.lock().unwrap();
162 for task in tasks {
163 if task.execute_at <= now
164 && task.retry_after_time <= now
165 && task.deleted_at.is_none()
166 && !executing.contains(&task.task_id)
167 && !queued.contains(&task.task_id)
168 {
169 queued.insert(task.task_id.clone());
170 pq.push(PriorityTask(task));
171 }
172 }
173 }
174
175 let pq_len = self.shared_pq.lock().unwrap().len();
177 if pq_len > 0 && self.dispatcher_count.load(std::sync::atomic::Ordering::Relaxed) < 2 {
178 self.spawn_dispatcher();
179 }
180 }
181
182 fn spawn_dispatcher(&self) {
188 self.dispatcher_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
189 let q = self.clone();
190 tokio::spawn(async move {
191 loop {
192 let permit = match q.worker_semaphore.clone().acquire_owned().await {
194 Ok(p) => p,
195 Err(_) => break,
196 };
197
198 let task = {
200 let mut pq = q.shared_pq.lock().unwrap();
201 pq.pop()
202 };
203
204 match task {
205 Some(PriorityTask(mut task)) => {
206 if let Some(ref group) = task.rate_limit_group {
208 if let Some(limit) = task.max_per_minute {
209 match q.rate_limiter.check_and_increment(group, limit) {
210 Ok(true) => {}
211 Ok(false) | Err(_) => {
212 task.retry_after_time = Utc::now() + chrono::Duration::seconds(60);
213 let _ = q.file_store.save_task(&task);
214 q.queued_tasks.lock().unwrap().remove(&task.task_id);
216 drop(permit);
217 continue;
218 }
219 }
220 }
221 }
222
223 {
225 let mut queued = q.queued_tasks.lock().unwrap();
226 queued.remove(&task.task_id);
227 let mut executing = q.executing_tasks.lock().unwrap();
228 if executing.contains(&task.task_id) {
229 drop(permit);
230 continue;
231 }
232 executing.insert(task.task_id.clone());
233 }
234
235 let q2 = q.clone();
236 tokio::spawn(async move {
237 let _permit = permit;
238 q2.execute_task(task).await;
239 });
240 }
241 None => {
242 drop(permit);
243 break; }
245 }
246 }
247 q.dispatcher_count.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
248 });
249 }
250
251 async fn execute_task(&self, mut task: RetryableTask) {
252 {
254 let completed = self.completed_tasks.lock().unwrap();
255 if completed.contains(&task.task_id) {
256 return; }
258 }
259
260 let _guard = ExecutingGuard {
262 executing_tasks: Arc::clone(&self.executing_tasks),
263 task_id: task.task_id.clone(),
264 };
265
266 let result: Result<(), String> = if let Some(ref url) = task.webhook_url.clone() {
268 let payload = json!({
270 "taskId": task.task_id,
271 "taskType": task.task_type,
272 "data": task.task_data,
273 });
274 let url = url.clone();
275 tokio::task::spawn_blocking(move || {
276 let rt = tokio::runtime::Handle::current();
277 rt.block_on(async {
278 let mut client_builder = reqwest::Client::builder();
279 if let Some(secs) = task.max_execution_seconds {
280 client_builder = client_builder.timeout(std::time::Duration::from_secs(secs));
281 }
282 let client = client_builder.build().unwrap_or_else(|_| reqwest::Client::new());
283
284 match client
285 .post(&url)
286 .header("Content-Type", "application/json")
287 .header("X-SnerdMQ-Event", "Execute")
288 .json(&payload)
289 .send()
290 .await
291 {
292 Ok(resp) if resp.status().is_success() => Ok(()),
293 Ok(resp) => Err(format!("Webhook returned non-2xx status: {}", resp.status())),
294 Err(e) => {
295 if e.is_timeout() {
296 Err(format!("Webhook execution timed out after {} seconds", task.max_execution_seconds.unwrap_or(0)))
297 } else {
298 Err(format!("Webhook request failed: {}", e))
299 }
300 }
301 }
302 })
303 })
304 .await
305 .unwrap_or_else(|e| Err(format!("Webhook task panic: {:?}", e)))
306 } else {
307 let handler = {
309 let handlers = self.task_handlers.read().await;
310 handlers.get(&task.task_type).cloned()
311 };
312 if let Some(h) = handler {
313 let task_data = task.task_data.clone();
314 let fut = tokio::task::spawn_blocking(move || h(task_data));
315
316 if let Some(secs) = task.max_execution_seconds {
317 match tokio::time::timeout(std::time::Duration::from_secs(secs), fut).await {
318 Ok(Ok(res)) => res,
319 Ok(Err(e)) => Err(format!("Task panic: {:?}", e)),
320 Err(_) => Err(format!("Task execution timed out after {} seconds", secs)),
321 }
322 } else {
323 fut.await.unwrap_or_else(|e| Err(format!("Task panic: {:?}", e)))
324 }
325 } else {
326 return; }
328 };
329
330 match result {
331 Ok(_) => {
332 let mut rescheduled = false;
333 if let Some(ref cron_expr) = task.cron_expression {
334 use cron::Schedule;
335 use std::str::FromStr;
336 if let Ok(schedule) = Schedule::from_str(cron_expr) {
337 if let Some(next) = schedule.upcoming(Utc).next() {
338 task.execute_at = next;
339 task.retry_count = 0;
340 task.last_error_obj = None;
341 task.last_job_error = None;
342 let _ = self.file_store.save_task(&task);
343 rescheduled = true;
344 }
345 }
346 }
347
348 if !rescheduled {
349 self.completed_tasks.lock().unwrap().insert(task.task_id.clone());
351 let _ = self.file_store.delete_task(&task.task_id);
352 if let Some(ref hash) = task.payload_hash {
353 if let Ok(mut hashes) = self.active_hashes.lock() {
354 hashes.remove(hash);
355 }
356 }
357 }
358 }
359 Err(e) => {
360 if task.retry_count < task.max_retries - 1 {
364 task.update_retry_config(Some(e));
365 let _ = self.file_store.save_task(&task);
366 } else {
367 if let Some(ref url) = task.webhook_url.clone() {
369 let payload = json!({
370 "taskId": task.task_id,
371 "taskType": task.task_type,
372 "data": task.task_data,
373 });
374 let url = url.clone();
375 tokio::spawn(async move {
376 let _ = reqwest::Client::new()
377 .post(&url)
378 .header("Content-Type", "application/json")
379 .header("X-SnerdMQ-Event", "MaxRetriesReached")
380 .json(&payload)
381 .send()
382 .await;
383 });
384 } else {
385 let max_handler = {
386 let max_handlers = self.max_retry_handlers.read().await;
387 max_handlers.get(&task.task_type).cloned()
388 };
389 if let Some(mh) = max_handler {
390 let dlq_payload = serde_json::to_string(&json!({
392 "taskId": task.task_id,
393 "taskType": task.task_type,
394 "data": task.task_data,
395 })).unwrap_or_else(|_| task.task_data.clone());
396 let _ = tokio::task::spawn_blocking(move || mh(dlq_payload)).await;
397 }
398 }
399
400 self.completed_tasks.lock().unwrap().insert(task.task_id.clone());
402 let _ = self.file_store.delete_task(&task.task_id);
403 if let Some(ref hash) = task.payload_hash {
404 if let Ok(mut hashes) = self.active_hashes.lock() {
405 hashes.remove(hash);
406 }
407 }
408 }
409 }
410 }
411 }
412}