awa_worker/context.rs
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/// Proof that this job registered an external callback in the database.
11///
12/// The public `id` can be sent to the external system. `#[non_exhaustive]`
13/// keeps external callers from constructing this type directly.
14#[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 /// Return the callback UUID persisted for this job.
26 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
40/// Context passed to worker handlers during job execution.
41///
42/// Provides access to the job metadata, shared state (e.g., service dependencies),
43/// callback registration, and structured progress reporting.
44pub struct JobContext {
45 /// The raw job row from the database.
46 pub job: JobRow,
47 /// Cancellation flag — set to true when shutdown or deadline is signalled.
48 cancelled: Arc<AtomicBool>,
49 /// Shared state map for dependency injection.
50 state: Arc<HashMap<std::any::TypeId, Box<dyn Any + Send + Sync>>>,
51 /// Database pool for callback registration and progress flush.
52 pool: PgPool,
53 /// Shared progress buffer — written by handler, read by heartbeat service.
54 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 /// Check if this job's execution has been cancelled (shutdown or deadline).
75 pub fn is_cancelled(&self) -> bool {
76 self.cancelled.load(Ordering::SeqCst)
77 }
78
79 /// Clone the shared cancellation flag for language bridges.
80 pub fn cancellation_flag(&self) -> Arc<AtomicBool> {
81 self.cancelled.clone()
82 }
83
84 /// Signal cancellation for this job.
85 pub fn cancel(&self) {
86 self.cancelled.store(true, Ordering::SeqCst);
87 }
88
89 /// Extract a shared state value by type.
90 ///
91 /// State values are registered via `Client::builder().state(value)`.
92 /// Register the concrete type you want to extract:
93 ///
94 /// ```ignore
95 /// // Register with the type you'll extract:
96 /// let deps = Arc::new(MyDeps::new());
97 /// Client::builder(pool)
98 /// .state(deps.clone()) // stores Arc<MyDeps>
99 /// .build()?;
100 ///
101 /// // In handler — extract the same type:
102 /// let deps = ctx.extract::<Arc<MyDeps>>().unwrap();
103 /// ```
104 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 /// Get a reference to the database pool.
112 pub fn pool(&self) -> &PgPool {
113 &self.pool
114 }
115
116 /// Register a callback for this job, writing the callback_id to the database
117 /// immediately.
118 ///
119 /// Call this BEFORE sending the callback_id to the external system to avoid
120 /// the race condition where the external system fires before the DB knows
121 /// about the callback.
122 ///
123 /// Returns a `CallbackGuard` whose `id` should be included in the URL or
124 /// payload sent to the external system.
125 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 /// Register a callback with CEL expressions for automatic resolution.
137 ///
138 /// See [`CallbackConfig`] for expression semantics.
139 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 /// Wait for an external callback to resolve, then resume with the payload.
156 ///
157 /// This enables sequential callbacks: the handler can register a callback,
158 /// wait for it, process the result, register another callback, wait again,
159 /// and so on — all within a single handler invocation.
160 ///
161 /// The handler's async task suspends while waiting. The job transitions to
162 /// `waiting_external`. When `resume_external(callback_id, payload)` is
163 /// called, the job transitions back to `running` and this method returns
164 /// the payload.
165 ///
166 /// The handler holds its permit (worker slot) during the wait. For very
167 /// long waits, consider whether the single-shot `WaitForCallback` pattern
168 /// (which releases the permit) is more appropriate.
169 ///
170 /// ```ignore
171 /// let token = ctx.register_callback(Duration::from_secs(3600)).await?;
172 /// send_to_external_system(token.id());
173 /// let payload = ctx.wait_for_callback(token).await?;
174 /// // Handler resumes here with the external system's response
175 /// ```
176 pub async fn wait_for_callback(
177 &self,
178 guard: CallbackGuard,
179 ) -> Result<serde_json::Value, AwaError> {
180 use awa_model::admin::{check_callback_state, enter_callback_wait, CallbackPollResult};
181
182 let callback_id = guard.id();
183
184 // Transition to waiting_external
185 let entered =
186 enter_callback_wait(&self.pool, self.job.id, self.job.run_lease, callback_id).await?;
187
188 if !entered {
189 // The UPDATE didn't match — check for early-resume race.
190 match check_callback_state(&self.pool, self.job.id, callback_id).await? {
191 CallbackPollResult::Resolved(payload) => return Ok(payload),
192 CallbackPollResult::Pending => { /* Already in waiting_external */ }
193 CallbackPollResult::Stale {
194 token,
195 current,
196 state,
197 } => {
198 return Err(AwaError::Validation(format!(
199 "wait_for_callback: token {token} is stale; current callback is {current} in state {state:?}"
200 )));
201 }
202 CallbackPollResult::UnexpectedState { token, state } => {
203 return Err(AwaError::Validation(format!(
204 "wait_for_callback: job is not waiting on callback {token}; state={state:?}"
205 )));
206 }
207 CallbackPollResult::NotFound => {
208 return Err(AwaError::Validation(
209 "job not found during callback wait".into(),
210 ));
211 }
212 }
213 }
214
215 // Poll DB until the callback is resolved (state changes from waiting_external).
216 // resume_external sets state to 'running' and stores payload in metadata.
217 loop {
218 if self.is_cancelled() {
219 return Err(AwaError::Validation(
220 "job cancelled while waiting for callback".into(),
221 ));
222 }
223
224 match check_callback_state(&self.pool, self.job.id, callback_id).await? {
225 CallbackPollResult::Resolved(payload) => return Ok(payload),
226 CallbackPollResult::Pending => {
227 tokio::time::sleep(Duration::from_millis(200)).await;
228 }
229 CallbackPollResult::Stale {
230 token,
231 current,
232 state,
233 } => {
234 return Err(AwaError::Validation(format!(
235 "wait_for_callback: token {token} is stale; current callback is {current} in state {state:?}"
236 )));
237 }
238 CallbackPollResult::UnexpectedState { token, state } => {
239 return Err(AwaError::Validation(format!(
240 "job left wait_for_callback unexpectedly for token {token}: state={state:?}"
241 )));
242 }
243 CallbackPollResult::NotFound => {
244 return Err(AwaError::Validation(
245 "job not found during callback wait".into(),
246 ));
247 }
248 }
249 }
250 }
251
252 /// Set structured progress (0-100 with message). Sync — writes to in-memory buffer.
253 ///
254 /// `percent` is clamped to 0-100. For progress without a message, pass `""`.
255 pub fn set_progress(&self, percent: u8, message: &str) {
256 let mut guard = self.progress.lock().expect("progress lock poisoned");
257 guard.set_progress(percent, Some(message));
258 }
259
260 /// Shallow-merge keys into progress.metadata for checkpointing. Sync.
261 ///
262 /// `updates` must be a JSON object. Top-level keys overwrite; nested objects
263 /// are replaced, not deep-merged.
264 pub fn update_metadata(&self, updates: serde_json::Value) -> Result<(), AwaError> {
265 let obj = updates
266 .as_object()
267 .ok_or_else(|| AwaError::Validation("update_metadata requires a JSON object".into()))?;
268
269 let mut guard = self.progress.lock().expect("progress lock poisoned");
270 if !guard.merge_metadata(obj) {
271 return Err(AwaError::Validation(
272 "progress.metadata is not a JSON object; cannot merge".into(),
273 ));
274 }
275 Ok(())
276 }
277
278 /// Force immediate flush of pending progress to DB. For critical checkpoints.
279 ///
280 /// Does not return success until the progress has been durably written
281 /// or the job is no longer in running state (rescued/cancelled).
282 pub async fn flush_progress(&self) -> Result<(), AwaError> {
283 let (snapshot, target_generation) = {
284 let guard = self.progress.lock().expect("progress lock poisoned");
285 match guard.pending_snapshot() {
286 Some(pair) => pair,
287 None => return Ok(()),
288 }
289 };
290
291 let result = sqlx::query(
292 r#"
293 UPDATE awa.jobs_hot
294 SET progress = $2
295 WHERE id = $1 AND state = 'running' AND run_lease = $3
296 "#,
297 )
298 .bind(self.job.id)
299 .bind(&snapshot)
300 .bind(self.job.run_lease)
301 .execute(&self.pool)
302 .await?;
303
304 if result.rows_affected() == 0 {
305 // Job was rescued/cancelled — not an error for the caller
306 return Ok(());
307 }
308
309 let mut guard = self.progress.lock().expect("progress lock poisoned");
310 guard.ack(target_generation);
311
312 Ok(())
313 }
314
315 /// Get a clone of the shared progress state Arc (for Python bridge).
316 pub fn progress_buffer(&self) -> Arc<std::sync::Mutex<ProgressState>> {
317 self.progress.clone()
318 }
319}