Skip to main content

snerd_rust/
queue.rs

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;
7
8use crate::file_store::FileStore;
9use crate::rate_limiter::RateLimiter;
10use crate::task::{RetryableTask, PriorityTask, ProgressMessage};
11use tokio::sync::broadcast;
12
13pub type TaskHandler = Arc<dyn Fn(String) -> Result<(), String> + Send + Sync>;
14pub type MaxRetryHandler = Arc<dyn Fn(String) -> Result<(), String> + Send + Sync>;
15
16struct ExecutingGuard {
17    executing_tasks: Arc<Mutex<HashSet<String>>>,
18    task_id: String,
19}
20
21impl Drop for ExecutingGuard {
22    fn drop(&mut self) {
23        if let Ok(mut executing) = self.executing_tasks.lock() {
24            executing.remove(&self.task_id);
25        }
26    }
27}
28
29#[derive(Clone)]
30pub struct SnerdQueue {
31    pub name: String,
32    pub file_store: FileStore,
33    pub rate_limiter: RateLimiter,
34    task_handlers: Arc<RwLock<HashMap<String, TaskHandler>>>,
35    max_retry_handlers: Arc<RwLock<HashMap<String, MaxRetryHandler>>>,
36    active_hashes: Arc<Mutex<HashSet<String>>>,
37    executing_tasks: Arc<Mutex<HashSet<String>>>,
38    worker_semaphore: Arc<Semaphore>,
39    pub progress_tx: broadcast::Sender<ProgressMessage>,
40}
41
42impl SnerdQueue {
43    pub fn new(name: &str, file_store: FileStore, rate_limiter: RateLimiter) -> Self {
44        let mut initial_hashes = HashSet::new();
45        if let Ok(tasks) = file_store.read_tasks() {
46            for task in tasks {
47                if task.deleted_at.is_none() {
48                    if let Some(hash) = task.payload_hash {
49                        initial_hashes.insert(hash);
50                    }
51                }
52            }
53        }
54
55        let (progress_tx, _) = broadcast::channel(1024);
56        Self {
57            name: name.to_string(),
58            file_store,
59            rate_limiter,
60            task_handlers: Arc::new(RwLock::new(HashMap::new())),
61            max_retry_handlers: Arc::new(RwLock::new(HashMap::new())),
62            active_hashes: Arc::new(Mutex::new(initial_hashes)),
63            executing_tasks: Arc::new(Mutex::new(HashSet::new())),
64            worker_semaphore: Arc::new(Semaphore::new(100)), // Limit to 100 concurrent tasks
65            progress_tx,
66        }
67    }
68
69    pub fn subscribe_progress(&self) -> broadcast::Receiver<ProgressMessage> {
70        self.progress_tx.subscribe()
71    }
72
73    pub fn yield_progress(&self, task_id: &str, data: &str) {
74        let _ = self.progress_tx.send(ProgressMessage {
75            task_id: task_id.to_string(),
76            data: data.to_string(),
77        });
78    }
79
80    pub async fn register_task_handler<F>(&self, task_type: &str, handler: F)
81    where
82        F: Fn(String) -> Result<(), String> + Send + Sync + 'static,
83    {
84        self.task_handlers
85            .write()
86            .await
87            .insert(task_type.to_string(), Arc::new(handler));
88    }
89
90    pub async fn register_max_retry_handler<F>(&self, task_type: &str, handler: F)
91    where
92        F: Fn(String) -> Result<(), String> + Send + Sync + 'static,
93    {
94        self.max_retry_handlers
95            .write()
96            .await
97            .insert(task_type.to_string(), Arc::new(handler));
98    }
99
100    pub fn enqueue(&self, mut task: RetryableTask) -> std::io::Result<()> {
101        if let Some(ref hash) = task.payload_hash {
102            if let Ok(mut hashes) = self.active_hashes.lock() {
103                if hashes.contains(hash) {
104                    return Ok(());
105                }
106                hashes.insert(hash.clone());
107            }
108        }
109        task.deleted_at = None;
110        self.file_store.save_task(&task)?;
111
112        if task.retry_after_time <= Utc::now() {
113            if let Some(ref group) = task.rate_limit_group {
114                if let Some(limit) = task.max_per_minute {
115                    match self.rate_limiter.check_and_increment(group, limit) {
116                        Ok(true) => {}
117                        Ok(false) | Err(_) => {
118                            task.retry_after_time = Utc::now() + chrono::Duration::seconds(60);
119                            let _ = self.file_store.save_task(&task);
120                            return Ok(());
121                        }
122                    }
123                }
124            }
125
126            // Lock check before executing
127            if let Ok(mut executing) = self.executing_tasks.lock() {
128                if executing.contains(&task.task_id) {
129                    return Ok(());
130                }
131                executing.insert(task.task_id.clone());
132            }
133
134            let q = self.clone();
135            tokio::spawn(async move {
136                q.execute_task(task).await;
137            });
138        }
139        Ok(())
140    }
141
142    pub async fn start_processor(&self, interval: Duration) {
143        let q = self.clone();
144        tokio::spawn(async move {
145            let mut interval_timer = tokio::time::interval(interval);
146            loop {
147                interval_timer.tick().await;
148                q.process_due_tasks().await;
149            }
150        });
151    }
152
153    pub async fn process_due_tasks(&self) {
154        let tasks = match self.file_store.read_tasks() {
155            Ok(t) => t,
156            Err(_) => return,
157        };
158
159        let now = Utc::now();
160        let mut heap = BinaryHeap::new();
161        
162        for task in tasks {
163            if task.retry_after_time <= now && task.deleted_at.is_none() {
164                heap.push(PriorityTask(task));
165            }
166        }
167
168        let available = self.worker_semaphore.available_permits();
169        for _ in 0..available {
170            if let Some(PriorityTask(mut task)) = heap.pop() {
171                if let Some(ref group) = task.rate_limit_group {
172                    if let Some(limit) = task.max_per_minute {
173                        match self.rate_limiter.check_and_increment(group, limit) {
174                            Ok(true) => {}
175                            Ok(false) | Err(_) => {
176                                task.retry_after_time = now + chrono::Duration::seconds(60);
177                                let _ = self.file_store.save_task(&task);
178                                continue;
179                            }
180                        }
181                    }
182                }
183
184                // Lock check before executing
185                if let Ok(mut executing) = self.executing_tasks.lock() {
186                    if executing.contains(&task.task_id) {
187                        continue;
188                    }
189                    executing.insert(task.task_id.clone());
190                }
191
192                if let Ok(permit) = self.worker_semaphore.clone().try_acquire_owned() {
193                    let q = self.clone();
194                    tokio::spawn(async move {
195                        let _p = permit;
196                        q.execute_task(task).await;
197                    });
198                }
199            } else {
200                break;
201            }
202        }
203    }
204
205    async fn execute_task(&self, mut task: RetryableTask) {
206        // Drop guard guarantees removal from executing_tasks
207        let _guard = ExecutingGuard {
208            executing_tasks: Arc::clone(&self.executing_tasks),
209            task_id: task.task_id.clone(),
210        };
211
212        let handler = {
213            let handlers = self.task_handlers.read().await;
214            handlers.get(&task.task_type).cloned()
215        };
216
217        if let Some(h) = handler {
218            let task_data = task.task_data.clone();
219
220            let result = tokio::task::spawn_blocking(move || h(task_data))
221                .await
222                .unwrap_or_else(|e| Err(format!("Task panic: {:?}", e)));
223
224            match result {
225                Ok(_) => {
226                    let _ = self.file_store.delete_task(&task.task_id);
227                    if let Some(ref hash) = task.payload_hash {
228                        if let Ok(mut hashes) = self.active_hashes.lock() {
229                            hashes.remove(hash);
230                        }
231                    }
232                }
233                Err(e) => {
234                    if task.retry_count < task.max_retries {
235                        task.update_retry_config(Some(e));
236                        let _ = self.file_store.save_task(&task);
237                    } else {
238                        // Max retries reached
239                        let max_handler = {
240                            let max_handlers = self.max_retry_handlers.read().await;
241                            max_handlers.get(&task.task_type).cloned()
242                        };
243
244                        if let Some(mh) = max_handler {
245                            let max_data = task.task_data.clone();
246                            let _ = tokio::task::spawn_blocking(move || mh(max_data)).await;
247                        }
248
249                        let _ = self.file_store.delete_task(&task.task_id);
250                        if let Some(ref hash) = task.payload_hash {
251                            if let Ok(mut hashes) = self.active_hashes.lock() {
252                                hashes.remove(hash);
253                            }
254                        }
255                    }
256                }
257            }
258        }
259    }
260}