Skip to main content

systemprompt_provider_contracts/
job.rs

1//! [`Job`] contract for scheduled / on-startup background jobs registered
2//! via the `inventory` crate.
3//!
4//! Jobs are dispatched as `&'static dyn Job` from the inventory, so the trait
5//! uses `#[async_trait]`; native `async fn` in traits is not `dyn`-compatible.
6//!
7//! Copyright (c) systemprompt.io — Business Source License 1.1.
8//! See <https://systemprompt.io> for licensing details.
9
10use std::collections::HashMap;
11use std::sync::Arc;
12
13use async_trait::async_trait;
14use serde::{Deserialize, Serialize};
15use systemprompt_identifiers::Actor;
16
17use crate::error::ProviderResult;
18
19#[derive(Debug, Clone)]
20pub struct JobResult {
21    pub success: bool,
22    pub message: Option<String>,
23    pub items_processed: Option<u64>,
24    pub items_failed: Option<u64>,
25    pub duration_ms: u64,
26}
27
28impl JobResult {
29    #[must_use]
30    pub const fn success() -> Self {
31        Self {
32            success: true,
33            message: None,
34            items_processed: None,
35            items_failed: None,
36            duration_ms: 0,
37        }
38    }
39
40    #[must_use]
41    pub fn with_message(mut self, message: impl Into<String>) -> Self {
42        self.message = Some(message.into());
43        self
44    }
45
46    #[must_use]
47    pub const fn with_stats(mut self, processed: u64, failed: u64) -> Self {
48        self.items_processed = Some(processed);
49        self.items_failed = Some(failed);
50        self
51    }
52
53    #[must_use]
54    pub const fn with_duration(mut self, duration_ms: u64) -> Self {
55        self.duration_ms = duration_ms;
56        self
57    }
58
59    #[must_use]
60    pub fn failure(message: impl Into<String>) -> Self {
61        Self {
62            success: false,
63            message: Some(message.into()),
64            items_processed: None,
65            items_failed: None,
66            duration_ms: 0,
67        }
68    }
69}
70
71pub struct JobContext {
72    actor: Actor,
73    db_pool: Arc<dyn std::any::Any + Send + Sync>,
74    app_context: Arc<dyn std::any::Any + Send + Sync>,
75    app_paths: Arc<dyn std::any::Any + Send + Sync>,
76    parameters: HashMap<String, String>,
77    enforce: bool,
78}
79
80impl std::fmt::Debug for JobContext {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        f.debug_struct("JobContext")
83            .field("actor", &self.actor)
84            .field("db_pool", &"<type-erased>")
85            .field("app_context", &"<type-erased>")
86            .field("app_paths", &"<type-erased>")
87            .field("parameters", &self.parameters)
88            .field("enforce", &self.enforce)
89            .finish()
90    }
91}
92
93impl JobContext {
94    #[must_use]
95    pub fn new(
96        actor: Actor,
97        db_pool: Arc<dyn std::any::Any + Send + Sync>,
98        app_context: Arc<dyn std::any::Any + Send + Sync>,
99        app_paths: Arc<dyn std::any::Any + Send + Sync>,
100    ) -> Self {
101        Self {
102            actor,
103            db_pool,
104            app_context,
105            app_paths,
106            parameters: HashMap::new(),
107            enforce: false,
108        }
109    }
110
111    #[must_use]
112    pub const fn enforce(&self) -> bool {
113        self.enforce
114    }
115
116    #[must_use]
117    pub const fn with_enforce(mut self, enforce: bool) -> Self {
118        self.enforce = enforce;
119        self
120    }
121
122    #[must_use]
123    pub const fn actor(&self) -> &Actor {
124        &self.actor
125    }
126
127    #[must_use]
128    pub fn with_parameters(mut self, parameters: HashMap<String, String>) -> Self {
129        self.parameters = parameters;
130        self
131    }
132
133    #[must_use]
134    pub fn db_pool<T: 'static>(&self) -> Option<&T> {
135        self.db_pool.as_ref().downcast_ref::<T>()
136    }
137
138    #[must_use]
139    pub fn app_context<T: 'static>(&self) -> Option<&T> {
140        self.app_context.as_ref().downcast_ref::<T>()
141    }
142
143    #[must_use]
144    pub fn app_paths<T: 'static>(&self) -> Option<&T> {
145        self.app_paths.as_ref().downcast_ref::<T>()
146    }
147
148    #[must_use]
149    pub fn db_pool_arc(&self) -> Arc<dyn std::any::Any + Send + Sync> {
150        Arc::clone(&self.db_pool)
151    }
152
153    #[must_use]
154    pub fn app_context_arc(&self) -> Arc<dyn std::any::Any + Send + Sync> {
155        Arc::clone(&self.app_context)
156    }
157
158    #[must_use]
159    pub fn app_paths_arc(&self) -> Arc<dyn std::any::Any + Send + Sync> {
160        Arc::clone(&self.app_paths)
161    }
162
163    #[must_use]
164    pub const fn parameters(&self) -> &HashMap<String, String> {
165        &self.parameters
166    }
167
168    #[must_use]
169    pub fn get_parameter(&self, key: &str) -> Option<&String> {
170        self.parameters.get(key)
171    }
172
173    pub fn get_parameter_parsed<T: std::str::FromStr>(
174        &self,
175        key: &str,
176    ) -> Result<Option<T>, crate::ProviderError>
177    where
178        T::Err: std::fmt::Display,
179    {
180        self.parameters
181            .get(key)
182            .map(|value| {
183                value.parse().map_err(|e| {
184                    crate::ProviderError::Configuration(format!(
185                        "invalid job parameter {key}={value}: {e}"
186                    ))
187                })
188            })
189            .transpose()
190    }
191}
192
193/// Where a job runs when the scheduler has several replicas.
194///
195/// `Cluster` jobs run once per tick across the whole deployment (one replica
196/// wins the advisory lock). `Node` jobs run on every replica, because their
197/// effect is local to the process or its filesystem.
198#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
199#[serde(rename_all = "lowercase")]
200pub enum JobScope {
201    #[default]
202    Cluster,
203    Node,
204}
205
206#[async_trait]
207pub trait Job: Send + Sync + 'static {
208    fn name(&self) -> &'static str;
209
210    fn description(&self) -> &'static str {
211        ""
212    }
213
214    fn schedule(&self) -> &'static str;
215
216    fn tags(&self) -> Vec<&'static str> {
217        vec![]
218    }
219
220    async fn execute(&self, ctx: &JobContext) -> ProviderResult<JobResult>;
221
222    fn enabled(&self) -> bool {
223        true
224    }
225
226    fn schedulable(&self) -> bool {
227        true
228    }
229
230    fn scope(&self) -> JobScope {
231        JobScope::Cluster
232    }
233}
234
235inventory::collect!(&'static dyn Job);
236
237#[macro_export]
238macro_rules! submit_job {
239    ($job:expr) => {
240        inventory::submit!($job as &'static dyn $crate::Job);
241    };
242}