1pub use crate::runtime::ProgressState;
2use awa_model::{AwaError, CallbackConfig, JobRow};
3use sqlx::PgPool;
4use std::any::Any;
5use std::collections::HashMap;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::Arc;
8use std::time::Duration;
9
10#[derive(Debug, Clone)]
15#[non_exhaustive]
16pub struct CallbackGuard {
17 pub id: uuid::Uuid,
18}
19
20impl CallbackGuard {
21 fn new(id: uuid::Uuid) -> Self {
22 Self { id }
23 }
24
25 pub fn id(&self) -> uuid::Uuid {
27 self.id
28 }
29
30 #[cfg(feature = "__python-bridge")]
31 #[doc(hidden)]
32 pub fn from_bridge_token(id: uuid::Uuid) -> Self {
33 Self::new(id)
34 }
35}
36
37#[doc(hidden)]
38pub type CallbackToken = CallbackGuard;
39
40pub struct JobContext {
45 pub job: JobRow,
47 cancelled: Arc<AtomicBool>,
49 state: Arc<HashMap<std::any::TypeId, Box<dyn Any + Send + Sync>>>,
51 pool: PgPool,
53 progress: Arc<std::sync::Mutex<ProgressState>>,
55}
56
57impl JobContext {
58 pub fn new(
59 job: JobRow,
60 cancelled: Arc<AtomicBool>,
61 state: Arc<HashMap<std::any::TypeId, Box<dyn Any + Send + Sync>>>,
62 pool: PgPool,
63 progress: Arc<std::sync::Mutex<ProgressState>>,
64 ) -> Self {
65 Self {
66 job,
67 cancelled,
68 state,
69 pool,
70 progress,
71 }
72 }
73
74 pub fn is_cancelled(&self) -> bool {
76 self.cancelled.load(Ordering::SeqCst)
77 }
78
79 pub fn cancellation_flag(&self) -> Arc<AtomicBool> {
81 self.cancelled.clone()
82 }
83
84 pub fn cancel(&self) {
86 self.cancelled.store(true, Ordering::SeqCst);
87 }
88
89 pub fn extract<T: Any + Send + Sync + Clone>(&self) -> Option<T> {
105 self.state
106 .get(&std::any::TypeId::of::<T>())
107 .and_then(|v| v.downcast_ref::<T>())
108 .cloned()
109 }
110
111 pub fn pool(&self) -> &PgPool {
113 &self.pool
114 }
115
116 pub async fn register_callback(&self, timeout: Duration) -> Result<CallbackGuard, AwaError> {
126 let callback_id = awa_model::admin::register_callback(
127 &self.pool,
128 self.job.id,
129 self.job.run_lease,
130 timeout,
131 )
132 .await?;
133 Ok(CallbackGuard::new(callback_id))
134 }
135
136 pub async fn register_callback_with_config(
140 &self,
141 timeout: Duration,
142 config: &CallbackConfig,
143 ) -> Result<CallbackGuard, AwaError> {
144 let callback_id = awa_model::admin::register_callback_with_config(
145 &self.pool,
146 self.job.id,
147 self.job.run_lease,
148 timeout,
149 config,
150 )
151 .await?;
152 Ok(CallbackGuard::new(callback_id))
153 }
154
155 pub fn set_progress(&self, percent: u8, message: &str) {
159 let mut guard = self.progress.lock().expect("progress lock poisoned");
160 guard.set_progress(percent, Some(message));
161 }
162
163 pub fn update_metadata(&self, updates: serde_json::Value) -> Result<(), AwaError> {
168 let obj = updates
169 .as_object()
170 .ok_or_else(|| AwaError::Validation("update_metadata requires a JSON object".into()))?;
171
172 let mut guard = self.progress.lock().expect("progress lock poisoned");
173 if !guard.merge_metadata(obj) {
174 return Err(AwaError::Validation(
175 "progress.metadata is not a JSON object; cannot merge".into(),
176 ));
177 }
178 Ok(())
179 }
180
181 pub async fn flush_progress(&self) -> Result<(), AwaError> {
186 let (snapshot, target_generation) = {
187 let guard = self.progress.lock().expect("progress lock poisoned");
188 match guard.pending_snapshot() {
189 Some(pair) => pair,
190 None => return Ok(()),
191 }
192 };
193
194 let result = sqlx::query(
195 r#"
196 UPDATE awa.jobs_hot
197 SET progress = $2
198 WHERE id = $1 AND state = 'running' AND run_lease = $3
199 "#,
200 )
201 .bind(self.job.id)
202 .bind(&snapshot)
203 .bind(self.job.run_lease)
204 .execute(&self.pool)
205 .await?;
206
207 if result.rows_affected() == 0 {
208 return Ok(());
210 }
211
212 let mut guard = self.progress.lock().expect("progress lock poisoned");
213 guard.ack(target_generation);
214
215 Ok(())
216 }
217
218 pub fn progress_buffer(&self) -> Arc<std::sync::Mutex<ProgressState>> {
220 self.progress.clone()
221 }
222}