use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, path::PathBuf, time::SystemTime};
use thiserror::Error;
mod bincode_serde;
mod validated_path;
pub use bincode_serde::{bounded_bincode, validate_message_size, MessageSizeError};
pub use validated_path::{PathValidationError, ValidatedPath};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DaemonCommand {
RegisterRepo {
repo_path: ValidatedPath,
},
UnregisterRepo {
repo_path: ValidatedPath,
},
ListRepos,
Status {
repo_path: ValidatedPath,
known_generation: Option<u64>,
},
QueueJob {
repo_path: ValidatedPath,
job: JobKind,
},
HealthCheck,
RepoHealth {
repo_path: ValidatedPath,
},
Metrics,
FsMonitorSnapshot {
repo_path: ValidatedPath,
last_seen_generation: Option<u64>,
},
FetchLogs {
repo_path: ValidatedPath,
limit: usize,
},
Shutdown,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum DaemonResponse {
Ack(Ack),
RepoList(Vec<RepoSummary>),
RepoStatus(RepoStatusDetail),
RepoStatusUnchanged { repo_path: PathBuf, generation: u64 },
Health(DaemonHealth),
RepoHealth(RepoHealthDetail),
Metrics(DaemonMetrics),
FsMonitorSnapshot(FsMonitorSnapshot),
Logs(Vec<LogEntry>),
Error(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Ack {
pub message: String,
}
impl Ack {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DaemonHealth {
pub repo_count: usize,
pub pending_jobs: usize,
pub uptime_seconds: u64,
pub repo_generations: Vec<RepoGeneration>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RepoHealthDetail {
pub repo_path: PathBuf,
pub generation: u64,
pub pending_jobs: usize,
pub watcher_active: bool,
pub last_event: Option<SystemTime>,
pub dirty_path_count: usize,
pub sled_ok: bool,
pub needs_reconciliation: bool,
pub throttling_active: bool,
pub next_scheduled_job: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RepoGeneration {
pub repo_path: PathBuf,
pub generation: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DaemonMetrics {
pub jobs: HashMap<JobKind, JobMetrics>,
pub global: GlobalMetrics,
pub repos: Vec<RepoMetrics>,
}
impl DaemonMetrics {
pub fn new() -> Self {
Self {
jobs: HashMap::new(),
global: GlobalMetrics::default(),
repos: Vec::new(),
}
}
}
impl Default for DaemonMetrics {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
pub struct GlobalMetrics {
pub pending_jobs: usize,
pub uptime_seconds: u64,
pub cpu_percent: f32,
pub rss_bytes: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RepoMetrics {
pub repo_path: PathBuf,
pub pending_jobs: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RepoSummary {
pub repo_path: PathBuf,
pub status: RepoStatus,
pub pending_jobs: usize,
pub last_event: Option<SystemTime>,
pub generation: u64,
}
impl RepoSummary {
pub fn new(repo_path: PathBuf) -> Self {
Self {
repo_path,
status: RepoStatus::Unknown,
pending_jobs: 0,
last_event: None,
generation: 0,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RepoStatusDetail {
pub repo_path: PathBuf,
pub dirty_paths: Vec<PathBuf>,
pub generation: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FsMonitorSnapshot {
pub repo_path: PathBuf,
pub dirty_paths: Vec<PathBuf>,
pub generation: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum RepoStatus {
Idle,
Busy,
Unknown,
}
impl RepoStatus {
pub fn as_str(&self) -> &'static str {
match self {
RepoStatus::Idle => "idle",
RepoStatus::Busy => "busy",
RepoStatus::Unknown => "unknown",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum JobKind {
Prefetch,
Maintenance,
}
impl JobKind {
pub const ALL: [JobKind; 2] = [JobKind::Prefetch, JobKind::Maintenance];
pub fn as_str(self) -> &'static str {
match self {
JobKind::Prefetch => "prefetch",
JobKind::Maintenance => "maintenance",
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct JobMetrics {
pub spawned: u64,
pub completed: u64,
pub failed: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DaemonNotification {
WatchEvent(WatchEventNotification),
JobEvent(JobEventNotification),
Log(LogEntry),
RepoStatus(RepoStatusDetail),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WatchEventNotification {
pub repo_path: PathBuf,
pub path: PathBuf,
pub kind: WatchEventKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum WatchEventKind {
Created,
Modified,
Deleted,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JobEventNotification {
pub repo_path: PathBuf,
pub job: JobKind,
pub kind: JobEventKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum JobEventKind {
Queued,
Started,
Completed,
Failed,
}
#[derive(Debug, Error)]
pub enum DaemonError {
#[error("daemon rejected command: {0}")]
Rejected(String),
#[error("transport error: {0}")]
Transport(String),
}
#[async_trait]
pub trait DaemonService: Send + Sync {
async fn execute(&self, command: DaemonCommand) -> Result<DaemonResponse, DaemonError>;
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn serde_roundtrip() {
let response = DaemonResponse::RepoStatus(RepoStatusDetail {
repo_path: PathBuf::from("/tmp/test"),
dirty_paths: vec![PathBuf::from("file.txt")],
generation: 1,
});
let serialized = serde_json::to_string(&response).expect("serialize");
let roundtrip: DaemonResponse =
serde_json::from_str(&serialized).expect("deserialize from json");
assert_eq!(response, roundtrip);
}
#[test]
fn serde_roundtrip_metrics() {
let mut jobs = HashMap::new();
let counts = JobMetrics {
spawned: 3,
completed: 2,
failed: 0,
};
jobs.insert(JobKind::Prefetch, counts);
let response = DaemonResponse::Metrics(DaemonMetrics {
jobs,
global: GlobalMetrics {
pending_jobs: 1,
uptime_seconds: 2,
cpu_percent: 3.0,
rss_bytes: 4,
},
repos: vec![RepoMetrics {
repo_path: PathBuf::from("/tmp"),
pending_jobs: 1,
}],
});
let serialized = serde_json::to_string(&response).expect("serialize metrics");
let roundtrip: DaemonResponse =
serde_json::from_str(&serialized).expect("deserialize metrics");
assert_eq!(response, roundtrip);
}
#[test]
fn serde_roundtrip_watch_notification() {
let notification = DaemonNotification::WatchEvent(WatchEventNotification {
repo_path: PathBuf::from("/tmp/demo"),
path: PathBuf::from("file.txt"),
kind: WatchEventKind::Modified,
});
let serialized =
serde_json::to_string(¬ification).expect("serialize watch notification");
let roundtrip: DaemonNotification =
serde_json::from_str(&serialized).expect("deserialize watch notification");
assert_eq!(notification, roundtrip);
}
#[test]
fn serde_roundtrip_job_notification() {
let notification = DaemonNotification::JobEvent(JobEventNotification {
repo_path: PathBuf::from("/tmp/demo"),
job: JobKind::Prefetch,
kind: JobEventKind::Started,
});
let serialized = serde_json::to_string(¬ification).expect("serialize job notification");
let roundtrip: DaemonNotification =
serde_json::from_str(&serialized).expect("deserialize job notification");
assert_eq!(notification, roundtrip);
}
#[test]
fn serde_roundtrip_status_notification() {
let detail = RepoStatusDetail {
repo_path: PathBuf::from("/tmp/demo"),
dirty_paths: vec![PathBuf::from("file.txt")],
generation: 7,
};
let notification = DaemonNotification::RepoStatus(detail.clone());
let serialized =
serde_json::to_string(¬ification).expect("serialize status notification");
let roundtrip: DaemonNotification =
serde_json::from_str(&serialized).expect("deserialize status notification");
assert_eq!(notification, roundtrip);
if let DaemonNotification::RepoStatus(decoded) = roundtrip {
assert_eq!(decoded, detail);
} else {
panic!("expected status notification");
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LogEntry {
pub repo_path: PathBuf,
pub message: String,
pub timestamp: SystemTime,
}