Skip to main content

khive_db/diagnostics/
disk_guard.rs

1use super::*;
2
3/// The configured write-admission reserve and the volume metadata used to
4/// sample it. The reserve is an admission floor, not reserved disk capacity.
5#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
6pub struct DiskGuardDiagnostics {
7    pub enabled: bool,
8    pub effective_reserve_bytes: Option<u64>,
9    pub reserve_source: Option<String>,
10    pub guard_deadline_ms: Option<u64>,
11    pub deadline_source: Option<String>,
12    pub probe_path: Option<String>,
13    pub volume_identity: Option<String>,
14    pub volume_root: Option<String>,
15    pub unavailable_reason: Option<String>,
16}
17
18impl DiskGuardDiagnostics {
19    pub(super) fn snapshot(pool: &ConnectionPool) -> Self {
20        let Some(policy) = pool.effective_disk_guard_config() else {
21            return Self {
22                enabled: false,
23                effective_reserve_bytes: None,
24                reserve_source: None,
25                guard_deadline_ms: None,
26                deadline_source: None,
27                probe_path: None,
28                volume_identity: None,
29                volume_root: None,
30                unavailable_reason: Some(if pool.config().read_only {
31                    "read-only backend: no disk-write admission".to_string()
32                } else {
33                    "in-memory backend: no filesystem volume to guard".to_string()
34                }),
35            };
36        };
37        let identity = pool.write_admission().volume_identity();
38        let (probe_path, volume_identity, volume_root, unavailable_reason) = match identity {
39            Ok(Some(volume)) => (
40                Some(volume.probe_path().display().to_string()),
41                Some(volume.diagnostic_key()),
42                volume.volume_root().map(|path| path.display().to_string()),
43                None,
44            ),
45            Ok(None) => (
46                None,
47                None,
48                None,
49                Some("writable disk guard has no canonical database path".to_string()),
50            ),
51            Err(error) => (None, None, None, Some(error.to_string())),
52        };
53        Self {
54            enabled: true,
55            effective_reserve_bytes: Some(policy.reserve_bytes),
56            reserve_source: Some(policy.reserve_source.as_str().to_string()),
57            guard_deadline_ms: Some(policy.guard_deadline_ms),
58            deadline_source: Some(policy.deadline_source.as_str().to_string()),
59            probe_path,
60            volume_identity,
61            volume_root,
62            unavailable_reason,
63        }
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use crate::disk_guard::VolumeIdentity;
71    use crate::pool::PoolConfig;
72
73    #[test]
74    fn disk_guard_diagnostics_report_effective_policy_and_stable_volume() {
75        let dir = tempfile::tempdir().expect("tempdir");
76        let path = dir.path().join("disk-guard-diagnostics.db");
77        let captured_volume = VolumeIdentity::resolve(&path).expect("initial volume identity");
78        let pool = ConnectionPool::new(PoolConfig {
79            path: Some(path),
80            disk_guard_config: Some(
81                crate::DiskGuardEnvironment::default()
82                    .resolve(Some(123), Some(500))
83                    .unwrap(),
84            ),
85            write_queue_enabled: Some(false),
86            ..PoolConfig::for_test()
87        })
88        .expect("writable pool");
89        let diagnostics = DiskGuardDiagnostics::snapshot(&pool);
90        assert!(diagnostics.enabled);
91        assert_eq!(diagnostics.effective_reserve_bytes, Some(123));
92        assert_eq!(diagnostics.reserve_source.as_deref(), Some("backend"));
93        assert_eq!(diagnostics.guard_deadline_ms, Some(500));
94        assert_eq!(diagnostics.deadline_source.as_deref(), Some("backend"));
95        assert_eq!(
96            diagnostics.volume_identity,
97            Some(captured_volume.diagnostic_key())
98        );
99        let probe_path = captured_volume.probe_path().display().to_string();
100        assert_eq!(diagnostics.probe_path.as_deref(), Some(probe_path.as_str()));
101        assert!(diagnostics.unavailable_reason.is_none());
102        let json = serde_json::to_value(&diagnostics).unwrap();
103        assert_eq!(json["effective_reserve_bytes"], 123);
104        assert_eq!(json["reserve_source"], "backend");
105        #[cfg(any(unix, windows))]
106        {
107            pool.write_admission()
108                .set_test_current_volume(Some(captured_volume.different_volume_for_test()));
109            let unavailable = DiskGuardDiagnostics::snapshot(&pool);
110            assert!(unavailable.volume_identity.is_none());
111            assert!(unavailable.probe_path.is_none());
112            assert!(unavailable
113                .unavailable_reason
114                .as_deref()
115                .unwrap()
116                .contains("database volume changed"));
117            pool.write_admission().set_test_current_volume(None);
118            assert_eq!(DiskGuardDiagnostics::snapshot(&pool), diagnostics);
119        }
120    }
121}