Skip to main content

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    /// Set structured progress (0-100 with message). Sync — writes to in-memory buffer.
156    ///
157    /// `percent` is clamped to 0-100. For progress without a message, pass `""`.
158    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    /// Shallow-merge keys into progress.metadata for checkpointing. Sync.
164    ///
165    /// `updates` must be a JSON object. Top-level keys overwrite; nested objects
166    /// are replaced, not deep-merged.
167    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    /// Force immediate flush of pending progress to DB. For critical checkpoints.
182    ///
183    /// Does not return success until the progress has been durably written
184    /// or the job is no longer in running state (rescued/cancelled).
185    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            // Job was rescued/cancelled — not an error for the caller
209            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    /// Get a clone of the shared progress state Arc (for Python bridge).
219    pub fn progress_buffer(&self) -> Arc<std::sync::Mutex<ProgressState>> {
220        self.progress.clone()
221    }
222}