Skip to main content

zeph_scheduler/
error.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use thiserror::Error;
5
6/// Errors that can occur inside the scheduler subsystem.
7#[derive(Debug, Error)]
8#[non_exhaustive]
9pub enum SchedulerError {
10    /// The provided cron expression could not be parsed.
11    ///
12    /// The inner string contains the original expression and the parser's error message.
13    #[error("invalid cron expression: {0}")]
14    InvalidCron(String),
15
16    /// A low-level `SQLx` error occurred during a database operation.
17    #[error("sqlx error: {0}")]
18    Database(#[from] zeph_db::SqlxError),
19
20    /// A high-level `zeph-db` error occurred (e.g. during migrations or connection setup).
21    #[error("db error: {0}")]
22    Db(#[from] zeph_db::DbError),
23
24    /// The [`crate::TaskHandler`] returned an error during task execution.
25    ///
26    /// The inner string is the human-readable description from the handler.
27    #[error("task execution failed: {0}")]
28    TaskFailed(String),
29
30    /// A job with the given name already exists in the store.
31    ///
32    /// Returned by [`crate::JobStore::insert_job`] on a UNIQUE constraint violation.
33    #[error("job '{0}' already exists")]
34    DuplicateJob(String),
35
36    /// Another `zeph serve` instance is already running with the given PID.
37    ///
38    /// Returned by [`crate::PidFile::acquire`] when the pid file is locked by another process.
39    #[cfg(all(unix, feature = "daemon"))]
40    #[error(
41        "daemon pid file is locked: another zeph serve instance appears to be running (pid {pid})"
42    )]
43    AlreadyRunning {
44        /// PID of the running daemon, as stored in the pid file.
45        pid: u32,
46    },
47
48    /// Failed to detach the daemon process (fork, exec, or I/O redirection error).
49    #[cfg(unix)]
50    #[error("daemon detach failed: {0}")]
51    Detach(String),
52
53    /// A generic I/O error from daemon lifecycle operations (pid file, log file).
54    #[cfg(unix)]
55    #[error("daemon I/O error: {0}")]
56    Io(String),
57
58    /// A task prompt matched an injection pattern and was blocked by the RTW-A defense.
59    ///
60    /// The task is skipped for this tick and logged at `WARN` level. No prompt is
61    /// forwarded to the agent loop.
62    #[error("prompt injection blocked in task '{task_name}': {reason}")]
63    PromptInjectionBlocked {
64        /// Name of the task whose prompt was blocked.
65        task_name: String,
66        /// Description of the pattern that triggered the block.
67        reason: String,
68    },
69
70    /// A task was quarantined by the RTW-A write-fence and skipped for this tick.
71    ///
72    /// Tasks written to the store in the same tick they would execute are held back
73    /// for one tick to prevent write-before-exposed-read re-entry attacks.
74    #[error("task '{task_name}' quarantined by write-fence (written this tick)")]
75    TaskQuarantined {
76        /// Name of the task that was quarantined.
77        task_name: String,
78    },
79}
80
81#[cfg(test)]
82mod tests {
83    use super::SchedulerError;
84
85    #[test]
86    fn database_variant_display() {
87        let inner = zeph_db::SqlxError::RowNotFound;
88        let err = SchedulerError::Database(inner);
89        assert!(
90            err.to_string().starts_with("sqlx error:"),
91            "unexpected display: {err}"
92        );
93    }
94
95    #[test]
96    fn db_variant_display() {
97        let inner = zeph_db::DbError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "test"));
98        let err = SchedulerError::Db(inner);
99        assert!(
100            err.to_string().starts_with("db error:"),
101            "unexpected display: {err}"
102        );
103    }
104}