1pub use crate::runtime::ProgressState;
2use crate::storage::RuntimeStorage;
3use awa_model::{AwaError, CallbackConfig, JobRow, QueueStorage};
4use sqlx::PgPool;
5use std::any::Any;
6use std::collections::HashMap;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::Arc;
9use std::time::Duration;
10
11#[derive(Debug, Clone)]
16#[non_exhaustive]
17pub struct CallbackGuard {
18 pub id: uuid::Uuid,
19}
20
21impl CallbackGuard {
22 fn new(id: uuid::Uuid) -> Self {
23 Self { id }
24 }
25
26 pub fn id(&self) -> uuid::Uuid {
28 self.id
29 }
30
31 #[cfg(feature = "__python-bridge")]
32 #[doc(hidden)]
33 pub fn from_bridge_token(id: uuid::Uuid) -> Self {
34 Self::new(id)
35 }
36}
37
38#[doc(hidden)]
39pub type CallbackToken = CallbackGuard;
40
41pub struct JobContext {
46 pub job: JobRow,
48 cancelled: Arc<AtomicBool>,
50 state: Arc<HashMap<std::any::TypeId, Box<dyn Any + Send + Sync>>>,
52 pool: PgPool,
54 storage: RuntimeStorage,
56 progress: Arc<std::sync::Mutex<ProgressState>>,
58}
59
60impl JobContext {
61 pub(crate) fn new(
62 job: JobRow,
63 cancelled: Arc<AtomicBool>,
64 state: Arc<HashMap<std::any::TypeId, Box<dyn Any + Send + Sync>>>,
65 pool: PgPool,
66 storage: RuntimeStorage,
67 progress: Arc<std::sync::Mutex<ProgressState>>,
68 ) -> Self {
69 Self {
70 job,
71 cancelled,
72 state,
73 pool,
74 storage,
75 progress,
76 }
77 }
78
79 #[doc(hidden)]
80 pub fn new_for_testing(
81 job: JobRow,
82 cancelled: Arc<AtomicBool>,
83 state: Arc<HashMap<std::any::TypeId, Box<dyn Any + Send + Sync>>>,
84 pool: PgPool,
85 progress: Arc<std::sync::Mutex<ProgressState>>,
86 ) -> Self {
87 Self::new(
88 job,
89 cancelled,
90 state,
91 pool,
92 RuntimeStorage::Canonical,
93 progress,
94 )
95 }
96
97 pub fn is_cancelled(&self) -> bool {
99 self.cancelled.load(Ordering::SeqCst)
100 }
101
102 pub fn cancellation_flag(&self) -> Arc<AtomicBool> {
104 self.cancelled.clone()
105 }
106
107 pub fn cancel(&self) {
109 self.cancelled.store(true, Ordering::SeqCst);
110 }
111
112 pub fn extract<T: Any + Send + Sync + Clone>(&self) -> Option<T> {
128 self.state
129 .get(&std::any::TypeId::of::<T>())
130 .and_then(|v| v.downcast_ref::<T>())
131 .cloned()
132 }
133
134 pub fn pool(&self) -> &PgPool {
136 &self.pool
137 }
138
139 pub fn queue_storage_store(&self) -> Option<Arc<QueueStorage>> {
141 self.storage.queue_storage_store()
142 }
143
144 pub async fn register_callback(&self, timeout: Duration) -> Result<CallbackGuard, AwaError> {
154 let callback_id = match &self.storage {
155 RuntimeStorage::Canonical => {
156 awa_model::admin::register_callback(
157 &self.pool,
158 self.job.id,
159 self.job.run_lease,
160 timeout,
161 )
162 .await?
163 }
164 RuntimeStorage::QueueStorage(runtime) => {
165 runtime
166 .store
167 .register_callback(&self.pool, self.job.id, self.job.run_lease, timeout)
168 .await?
169 }
170 };
171 Ok(CallbackGuard::new(callback_id))
172 }
173
174 pub async fn register_callback_with_config(
178 &self,
179 timeout: Duration,
180 config: &CallbackConfig,
181 ) -> Result<CallbackGuard, AwaError> {
182 let callback_id = match &self.storage {
183 RuntimeStorage::Canonical => {
184 awa_model::admin::register_callback_with_config(
185 &self.pool,
186 self.job.id,
187 self.job.run_lease,
188 timeout,
189 config,
190 )
191 .await?
192 }
193 RuntimeStorage::QueueStorage(runtime) => {
194 runtime
195 .store
196 .register_callback_with_config(
197 &self.pool,
198 self.job.id,
199 self.job.run_lease,
200 timeout,
201 config,
202 )
203 .await?
204 }
205 };
206 Ok(CallbackGuard::new(callback_id))
207 }
208
209 pub async fn wait_for_callback(
231 &self,
232 guard: CallbackGuard,
233 ) -> Result<serde_json::Value, AwaError> {
234 use awa_model::admin::CallbackPollResult;
235
236 let callback_id = guard.id();
237
238 let entered = match &self.storage {
239 RuntimeStorage::Canonical => {
240 awa_model::admin::enter_callback_wait(
241 &self.pool,
242 self.job.id,
243 self.job.run_lease,
244 callback_id,
245 )
246 .await?
247 }
248 RuntimeStorage::QueueStorage(runtime) => {
249 runtime
250 .store
251 .enter_callback_wait(&self.pool, self.job.id, self.job.run_lease, callback_id)
252 .await?
253 }
254 };
255
256 let check_state = || async {
257 match &self.storage {
258 RuntimeStorage::Canonical => {
259 awa_model::admin::check_callback_state(&self.pool, self.job.id, callback_id)
260 .await
261 }
262 RuntimeStorage::QueueStorage(runtime) => {
263 runtime
264 .store
265 .check_callback_state(&self.pool, self.job.id, callback_id)
266 .await
267 }
268 }
269 };
270
271 if !entered {
272 match check_state().await? {
273 CallbackPollResult::Resolved(payload) => return Ok(payload),
274 CallbackPollResult::Pending => { }
275 CallbackPollResult::Stale {
276 token,
277 current,
278 state,
279 } => {
280 return Err(AwaError::Validation(format!(
281 "wait_for_callback: token {token} is stale; current callback is {current} in state {state:?}"
282 )));
283 }
284 CallbackPollResult::UnexpectedState { token, state } => {
285 return Err(AwaError::Validation(format!(
286 "wait_for_callback: job is not waiting on callback {token}; state={state:?}"
287 )));
288 }
289 CallbackPollResult::NotFound => {
290 return Err(AwaError::Validation(
291 "job not found during callback wait".into(),
292 ));
293 }
294 }
295 }
296
297 loop {
298 if self.is_cancelled() {
299 return Err(AwaError::Validation(
300 "job cancelled while waiting for callback".into(),
301 ));
302 }
303
304 match check_state().await? {
305 CallbackPollResult::Resolved(payload) => return Ok(payload),
306 CallbackPollResult::Pending => {
307 tokio::time::sleep(Duration::from_millis(200)).await;
308 }
309 CallbackPollResult::Stale {
310 token,
311 current,
312 state,
313 } => {
314 return Err(AwaError::Validation(format!(
315 "wait_for_callback: token {token} is stale; current callback is {current} in state {state:?}"
316 )));
317 }
318 CallbackPollResult::UnexpectedState { token, state } => {
319 return Err(AwaError::Validation(format!(
320 "job left wait_for_callback unexpectedly for token {token}: state={state:?}"
321 )));
322 }
323 CallbackPollResult::NotFound => {
324 return Err(AwaError::Validation(
325 "job not found during callback wait".into(),
326 ));
327 }
328 }
329 }
330 }
331
332 pub fn set_progress(&self, percent: u8, message: &str) {
336 let mut guard = self.progress.lock().expect("progress lock poisoned");
337 guard.set_progress(percent, Some(message));
338 }
339
340 pub fn update_metadata(&self, updates: serde_json::Value) -> Result<(), AwaError> {
345 let obj = updates
346 .as_object()
347 .ok_or_else(|| AwaError::Validation("update_metadata requires a JSON object".into()))?;
348
349 let mut guard = self.progress.lock().expect("progress lock poisoned");
350 if !guard.merge_metadata(obj) {
351 return Err(AwaError::Validation(
352 "progress.metadata is not a JSON object; cannot merge".into(),
353 ));
354 }
355 Ok(())
356 }
357
358 pub async fn flush_progress(&self) -> Result<(), AwaError> {
363 let (snapshot, target_generation) = {
364 let guard = self.progress.lock().expect("progress lock poisoned");
365 match guard.pending_snapshot() {
366 Some(pair) => pair,
367 None => return Ok(()),
368 }
369 };
370
371 match &self.storage {
372 RuntimeStorage::Canonical => {
373 let result = sqlx::query(
374 r#"
375 UPDATE awa.jobs_hot
376 SET progress = $2
377 WHERE id = $1 AND state = 'running' AND run_lease = $3
378 "#,
379 )
380 .bind(self.job.id)
381 .bind(&snapshot)
382 .bind(self.job.run_lease)
383 .execute(&self.pool)
384 .await?;
385
386 if result.rows_affected() == 0 {
387 return Ok(());
388 }
389 }
390 RuntimeStorage::QueueStorage(runtime) => {
391 runtime
392 .store
393 .flush_progress(
394 &self.pool,
395 self.job.id,
396 self.job.run_lease,
397 snapshot.clone(),
398 )
399 .await?;
400 }
401 }
402
403 let mut guard = self.progress.lock().expect("progress lock poisoned");
404 guard.ack(target_generation);
405
406 Ok(())
407 }
408
409 pub fn progress_buffer(&self) -> Arc<std::sync::Mutex<ProgressState>> {
411 self.progress.clone()
412 }
413}