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 worker_semaphore: Arc<Semaphore>,
40 pub progress_tx: broadcast::Sender<ProgressMessage>,
41}
42
43impl SnerdQueue {
44 pub fn new(name: &str, file_store: FileStore, rate_limiter: RateLimiter) -> Self {
45 let mut initial_hashes = HashSet::new();
46 if let Ok(tasks) = file_store.read_tasks() {
47 for task in tasks {
48 if task.deleted_at.is_none() {
49 if let Some(hash) = task.payload_hash {
50 initial_hashes.insert(hash);
51 }
52 }
53 }
54 }
55
56 let (progress_tx, _) = broadcast::channel(1024);
57 Self {
58 name: name.to_string(),
59 file_store,
60 rate_limiter,
61 task_handlers: Arc::new(RwLock::new(HashMap::new())),
62 max_retry_handlers: Arc::new(RwLock::new(HashMap::new())),
63 active_hashes: Arc::new(Mutex::new(initial_hashes)),
64 executing_tasks: Arc::new(Mutex::new(HashSet::new())),
65 worker_semaphore: Arc::new(Semaphore::new(100)), progress_tx,
67 }
68 }
69
70 pub fn subscribe_progress(&self) -> broadcast::Receiver<ProgressMessage> {
71 self.progress_tx.subscribe()
72 }
73
74 pub fn yield_progress(&self, task_id: &str, data: &str) {
75 let _ = self.progress_tx.send(ProgressMessage {
76 task_id: task_id.to_string(),
77 data: data.to_string(),
78 });
79 }
80
81 pub async fn register_task_handler<F>(&self, task_type: &str, handler: F)
82 where
83 F: Fn(String) -> Result<(), String> + Send + Sync + 'static,
84 {
85 self.task_handlers
86 .write()
87 .await
88 .insert(task_type.to_string(), Arc::new(handler));
89 }
90
91 pub async fn register_max_retry_handler<F>(&self, task_type: &str, handler: F)
92 where
93 F: Fn(String) -> Result<(), String> + Send + Sync + 'static,
94 {
95 self.max_retry_handlers
96 .write()
97 .await
98 .insert(task_type.to_string(), Arc::new(handler));
99 }
100
101 pub fn enqueue(&self, mut task: RetryableTask) -> std::io::Result<()> {
102 if let Some(ref hash) = task.payload_hash {
103 if let Ok(mut hashes) = self.active_hashes.lock() {
104 if hashes.contains(hash) {
105 return Ok(());
106 }
107 hashes.insert(hash.clone());
108 }
109 }
110 task.deleted_at = None;
111 self.file_store.save_task(&task)?;
112
113 if task.execute_at <= Utc::now() && task.retry_after_time <= Utc::now() {
114 if let Some(ref group) = task.rate_limit_group {
115 if let Some(limit) = task.max_per_minute {
116 match self.rate_limiter.check_and_increment(group, limit) {
117 Ok(true) => {}
118 Ok(false) | Err(_) => {
119 task.retry_after_time = Utc::now() + chrono::Duration::seconds(60);
120 let _ = self.file_store.save_task(&task);
121 return Ok(());
122 }
123 }
124 }
125 }
126
127 if let Ok(mut executing) = self.executing_tasks.lock() {
129 if executing.contains(&task.task_id) {
130 return Ok(());
131 }
132 executing.insert(task.task_id.clone());
133 }
134
135 let q = self.clone();
136 tokio::spawn(async move {
137 q.execute_task(task).await;
138 });
139 }
140 Ok(())
141 }
142
143 pub async fn start_processor(&self, interval: Duration) {
144 let q = self.clone();
145 tokio::spawn(async move {
146 let mut interval_timer = tokio::time::interval(interval);
147 loop {
148 interval_timer.tick().await;
149 q.process_due_tasks().await;
150 }
151 });
152 }
153
154 pub async fn process_due_tasks(&self) {
155 let tasks = match self.file_store.read_tasks() {
156 Ok(t) => t,
157 Err(_) => return,
158 };
159
160 let now = Utc::now();
161 let mut heap = BinaryHeap::new();
162
163 for task in tasks {
164 if task.execute_at <= now && task.retry_after_time <= now && task.deleted_at.is_none() {
165 heap.push(PriorityTask(task));
166 }
167 }
168
169 let available = self.worker_semaphore.available_permits();
170 for _ in 0..available {
171 if let Some(PriorityTask(mut task)) = heap.pop() {
172 if let Some(ref group) = task.rate_limit_group {
173 if let Some(limit) = task.max_per_minute {
174 match self.rate_limiter.check_and_increment(group, limit) {
175 Ok(true) => {}
176 Ok(false) | Err(_) => {
177 task.retry_after_time = now + chrono::Duration::seconds(60);
178 let _ = self.file_store.save_task(&task);
179 continue;
180 }
181 }
182 }
183 }
184
185 if let Ok(mut executing) = self.executing_tasks.lock() {
187 if let Ok(Some(latest_task)) = self.file_store.get_latest_task(&task.task_id) {
189 if latest_task.execute_at > now || latest_task.retry_after_time > now || latest_task.deleted_at.is_some() {
190 continue;
191 }
192 } else {
193 continue;
195 }
196
197 if executing.contains(&task.task_id) {
198 continue;
199 }
200 executing.insert(task.task_id.clone());
201 }
202
203 if let Ok(permit) = self.worker_semaphore.clone().try_acquire_owned() {
204 let q = self.clone();
205 tokio::spawn(async move {
206 let _p = permit;
207 q.execute_task(task).await;
208 });
209 }
210 } else {
211 break;
212 }
213 }
214 }
215
216 async fn execute_task(&self, mut task: RetryableTask) {
217 let _guard = ExecutingGuard {
219 executing_tasks: Arc::clone(&self.executing_tasks),
220 task_id: task.task_id.clone(),
221 };
222
223 let result: Result<(), String> = if let Some(ref url) = task.webhook_url.clone() {
225 let payload = json!({
227 "taskId": task.task_id,
228 "taskType": task.task_type,
229 "data": task.task_data,
230 });
231 let url = url.clone();
232 tokio::task::spawn_blocking(move || {
233 let rt = tokio::runtime::Handle::current();
234 rt.block_on(async {
235 let mut client_builder = reqwest::Client::builder();
236 if let Some(secs) = task.max_execution_seconds {
237 client_builder = client_builder.timeout(std::time::Duration::from_secs(secs));
238 }
239 let client = client_builder.build().unwrap_or_else(|_| reqwest::Client::new());
240
241 match client
242 .post(&url)
243 .header("Content-Type", "application/json")
244 .header("X-SnerdMQ-Event", "Execute")
245 .json(&payload)
246 .send()
247 .await
248 {
249 Ok(resp) if resp.status().is_success() => Ok(()),
250 Ok(resp) => Err(format!("Webhook returned non-2xx status: {}", resp.status())),
251 Err(e) => {
252 if e.is_timeout() {
253 Err(format!("Webhook execution timed out after {} seconds", task.max_execution_seconds.unwrap_or(0)))
254 } else {
255 Err(format!("Webhook request failed: {}", e))
256 }
257 }
258 }
259 })
260 })
261 .await
262 .unwrap_or_else(|e| Err(format!("Webhook task panic: {:?}", e)))
263 } else {
264 let handler = {
266 let handlers = self.task_handlers.read().await;
267 handlers.get(&task.task_type).cloned()
268 };
269 if let Some(h) = handler {
270 let task_data = task.task_data.clone();
271 let fut = tokio::task::spawn_blocking(move || h(task_data));
272
273 if let Some(secs) = task.max_execution_seconds {
274 match tokio::time::timeout(std::time::Duration::from_secs(secs), fut).await {
275 Ok(Ok(res)) => res,
276 Ok(Err(e)) => Err(format!("Task panic: {:?}", e)),
277 Err(_) => Err(format!("Task execution timed out after {} seconds", secs)),
278 }
279 } else {
280 fut.await.unwrap_or_else(|e| Err(format!("Task panic: {:?}", e)))
281 }
282 } else {
283 return; }
285 };
286
287 match result {
288 Ok(_) => {
289 let mut rescheduled = false;
290 if let Some(ref cron_expr) = task.cron_expression {
291 use cron::Schedule;
292 use std::str::FromStr;
293 if let Ok(schedule) = Schedule::from_str(cron_expr) {
294 if let Some(next) = schedule.upcoming(Utc).next() {
295 task.execute_at = next;
296 task.retry_count = 0;
297 task.last_error_obj = None;
298 task.last_job_error = None;
299 let _ = self.file_store.save_task(&task);
300 rescheduled = true;
301 }
302 }
303 }
304
305 if !rescheduled {
306 let _ = self.file_store.delete_task(&task.task_id);
307 if let Some(ref hash) = task.payload_hash {
308 if let Ok(mut hashes) = self.active_hashes.lock() {
309 hashes.remove(hash);
310 }
311 }
312 }
313 }
314 Err(e) => {
315 if task.retry_count < task.max_retries {
316 task.update_retry_config(Some(e));
317 let _ = self.file_store.save_task(&task);
318 } else {
319 if let Some(ref url) = task.webhook_url.clone() {
321 let payload = json!({
322 "taskId": task.task_id,
323 "taskType": task.task_type,
324 "data": task.task_data,
325 });
326 let url = url.clone();
327 tokio::spawn(async move {
328 let _ = reqwest::Client::new()
329 .post(&url)
330 .header("Content-Type", "application/json")
331 .header("X-SnerdMQ-Event", "MaxRetriesReached")
332 .json(&payload)
333 .send()
334 .await;
335 });
336 } else {
337 let max_handler = {
338 let max_handlers = self.max_retry_handlers.read().await;
339 max_handlers.get(&task.task_type).cloned()
340 };
341 if let Some(mh) = max_handler {
342 let max_data = task.task_data.clone();
343 let _ = tokio::task::spawn_blocking(move || mh(max_data)).await;
344 }
345 }
346
347 let _ = self.file_store.delete_task(&task.task_id);
348 if let Some(ref hash) = task.payload_hash {
349 if let Ok(mut hashes) = self.active_hashes.lock() {
350 hashes.remove(hash);
351 }
352 }
353 }
354 }
355 }
356 }
357}