Skip to main content

agent_queue/
queue.rs

1use crate::{
2    config::QueueConfig,
3    db,
4    error::QueueError,
5    executor::{ProcessedJob, QueueExecutor},
6    types::{QueueJob, QueueJobDetails, QueuePriority, QueueStats},
7    JobHandler, QueueEventEmitter,
8};
9use rusqlite::Connection;
10use std::sync::{Arc, Mutex};
11
12/// High-level queue manager providing the public API.
13///
14/// Create a `QueueManager`, add jobs to it, then call [`spawn()`](Self::spawn)
15/// to start the background executor that processes them.
16///
17/// # Example
18///
19/// ```ignore
20/// let config = QueueConfig::builder()
21///     .with_db_path(PathBuf::from("queue.db"))
22///     .build();
23///
24/// let manager = QueueManager::new(config).unwrap();
25/// let job = QueueJob::new(MyJob { ... });
26/// manager.add(job).unwrap();
27///
28/// manager.spawn::<MyJob>(Arc::new(LoggingEventEmitter));
29/// ```
30pub struct QueueManager {
31    db: Arc<Mutex<Connection>>,
32    executor: Arc<QueueExecutor>,
33    config: QueueConfig,
34}
35
36impl QueueManager {
37    /// Create a new queue manager with the given configuration.
38    ///
39    /// Opens (or creates) the SQLite database and requeues any jobs that
40    /// were interrupted by a previous crash.
41    pub fn new(config: QueueConfig) -> Result<Self, QueueError> {
42        let conn = db::open_database(config.db_path.as_deref())?;
43
44        // Requeue interrupted jobs from a previous crash
45        let requeued = db::requeue_interrupted(&conn)?;
46        if requeued > 0 {
47            tracing::info!(count = requeued, "Requeued interrupted jobs");
48        }
49
50        let db = Arc::new(Mutex::new(conn));
51        let executor = Arc::new(QueueExecutor::new(config.clone(), Arc::clone(&db)));
52
53        Ok(Self {
54            db,
55            executor,
56            config,
57        })
58    }
59
60    /// Add a job to the queue. Returns the job ID.
61    pub fn add<H>(&self, job: QueueJob<H>) -> Result<String, QueueError>
62    where
63        H: JobHandler,
64    {
65        let conn = self.db.lock()?;
66        let data = serde_json::to_value(&job.data)?;
67        db::insert_job_full(
68            &conn,
69            &job.id,
70            job.priority.as_i32(),
71            &data,
72            job.trace_id.as_deref(),
73            job.attempt_id.as_ref().map(|a| a.as_str()),
74            job.trial_id.as_ref().map(|t| t.as_str()),
75        )?;
76        Ok(job.id)
77    }
78
79    /// Cancel a pending or processing job by ID.
80    pub fn cancel(&self, job_id: &str) -> Result<(), QueueError> {
81        let conn = self.db.lock()?;
82        db::cancel_job(&conn, job_id)?;
83        Ok(())
84    }
85
86    /// Reorder a pending job to a new priority.
87    pub fn reorder(&self, job_id: &str, new_priority: QueuePriority) -> Result<(), QueueError> {
88        let conn = self.db.lock()?;
89
90        match db::reorder_pending(&conn, job_id, new_priority.as_i32()) {
91            Ok(true) => Ok(()),
92            Ok(false) => {
93                // Job exists but is not pending — fetch its status for the error message
94                let status = db::get_job(&conn, job_id)
95                    .ok()
96                    .flatten()
97                    .map(|j| j.2)
98                    .unwrap_or_else(|| "unknown".to_string());
99                Err(QueueError::Other(format!(
100                    "Can only reorder pending jobs (job {} is {})",
101                    job_id, status
102                )))
103            }
104            Err(_) => Err(QueueError::NotFound(job_id.to_string())),
105        }
106    }
107
108    /// Pause the queue. The current job will finish, but no new jobs start.
109    pub fn pause(&self) {
110        self.executor.pause();
111    }
112
113    /// Resume the queue after a pause.
114    pub fn resume(&self) {
115        self.executor.resume();
116    }
117
118    /// Check if the queue is currently paused.
119    pub fn is_paused(&self) -> bool {
120        self.executor.is_paused()
121    }
122
123    /// Get all jobs as `(id, status)` pairs, ordered by status then priority.
124    pub fn list_jobs(&self) -> Result<Vec<(String, String)>, QueueError> {
125        let conn = self.db.lock()?;
126        let jobs = db::list_all_jobs(&conn)?;
127        Ok(jobs
128            .into_iter()
129            .map(|(id, status, _)| (id, status))
130            .collect())
131    }
132
133    /// Get all jobs as `(id, status, data_json)` tuples.
134    pub fn list_jobs_with_data(&self) -> Result<Vec<(String, String, String)>, QueueError> {
135        let conn = self.db.lock()?;
136        Ok(db::list_all_jobs(&conn)?)
137    }
138
139    /// Fetch a structured view of a single job for debugging or UI inspection.
140    pub fn get_job_details(&self, job_id: &str) -> Result<Option<QueueJobDetails>, QueueError> {
141        let conn = self.db.lock()?;
142        Ok(db::get_job_details(&conn, job_id)?)
143    }
144
145    /// Prune completed/failed/cancelled jobs older than `days`.
146    /// Returns the number of jobs deleted.
147    pub fn prune(&self, days: u32) -> Result<u32, QueueError> {
148        let conn = self.db.lock()?;
149        Ok(db::prune_old_jobs(&conn, days)?)
150    }
151
152    /// Process the next pending job in the foreground and return the result.
153    ///
154    /// Returns `Ok(None)` if no pending jobs are available.
155    /// Unlike [`spawn()`](Self::spawn), this does not start a background loop —
156    /// it processes exactly one job and returns. Useful for CLI-driven loops
157    /// where cascade logic runs between jobs.
158    pub async fn process_one<H>(
159        &self,
160        event_emitter: &Arc<dyn QueueEventEmitter>,
161    ) -> Result<Option<ProcessedJob>, QueueError>
162    where
163        H: JobHandler,
164    {
165        self.executor.process_one::<H>(event_emitter).await
166    }
167
168    /// Count jobs by status.
169    pub fn count_by_status(&self) -> Result<QueueStats, QueueError> {
170        let conn = self.db.lock()?;
171        Ok(db::count_by_status(&conn)?)
172    }
173
174    /// Signal the executor to shut down gracefully.
175    ///
176    /// The currently running job (if any) will finish, then the background
177    /// loop exits. This is safe to call multiple times.
178    pub fn shutdown(&self) {
179        self.executor.shutdown();
180    }
181
182    /// Check if a shutdown has been requested.
183    pub fn is_shutdown(&self) -> bool {
184        self.executor.is_shutdown()
185    }
186
187    /// Returns the configured worker identifier.
188    pub fn worker_id(&self) -> &str {
189        &self.config.worker_id
190    }
191
192    /// Spawn the background executor and return the manager wrapped in an `Arc`.
193    ///
194    /// The event emitter receives notifications about job lifecycle events.
195    ///
196    /// If the current thread is inside a tokio runtime, the executor loop is
197    /// spawned directly. Otherwise (e.g., during Tauri's synchronous `setup()`
198    /// phase), a dedicated background thread is created automatically.
199    pub fn spawn<H>(self, event_emitter: Arc<dyn QueueEventEmitter>) -> Arc<Self>
200    where
201        H: JobHandler + 'static,
202    {
203        let manager = Arc::new(self);
204        let executor = Arc::clone(&manager.executor);
205        executor.spawn::<H>(event_emitter);
206        manager
207    }
208
209    /// Spawn the background executor on a specific tokio runtime handle.
210    ///
211    /// Use this instead of [`spawn()`](Self::spawn) when you have an explicit
212    /// runtime handle (e.g., from `tauri::async_runtime::handle()`).
213    pub fn spawn_on<H>(
214        self,
215        event_emitter: Arc<dyn QueueEventEmitter>,
216        handle: &tokio::runtime::Handle,
217    ) -> Arc<Self>
218    where
219        H: JobHandler + 'static,
220    {
221        let manager = Arc::new(self);
222        let executor = Arc::clone(&manager.executor);
223        executor.spawn_on::<H>(event_emitter, handle);
224        manager
225    }
226}