qualia_core_db/solvers/qpu/
dispatcher.rs1use super::{JobStatus, QpuError, QpuJob, QpuResult};
10use std::cmp::Ordering;
11use std::collections::{BinaryHeap, HashMap};
12use std::sync::Arc;
13use tokio::sync::{Mutex, RwLock};
14
15#[derive(Debug, Clone)]
18struct JobState {
19 job: QpuJob,
20 enqueued_at_ms: u64,
21 retries: u32,
22 status: InternalStatus,
23}
24
25#[derive(Debug, Clone, PartialEq)]
26enum InternalStatus {
27 Queued,
28 Submitted,
29 Running,
30 Completed,
31 Failed,
32}
33
34fn now_ms() -> u64 {
35 std::time::SystemTime::now()
36 .duration_since(std::time::UNIX_EPOCH)
37 .map(|d| d.as_millis() as u64)
38 .unwrap_or(0)
39}
40
41#[derive(Debug, Clone)]
44pub struct QueueStats {
45 pub pending_count: usize,
46 pub running_count: usize,
47 pub completed_count: usize,
48}
49
50#[derive(Debug, Clone)]
54pub struct PrioritizedJob(pub QpuJob);
55
56impl PartialEq for PrioritizedJob {
57 fn eq(&self, other: &Self) -> bool {
58 self.0.job_id == other.0.job_id
59 }
60}
61impl Eq for PrioritizedJob {}
62
63impl PartialOrd for PrioritizedJob {
64 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
65 Some(self.cmp(other))
66 }
67}
68
69impl Ord for PrioritizedJob {
70 fn cmp(&self, other: &Self) -> Ordering {
71 let depth_ord = self
75 .0
76 .parameters
77 .circuit_depth
78 .cmp(&other.0.parameters.circuit_depth);
79 if depth_ord != Ordering::Equal {
80 return depth_ord;
81 }
82 self.0.parameters.shots.cmp(&other.0.parameters.shots)
83 }
84}
85
86pub struct JobQueue {
89 pending: Arc<Mutex<BinaryHeap<PrioritizedJob>>>,
90 running: Arc<RwLock<HashMap<String, JobState>>>,
91 completed: Arc<Mutex<Vec<QpuResult>>>,
92}
93
94impl JobQueue {
95 pub fn new() -> Self {
96 Self {
97 pending: Arc::new(Mutex::new(BinaryHeap::new())),
98 running: Arc::new(RwLock::new(HashMap::new())),
99 completed: Arc::new(Mutex::new(Vec::new())),
100 }
101 }
102
103 pub async fn enqueue(&self, job: QpuJob) -> String {
105 let id = job.job_id.clone();
106 self.pending.lock().await.push(PrioritizedJob(job));
107 id
108 }
109
110 pub async fn process_queue<F>(&self, dispatch: F) -> Result<(), QpuError>
114 where
115 F: Fn(&QpuJob) -> Result<String, String>,
116 {
117 let jobs: Vec<QpuJob> = {
118 let mut pending = self.pending.lock().await;
119 let mut jobs = Vec::with_capacity(pending.len());
120 while let Some(job) = pending.pop() {
121 jobs.push(job.0);
122 }
123 jobs
124 };
125
126 for job in jobs {
127 let job_id = job.job_id.clone();
128
129 let mut state = JobState {
130 job: job.clone(),
131 enqueued_at_ms: now_ms(),
132 retries: 0,
133 status: InternalStatus::Queued,
134 };
135
136 let mut dispatch_result = dispatch(&job);
137
138 while dispatch_result.is_err() && state.retries < 3 {
139 state.retries += 1;
140 log::warn!(
141 "Retrying QPU dispatch for {} (retry {})",
142 job_id,
143 state.retries
144 );
145 dispatch_result = dispatch(&job);
146 }
147
148 match dispatch_result {
149 Ok(provider_id) => {
150 state.job.job_id = provider_id.clone();
151 state.status = InternalStatus::Submitted;
152 self.running
153 .write()
154 .await
155 .insert(provider_id.clone(), state.clone());
156
157 let mut running = self.running.write().await;
158 if let Some(s) = running.get_mut(&provider_id) {
159 s.status = InternalStatus::Running;
160 }
161 }
162 Err(e) => {
163 log::error!(
164 "QPU dispatch failed for {} after {} retries: {}",
165 job_id,
166 state.retries,
167 e
168 );
169 state.status = InternalStatus::Failed;
170 let result = QpuResult::failed(job_id, e);
171 self.completed.lock().await.push(result);
172 }
173 }
174 }
175 Ok(())
176 }
177
178 pub async fn record_result(&self, job_id: &str, result: QpuResult) {
180 let mut running = self.running.write().await;
181 if let Some(mut state) = running.remove(job_id) {
182 state.status = if result.error.is_some() {
183 InternalStatus::Failed
184 } else {
185 InternalStatus::Completed
186 };
187 let _duration = now_ms() - state.enqueued_at_ms;
188 }
189 self.completed.lock().await.push(result);
190 }
191
192 pub async fn take_results(&self) -> Vec<QpuResult> {
194 std::mem::take(&mut *self.completed.lock().await)
195 }
196
197 pub async fn stats(&self) -> QueueStats {
198 QueueStats {
199 pending_count: self.pending.lock().await.len(),
200 running_count: self.running.read().await.len(),
201 completed_count: self.completed.lock().await.len(),
202 }
203 }
204}
205
206impl Default for JobQueue {
207 fn default() -> Self {
208 Self::new()
209 }
210}
211
212pub struct Dispatcher {
216 pub queue: Arc<JobQueue>,
217}
218
219impl Dispatcher {
220 pub fn new() -> Self {
221 Self {
222 queue: Arc::new(JobQueue::new()),
223 }
224 }
225
226 pub async fn submit(&self, job: QpuJob) -> String {
227 self.queue.enqueue(job).await
228 }
229
230 pub async fn flush<F>(&self, dispatch: F) -> Result<(), QpuError>
231 where
232 F: Fn(&QpuJob) -> Result<String, String>,
233 {
234 self.queue.process_queue(dispatch).await
235 }
236
237 pub async fn drain_results(&self) -> Vec<QpuResult> {
238 self.queue.take_results().await
239 }
240
241 pub async fn stats(&self) -> QueueStats {
242 self.queue.stats().await
243 }
244}
245
246impl Default for Dispatcher {
247 fn default() -> Self {
248 Self::new()
249 }
250}
251
252pub struct FallbackHandler {
256 pub enabled: bool,
257}
258
259impl FallbackHandler {
260 pub fn new(enabled: bool) -> Self {
261 Self { enabled }
262 }
263
264 pub fn simulate_classically(&self, job: &QpuJob) -> Result<QpuResult, QpuError> {
265 if !self.enabled {
266 return Err(QpuError::Api("Fallback is disabled".into()));
267 }
268 Ok(QpuResult {
269 job_id: job.job_id.clone(),
270 status: JobStatus::Completed,
271 result: Some(super::JobResultData {
272 measurements: vec![],
273 energies: Some(vec![0.0]),
274 metadata: serde_json::json!({"method": "classical_simulation"}),
275 }),
276 completed_at_ms: Some(
277 std::time::SystemTime::now()
278 .duration_since(std::time::UNIX_EPOCH)
279 .map(|d| d.as_millis() as u64)
280 .unwrap_or(0),
281 ),
282 error: None,
283 })
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use crate::solvers::qpu::{JobParameters, ProblemType};
291
292 #[tokio::test]
293 async fn queue_enqueue_and_stats() {
294 let q = JobQueue::new();
295 let job = QpuJob::new(
296 "test-job-1".into(),
297 ProblemType::Annealing,
298 JobParameters::default(),
299 );
300 q.enqueue(job).await;
301 let stats = q.stats().await;
302 assert_eq!(stats.pending_count, 1);
303 }
304
305 #[test]
306 fn fallback_handler_enabled() {
307 let handler = FallbackHandler::new(true);
308 let job = QpuJob::default();
309 let result = handler.simulate_classically(&job).unwrap();
310 assert_eq!(result.status, JobStatus::Completed);
311 }
312
313 #[test]
314 fn fallback_handler_disabled() {
315 let handler = FallbackHandler::new(false);
316 let job = QpuJob::default();
317 assert!(handler.simulate_classically(&job).is_err());
318 }
319
320 #[tokio::test]
321 async fn test_qgroup_heuristic_sorting() {
322 let q = JobQueue::new();
323
324 let mut job1 = QpuJob::new("job1".into(), ProblemType::Vqe, JobParameters::default());
325 job1.parameters.circuit_depth = 10;
326 job1.parameters.shots = 1000;
327
328 let mut job2 = QpuJob::new("job2".into(), ProblemType::Vqe, JobParameters::default());
329 job2.parameters.circuit_depth = 50;
330 job2.parameters.shots = 1000;
331
332 let mut job3 = QpuJob::new("job3".into(), ProblemType::Vqe, JobParameters::default());
333 job3.parameters.circuit_depth = 10;
334 job3.parameters.shots = 2000;
335
336 let mut job4 = QpuJob::new("job4".into(), ProblemType::Vqe, JobParameters::default());
337 job4.parameters.circuit_depth = 50;
338 job4.parameters.shots = 500;
339
340 q.enqueue(job1).await;
342 q.enqueue(job2).await;
343 q.enqueue(job3).await;
344 q.enqueue(job4).await;
345
346 let jobs = {
347 let mut pending = q.pending.lock().await;
348 let mut extracted = Vec::new();
349 while let Some(job) = pending.pop() {
350 extracted.push(job.0);
351 }
352 extracted
353 };
354
355 assert_eq!(jobs[0].job_id, "job2"); assert_eq!(jobs[1].job_id, "job4"); assert_eq!(jobs[2].job_id, "job3"); assert_eq!(jobs[3].job_id, "job1"); }
362}