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/// Token representing a registered callback for external webhook completion.
11///
12/// The `id` is a UUID that has been persisted to the database. Pass this ID
13/// to the external system so it can call back to complete/fail/retry the job.
14pub struct CallbackToken {
15    pub id: uuid::Uuid,
16}
17
18/// Context passed to worker handlers during job execution.
19///
20/// Provides access to the job metadata, shared state (e.g., service dependencies),
21/// callback registration, and structured progress reporting.
22pub struct JobContext {
23    /// The raw job row from the database.
24    pub job: JobRow,
25    /// Cancellation flag — set to true when shutdown or deadline is signalled.
26    cancelled: Arc<AtomicBool>,
27    /// Shared state map for dependency injection.
28    state: Arc<HashMap<std::any::TypeId, Box<dyn Any + Send + Sync>>>,
29    /// Database pool for callback registration and progress flush.
30    pool: PgPool,
31    /// Shared progress buffer — written by handler, read by heartbeat service.
32    progress: Arc<std::sync::Mutex<ProgressState>>,
33}
34
35impl JobContext {
36    pub fn new(
37        job: JobRow,
38        cancelled: Arc<AtomicBool>,
39        state: Arc<HashMap<std::any::TypeId, Box<dyn Any + Send + Sync>>>,
40        pool: PgPool,
41        progress: Arc<std::sync::Mutex<ProgressState>>,
42    ) -> Self {
43        Self {
44            job,
45            cancelled,
46            state,
47            pool,
48            progress,
49        }
50    }
51
52    /// Check if this job's execution has been cancelled (shutdown or deadline).
53    pub fn is_cancelled(&self) -> bool {
54        self.cancelled.load(Ordering::SeqCst)
55    }
56
57    /// Clone the shared cancellation flag for language bridges.
58    pub fn cancellation_flag(&self) -> Arc<AtomicBool> {
59        self.cancelled.clone()
60    }
61
62    /// Signal cancellation for this job.
63    pub fn cancel(&self) {
64        self.cancelled.store(true, Ordering::SeqCst);
65    }
66
67    /// Extract a shared state value by type.
68    ///
69    /// State values are registered via `Client::builder().state(value)`.
70    pub fn extract<T: Any + Send + Sync + Clone>(&self) -> Option<T> {
71        self.state
72            .get(&std::any::TypeId::of::<T>())
73            .and_then(|v| v.downcast_ref::<T>())
74            .cloned()
75    }
76
77    /// Get a reference to the database pool.
78    pub fn pool(&self) -> &PgPool {
79        &self.pool
80    }
81
82    /// Register a callback for this job, writing the callback_id to the database
83    /// immediately.
84    ///
85    /// Call this BEFORE sending the callback_id to the external system to avoid
86    /// the race condition where the external system fires before the DB knows
87    /// about the callback.
88    ///
89    /// Returns a `CallbackToken` whose `id` should be included in the URL or
90    /// payload sent to the external system.
91    pub async fn register_callback(&self, timeout: Duration) -> Result<CallbackToken, AwaError> {
92        let callback_id = awa_model::admin::register_callback(
93            &self.pool,
94            self.job.id,
95            self.job.run_lease,
96            timeout,
97        )
98        .await?;
99        Ok(CallbackToken { id: callback_id })
100    }
101
102    /// Register a callback with CEL expressions for automatic resolution.
103    ///
104    /// See [`CallbackConfig`] for expression semantics.
105    pub async fn register_callback_with_config(
106        &self,
107        timeout: Duration,
108        config: &CallbackConfig,
109    ) -> Result<CallbackToken, AwaError> {
110        let callback_id = awa_model::admin::register_callback_with_config(
111            &self.pool,
112            self.job.id,
113            self.job.run_lease,
114            timeout,
115            config,
116        )
117        .await?;
118        Ok(CallbackToken { id: callback_id })
119    }
120
121    /// Set structured progress (0-100, optional message). Sync — writes to in-memory buffer.
122    ///
123    /// `percent` is clamped to 0-100.
124    pub fn set_progress(&self, percent: u8, message: Option<&str>) {
125        let mut guard = self.progress.lock().expect("progress lock poisoned");
126        guard.set_progress(percent, message);
127    }
128
129    /// Shallow-merge keys into progress.metadata for checkpointing. Sync.
130    ///
131    /// `updates` must be a JSON object. Top-level keys overwrite; nested objects
132    /// are replaced, not deep-merged.
133    pub fn update_metadata(&self, updates: serde_json::Value) -> Result<(), AwaError> {
134        let obj = updates
135            .as_object()
136            .ok_or_else(|| AwaError::Validation("update_metadata requires a JSON object".into()))?;
137
138        let mut guard = self.progress.lock().expect("progress lock poisoned");
139        if !guard.merge_metadata(obj) {
140            return Err(AwaError::Validation(
141                "progress.metadata is not a JSON object; cannot merge".into(),
142            ));
143        }
144        Ok(())
145    }
146
147    /// Force immediate flush of pending progress to DB. For critical checkpoints.
148    ///
149    /// Does not return success until the progress has been durably written
150    /// or the job is no longer in running state (rescued/cancelled).
151    pub async fn flush_progress(&self) -> Result<(), AwaError> {
152        let (snapshot, target_generation) = {
153            let guard = self.progress.lock().expect("progress lock poisoned");
154            match guard.pending_snapshot() {
155                Some(pair) => pair,
156                None => return Ok(()),
157            }
158        };
159
160        let result = sqlx::query(
161            r#"
162            UPDATE awa.jobs_hot
163            SET progress = $2
164            WHERE id = $1 AND state = 'running' AND run_lease = $3
165            "#,
166        )
167        .bind(self.job.id)
168        .bind(&snapshot)
169        .bind(self.job.run_lease)
170        .execute(&self.pool)
171        .await?;
172
173        if result.rows_affected() == 0 {
174            // Job was rescued/cancelled — not an error for the caller
175            return Ok(());
176        }
177
178        let mut guard = self.progress.lock().expect("progress lock poisoned");
179        guard.ack(target_generation);
180
181        Ok(())
182    }
183
184    /// Get a clone of the shared progress state Arc (for Python bridge).
185    pub fn progress_buffer(&self) -> Arc<std::sync::Mutex<ProgressState>> {
186        self.progress.clone()
187    }
188}