1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3use std::{collections::HashMap, path::PathBuf, time::SystemTime};
4use thiserror::Error;
5
6mod bincode_serde;
7mod validated_path;
8
9pub use bincode_serde::{bounded_bincode, validate_message_size, MessageSizeError};
10pub use validated_path::{PathValidationError, ValidatedPath};
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub enum DaemonCommand {
15 RegisterRepo {
16 repo_path: ValidatedPath,
17 },
18 UnregisterRepo {
19 repo_path: ValidatedPath,
20 },
21 ListRepos,
22 Status {
23 repo_path: ValidatedPath,
24 known_generation: Option<u64>,
25 },
26 QueueJob {
27 repo_path: ValidatedPath,
28 job: JobKind,
29 },
30 HealthCheck,
31 RepoHealth {
33 repo_path: ValidatedPath,
34 },
35 Metrics,
36 FsMonitorSnapshot {
37 repo_path: ValidatedPath,
38 last_seen_generation: Option<u64>,
39 },
40 FetchLogs {
41 repo_path: ValidatedPath,
42 limit: usize,
43 },
44 Shutdown,
46}
47
48#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51pub enum DaemonResponse {
52 Ack(Ack),
53 RepoList(Vec<RepoSummary>),
54 RepoStatus(RepoStatusDetail),
55 RepoStatusUnchanged { repo_path: PathBuf, generation: u64 },
56 Health(DaemonHealth),
57 RepoHealth(RepoHealthDetail),
58 Metrics(DaemonMetrics),
59 FsMonitorSnapshot(FsMonitorSnapshot),
60 Logs(Vec<LogEntry>),
61 Error(String),
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66pub struct Ack {
67 pub message: String,
68}
69
70impl Ack {
71 pub fn new(message: impl Into<String>) -> Self {
72 Self {
73 message: message.into(),
74 }
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub struct DaemonHealth {
81 pub repo_count: usize,
82 pub pending_jobs: usize,
83 pub uptime_seconds: u64,
84 pub repo_generations: Vec<RepoGeneration>,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89pub struct RepoHealthDetail {
90 pub repo_path: PathBuf,
91 pub generation: u64,
92 pub pending_jobs: usize,
93 pub watcher_active: bool,
94 pub last_event: Option<SystemTime>,
95 pub dirty_path_count: usize,
96 pub sled_ok: bool,
97 pub needs_reconciliation: bool,
98 pub throttling_active: bool,
99 pub next_scheduled_job: Option<String>,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104pub struct RepoGeneration {
105 pub repo_path: PathBuf,
106 pub generation: u64,
107}
108
109#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
111pub struct DaemonMetrics {
112 pub jobs: HashMap<JobKind, JobMetrics>,
113 pub global: GlobalMetrics,
114 pub repos: Vec<RepoMetrics>,
115}
116
117impl DaemonMetrics {
118 pub fn new() -> Self {
119 Self {
120 jobs: HashMap::new(),
121 global: GlobalMetrics::default(),
122 repos: Vec::new(),
123 }
124 }
125}
126
127impl Default for DaemonMetrics {
128 fn default() -> Self {
129 Self::new()
130 }
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
135pub struct GlobalMetrics {
136 pub pending_jobs: usize,
137 pub uptime_seconds: u64,
138 pub cpu_percent: f32,
139 pub rss_bytes: u64,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144pub struct RepoMetrics {
145 pub repo_path: PathBuf,
146 pub pending_jobs: usize,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
151pub struct RepoSummary {
152 pub repo_path: PathBuf,
153 pub status: RepoStatus,
154 pub pending_jobs: usize,
155 pub last_event: Option<SystemTime>,
156 pub generation: u64,
157}
158
159impl RepoSummary {
160 pub fn new(repo_path: PathBuf) -> Self {
161 Self {
162 repo_path,
163 status: RepoStatus::Unknown,
164 pending_jobs: 0,
165 last_event: None,
166 generation: 0,
167 }
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
173pub struct RepoStatusDetail {
174 pub repo_path: PathBuf,
175 pub dirty_paths: Vec<PathBuf>,
176 pub generation: u64,
177}
178
179#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181pub struct FsMonitorSnapshot {
182 pub repo_path: PathBuf,
183 pub dirty_paths: Vec<PathBuf>,
184 pub generation: u64,
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
190pub enum RepoStatus {
191 Idle,
192 Busy,
193 Unknown,
194}
195
196impl RepoStatus {
197 pub fn as_str(&self) -> &'static str {
198 match self {
199 RepoStatus::Idle => "idle",
200 RepoStatus::Busy => "busy",
201 RepoStatus::Unknown => "unknown",
202 }
203 }
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
208pub enum JobKind {
209 Prefetch,
210 Maintenance,
211}
212
213impl JobKind {
214 pub const ALL: [JobKind; 2] = [JobKind::Prefetch, JobKind::Maintenance];
215
216 pub fn as_str(self) -> &'static str {
217 match self {
218 JobKind::Prefetch => "prefetch",
219 JobKind::Maintenance => "maintenance",
220 }
221 }
222}
223
224#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
226pub struct JobMetrics {
227 pub spawned: u64,
228 pub completed: u64,
229 pub failed: u64,
230}
231
232#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
234pub enum DaemonNotification {
235 WatchEvent(WatchEventNotification),
236 JobEvent(JobEventNotification),
237 Log(LogEntry),
238 RepoStatus(RepoStatusDetail),
239}
240
241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
243pub struct WatchEventNotification {
244 pub repo_path: PathBuf,
245 pub path: PathBuf,
246 pub kind: WatchEventKind,
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
250pub enum WatchEventKind {
251 Created,
252 Modified,
253 Deleted,
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
258pub struct JobEventNotification {
259 pub repo_path: PathBuf,
260 pub job: JobKind,
261 pub kind: JobEventKind,
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
265pub enum JobEventKind {
266 Queued,
267 Started,
268 Completed,
269 Failed,
270}
271
272#[derive(Debug, Error)]
274pub enum DaemonError {
275 #[error("daemon rejected command: {0}")]
276 Rejected(String),
277 #[error("transport error: {0}")]
278 Transport(String),
279}
280
281#[async_trait]
284pub trait DaemonService: Send + Sync {
285 async fn execute(&self, command: DaemonCommand) -> Result<DaemonResponse, DaemonError>;
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use std::collections::HashMap;
292
293 #[test]
294 fn serde_roundtrip() {
295 let response = DaemonResponse::RepoStatus(RepoStatusDetail {
296 repo_path: PathBuf::from("/tmp/test"),
297 dirty_paths: vec![PathBuf::from("file.txt")],
298 generation: 1,
299 });
300 let serialized = serde_json::to_string(&response).expect("serialize");
301 let roundtrip: DaemonResponse =
302 serde_json::from_str(&serialized).expect("deserialize from json");
303 assert_eq!(response, roundtrip);
304 }
305
306 #[test]
307 fn serde_roundtrip_metrics() {
308 let mut jobs = HashMap::new();
309 let counts = JobMetrics {
310 spawned: 3,
311 completed: 2,
312 failed: 0,
313 };
314 jobs.insert(JobKind::Prefetch, counts);
315 let response = DaemonResponse::Metrics(DaemonMetrics {
316 jobs,
317 global: GlobalMetrics {
318 pending_jobs: 1,
319 uptime_seconds: 2,
320 cpu_percent: 3.0,
321 rss_bytes: 4,
322 },
323 repos: vec![RepoMetrics {
324 repo_path: PathBuf::from("/tmp"),
325 pending_jobs: 1,
326 }],
327 });
328 let serialized = serde_json::to_string(&response).expect("serialize metrics");
329 let roundtrip: DaemonResponse =
330 serde_json::from_str(&serialized).expect("deserialize metrics");
331 assert_eq!(response, roundtrip);
332 }
333
334 #[test]
335 fn serde_roundtrip_watch_notification() {
336 let notification = DaemonNotification::WatchEvent(WatchEventNotification {
337 repo_path: PathBuf::from("/tmp/demo"),
338 path: PathBuf::from("file.txt"),
339 kind: WatchEventKind::Modified,
340 });
341 let serialized =
342 serde_json::to_string(¬ification).expect("serialize watch notification");
343 let roundtrip: DaemonNotification =
344 serde_json::from_str(&serialized).expect("deserialize watch notification");
345 assert_eq!(notification, roundtrip);
346 }
347
348 #[test]
349 fn serde_roundtrip_job_notification() {
350 let notification = DaemonNotification::JobEvent(JobEventNotification {
351 repo_path: PathBuf::from("/tmp/demo"),
352 job: JobKind::Prefetch,
353 kind: JobEventKind::Started,
354 });
355 let serialized = serde_json::to_string(¬ification).expect("serialize job notification");
356 let roundtrip: DaemonNotification =
357 serde_json::from_str(&serialized).expect("deserialize job notification");
358 assert_eq!(notification, roundtrip);
359 }
360
361 #[test]
362 fn serde_roundtrip_status_notification() {
363 let detail = RepoStatusDetail {
364 repo_path: PathBuf::from("/tmp/demo"),
365 dirty_paths: vec![PathBuf::from("file.txt")],
366 generation: 7,
367 };
368 let notification = DaemonNotification::RepoStatus(detail.clone());
369 let serialized =
370 serde_json::to_string(¬ification).expect("serialize status notification");
371 let roundtrip: DaemonNotification =
372 serde_json::from_str(&serialized).expect("deserialize status notification");
373 assert_eq!(notification, roundtrip);
374 if let DaemonNotification::RepoStatus(decoded) = roundtrip {
375 assert_eq!(decoded, detail);
376 } else {
377 panic!("expected status notification");
378 }
379 }
380}
381#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
383pub struct LogEntry {
384 pub repo_path: PathBuf,
385 pub message: String,
386 pub timestamp: SystemTime,
387}