Skip to main content

forge_core/cron/
context.rs

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