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}