forge_core/cron/
context.rs

1use chrono::{DateTime, Utc};
2use uuid::Uuid;
3
4use crate::function::AuthContext;
5
6/// Context available to cron handlers.
7pub struct CronContext {
8    /// Cron run ID.
9    pub run_id: Uuid,
10    /// Cron name.
11    pub cron_name: String,
12    /// Scheduled time (when the cron was supposed to run).
13    pub scheduled_time: DateTime<Utc>,
14    /// Actual execution time.
15    pub execution_time: DateTime<Utc>,
16    /// Timezone of the cron.
17    pub timezone: String,
18    /// Whether this is a catch-up run.
19    pub is_catch_up: bool,
20    /// Authentication context.
21    pub auth: AuthContext,
22    /// Database pool.
23    db_pool: sqlx::PgPool,
24    /// HTTP client.
25    http_client: reqwest::Client,
26    /// Structured logger.
27    pub log: CronLog,
28}
29
30impl CronContext {
31    /// Create a new cron context.
32    pub fn new(
33        run_id: Uuid,
34        cron_name: String,
35        scheduled_time: DateTime<Utc>,
36        timezone: String,
37        is_catch_up: bool,
38        db_pool: sqlx::PgPool,
39        http_client: reqwest::Client,
40    ) -> Self {
41        Self {
42            run_id,
43            cron_name: cron_name.clone(),
44            scheduled_time,
45            execution_time: Utc::now(),
46            timezone,
47            is_catch_up,
48            auth: AuthContext::unauthenticated(),
49            db_pool,
50            http_client,
51            log: CronLog::new(cron_name),
52        }
53    }
54
55    /// Get the database pool.
56    pub fn db(&self) -> &sqlx::PgPool {
57        &self.db_pool
58    }
59
60    /// Get the HTTP client.
61    pub fn http(&self) -> &reqwest::Client {
62        &self.http_client
63    }
64
65    /// Get the delay between scheduled and actual execution time.
66    pub fn delay(&self) -> chrono::Duration {
67        self.execution_time - self.scheduled_time
68    }
69
70    /// Check if the cron is running late (more than 1 minute delay).
71    pub fn is_late(&self) -> bool {
72        self.delay() > chrono::Duration::minutes(1)
73    }
74
75    /// Set authentication context.
76    pub fn with_auth(mut self, auth: AuthContext) -> Self {
77        self.auth = auth;
78        self
79    }
80}
81
82/// Structured logger for cron jobs.
83#[derive(Clone)]
84pub struct CronLog {
85    cron_name: String,
86}
87
88impl CronLog {
89    /// Create a new cron logger.
90    pub fn new(cron_name: String) -> Self {
91        Self { cron_name }
92    }
93
94    /// Log an info message.
95    pub fn info(&self, message: &str, data: serde_json::Value) {
96        tracing::info!(
97            cron_name = %self.cron_name,
98            data = %data,
99            "{}",
100            message
101        );
102    }
103
104    /// Log a warning message.
105    pub fn warn(&self, message: &str, data: serde_json::Value) {
106        tracing::warn!(
107            cron_name = %self.cron_name,
108            data = %data,
109            "{}",
110            message
111        );
112    }
113
114    /// Log an error message.
115    pub fn error(&self, message: &str, data: serde_json::Value) {
116        tracing::error!(
117            cron_name = %self.cron_name,
118            data = %data,
119            "{}",
120            message
121        );
122    }
123
124    /// Log a debug message.
125    pub fn debug(&self, message: &str, data: serde_json::Value) {
126        tracing::debug!(
127            cron_name = %self.cron_name,
128            data = %data,
129            "{}",
130            message
131        );
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[tokio::test]
140    async fn test_cron_context_creation() {
141        let pool = sqlx::postgres::PgPoolOptions::new()
142            .max_connections(1)
143            .connect_lazy("postgres://localhost/nonexistent")
144            .expect("Failed to create mock pool");
145
146        let run_id = Uuid::new_v4();
147        let scheduled = Utc::now() - chrono::Duration::seconds(30);
148
149        let ctx = CronContext::new(
150            run_id,
151            "test_cron".to_string(),
152            scheduled,
153            "UTC".to_string(),
154            false,
155            pool,
156            reqwest::Client::new(),
157        );
158
159        assert_eq!(ctx.run_id, run_id);
160        assert_eq!(ctx.cron_name, "test_cron");
161        assert!(!ctx.is_catch_up);
162    }
163
164    #[tokio::test]
165    async fn test_cron_delay() {
166        let pool = sqlx::postgres::PgPoolOptions::new()
167            .max_connections(1)
168            .connect_lazy("postgres://localhost/nonexistent")
169            .expect("Failed to create mock pool");
170
171        let scheduled = Utc::now() - chrono::Duration::minutes(5);
172
173        let ctx = CronContext::new(
174            Uuid::new_v4(),
175            "test_cron".to_string(),
176            scheduled,
177            "UTC".to_string(),
178            false,
179            pool,
180            reqwest::Client::new(),
181        );
182
183        assert!(ctx.is_late());
184        assert!(ctx.delay() >= chrono::Duration::minutes(5));
185    }
186
187    #[test]
188    fn test_cron_log() {
189        let log = CronLog::new("test_cron".to_string());
190        log.info("Test message", serde_json::json!({"key": "value"}));
191    }
192}