Skip to main content

systemprompt_models/services/
scheduler.rs

1//! Scheduler job configuration and the built-in job set.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use std::collections::HashMap;
7
8use serde::{Deserialize, Serialize};
9use systemprompt_identifiers::UserId;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(deny_unknown_fields)]
13pub struct JobConfig {
14    #[serde(default)]
15    pub extension: Option<String>,
16    pub name: String,
17    #[serde(default)]
18    pub owner: Option<UserId>,
19    #[serde(default = "default_true")]
20    pub enabled: bool,
21    #[serde(default)]
22    pub schedule: Option<String>,
23    /// Opt-in for destructive job actions (e.g. automated IP bans, retention
24    /// deletes). When `false` — the default — such jobs run in
25    /// observe-and-log mode, reporting would-delete counts.
26    #[serde(default)]
27    pub enforce: bool,
28    /// String-valued parameters passed to the job on every run (scheduled,
29    /// bootstrap, and manual). Core jobs read:
30    ///
31    /// | Job | Key | Default |
32    /// |---|---|---|
33    /// | `cleanup_empty_contexts` | `retention_hours` | 24 |
34    /// | `database_cleanup` | `log_retention_days` | 30 |
35    /// | `cleanup_inactive_sessions` | `inactive_hours` | 1 |
36    /// | `mcp_session_cleanup` | `retention_days` | 7 |
37    /// | `cleanup_anonymous_users` | `retention_days` | 30 |
38    #[serde(default)]
39    pub parameters: HashMap<String, String>,
40}
41
42const fn default_true() -> bool {
43    true
44}
45
46impl JobConfig {
47    /// A job with no explicit `owner` runs as the profile `system_admin`,
48    /// resolved per-environment at scheduler start. Set one with
49    /// [`Self::with_owner`] only for a job that must run as a specific user.
50    #[must_use]
51    pub fn new(name: impl Into<String>) -> Self {
52        Self {
53            extension: None,
54            name: name.into(),
55            owner: None,
56            enabled: true,
57            schedule: None,
58            enforce: false,
59            parameters: HashMap::new(),
60        }
61    }
62
63    #[must_use]
64    pub const fn with_enforce(mut self) -> Self {
65        self.enforce = true;
66        self
67    }
68
69    #[must_use]
70    pub fn with_owner(mut self, owner: UserId) -> Self {
71        self.owner = Some(owner);
72        self
73    }
74
75    #[must_use]
76    pub fn with_extension(mut self, extension: impl Into<String>) -> Self {
77        self.extension = Some(extension.into());
78        self
79    }
80
81    #[must_use]
82    pub fn with_schedule(mut self, schedule: impl Into<String>) -> Self {
83        self.schedule = Some(schedule.into());
84        self
85    }
86
87    #[must_use]
88    pub fn with_parameters(mut self, parameters: HashMap<String, String>) -> Self {
89        self.parameters = parameters;
90        self
91    }
92
93    #[must_use]
94    pub const fn disabled(mut self) -> Self {
95        self.enabled = false;
96        self
97    }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct SchedulerConfig {
102    #[serde(default = "default_true")]
103    pub enabled: bool,
104    #[serde(default)]
105    pub jobs: Vec<JobConfig>,
106    #[serde(default = "default_bootstrap_jobs")]
107    pub bootstrap_jobs: Vec<String>,
108    #[serde(default = "default_true")]
109    pub distributed_lock: bool,
110}
111
112fn default_bootstrap_jobs() -> Vec<String> {
113    vec!["cleanup_inactive_sessions".to_owned()]
114}
115
116impl SchedulerConfig {
117    /// The built-in core job set. The four cleanup jobs
118    /// (`cleanup_anonymous_users`, `cleanup_empty_contexts`,
119    /// `cleanup_inactive_sessions`, `database_cleanup`) have no human
120    /// originator, so they carry no explicit `owner` and run as the profile
121    /// `system_admin` resolved per-environment at scheduler start.
122    #[must_use]
123    pub fn with_system_admin() -> Self {
124        Self {
125            enabled: true,
126            jobs: vec![
127                JobConfig::new("cleanup_anonymous_users")
128                    .with_extension("core")
129                    .with_schedule("0 0 3 * * *")
130                    .with_enforce(),
131                JobConfig::new("cleanup_empty_contexts")
132                    .with_extension("core")
133                    .with_schedule("0 0 * * * *")
134                    .with_enforce(),
135                JobConfig::new("cleanup_inactive_sessions")
136                    .with_extension("core")
137                    .with_schedule("0 0 * * * *"),
138                JobConfig::new("database_cleanup")
139                    .with_extension("core")
140                    .with_schedule("0 0 4 * * *")
141                    .with_enforce(),
142            ],
143            bootstrap_jobs: default_bootstrap_jobs(),
144            distributed_lock: true,
145        }
146    }
147}