Skip to main content

linkrs_storage/engine/persistence/
diagnostics.rs

1use linkrs_core::{StorageError, StorageResult};
2use linkrs_transaction::wal::Lsn;
3
4use crate::engine::persistence_coordinator::{CheckpointStats, PersistenceState};
5
6#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
7pub struct SnapshotStats {
8    pub snapshot_count: usize,
9    pub total_size_bytes: u64,
10    pub latest_snapshot_id: Option<u64>,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct PersistenceDiagnostics {
15    pub state: PersistenceState,
16    pub checkpoint_sequence: u64,
17    pub safe_lsn: Lsn,
18    pub last_checkpoint_error: Option<String>,
19    pub last_snapshot_error: Option<String>,
20    pub temporary_checkpoint_count: usize,
21    pub last_checkpoint_stats: Option<CheckpointStats>,
22    /// Number of catalog lock acquisitions observed by the storage engine.
23    pub catalog_lock_acquisitions: u64,
24    /// Total time spent waiting for catalog locks, in nanoseconds.
25    pub catalog_lock_wait_nanos: u64,
26    /// Total time catalog guards were held, in nanoseconds.
27    pub catalog_lock_hold_nanos: u64,
28    /// Number of catalog acquisitions that observed measurable contention.
29    pub catalog_lock_contentions: u64,
30    /// Lock metrics split by catalog operation type.
31    pub catalog_lock_by_operation: Vec<CatalogLockDiagnostic>,
32    /// Edge-table read lock acquisitions across all partitions.
33    pub table_lock_read_acquisitions: u64,
34    /// Total time spent waiting for edge-table read locks, in nanoseconds.
35    pub table_lock_read_wait_nanos: u64,
36    /// Edge-table read acquisitions that observed measurable contention.
37    pub table_lock_read_contentions: u64,
38    /// Edge-table write lock acquisitions across all partitions.
39    pub table_lock_write_acquisitions: u64,
40    /// Total time spent waiting for edge-table write locks, in nanoseconds.
41    pub table_lock_write_wait_nanos: u64,
42    /// Edge-table write acquisitions that observed measurable contention.
43    pub table_lock_write_contentions: u64,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct CheckpointDiagnostics {
48    pub last_stats: Option<CheckpointStats>,
49    pub pending_checkpoint: bool,
50    pub last_trigger_age_secs: Option<u64>,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct CatalogLockDiagnostic {
55    pub operation: String,
56    pub acquisitions: u64,
57    pub wait_nanos: u64,
58    pub hold_nanos: u64,
59    pub contentions: u64,
60}
61
62impl crate::engine::persistence_coordinator::PersistenceCoordinator {
63    pub fn verify_snapshot(&self, snapshot_id: u64) -> StorageResult<bool> {
64        let snapshot_manager = self
65            .snapshot_manager
66            .as_ref()
67            .ok_or_else(|| StorageError::not_supported("Snapshots are not enabled"))?;
68
69        snapshot_manager.verify_snapshot(snapshot_id)
70    }
71
72    pub fn cleanup_old_snapshots(&self) -> StorageResult<usize> {
73        let snapshot_manager = self
74            .snapshot_manager
75            .as_ref()
76            .ok_or_else(|| StorageError::not_supported("Snapshots are not enabled"))?;
77
78        snapshot_manager.cleanup_old_snapshots()
79    }
80
81    /// Remove published checkpoints older than the retention limit while
82    /// keeping the newest valid recovery points.
83    pub fn cleanup_old_checkpoints(&self, max_checkpoints: usize) -> StorageResult<usize> {
84        let keep = max_checkpoints.max(1);
85        let current_sequence = self.checkpoint_manager.read().current_seq();
86        let retained_by_snapshot = self
87            .snapshot_manager
88            .as_ref()
89            .map(|manager| manager.retained_checkpoint_sequences())
90            .unwrap_or_default();
91        let mut checkpoints: Vec<(u64, std::path::PathBuf)> =
92            std::fs::read_dir(&self.config.checkpoint_dir)?
93                .filter_map(Result::ok)
94                .filter_map(|entry| {
95                    let path = entry.path();
96                    let name = path.file_name()?.to_str()?;
97                    let sequence = name.strip_prefix("checkpoint_")?.parse::<u64>().ok()?;
98                    path.is_dir().then_some((sequence, path))
99                })
100                .collect();
101        checkpoints.sort_by_key(|(sequence, _)| *sequence);
102        let remove_count = checkpoints.len().saturating_sub(keep);
103        let mut removed = 0;
104        for (_sequence, path) in checkpoints.into_iter().filter(|(sequence, _)| {
105            *sequence != current_sequence && !retained_by_snapshot.contains(sequence)
106        }) {
107            if removed >= remove_count {
108                break;
109            }
110            std::fs::remove_dir_all(path)?;
111            removed += 1;
112        }
113        if removed > 0 {
114            super::fs_util::sync_directory(&self.config.checkpoint_dir)?;
115        }
116        Ok(removed)
117    }
118
119    pub fn snapshot_stats(&self) -> SnapshotStats {
120        if let Some(snapshot_manager) = self.snapshot_manager.as_ref() {
121            SnapshotStats {
122                snapshot_count: snapshot_manager.snapshot_count(),
123                total_size_bytes: snapshot_manager.total_snapshot_size(),
124                latest_snapshot_id: snapshot_manager.get_latest_snapshot().map(|info| info.id),
125            }
126        } else {
127            SnapshotStats::default()
128        }
129    }
130
131    pub fn diagnostics(&self) -> PersistenceDiagnostics {
132        let temporary_checkpoint_count = std::fs::read_dir(&self.config.checkpoint_dir)
133            .map(|entries| {
134                entries
135                    .filter_map(Result::ok)
136                    .filter(|entry| {
137                        entry
138                            .file_name()
139                            .to_str()
140                            .is_some_and(|name| name.ends_with(".tmp"))
141                    })
142                    .count()
143            })
144            .unwrap_or(0);
145        let manifest_safe_lsn = self
146            .manifest_manager
147            .latest_safe_lsn()
148            .map(|lsn| Lsn::new(lsn.get()))
149            .unwrap_or_else(|_| *self.last_checkpoint_lsn.read());
150        PersistenceDiagnostics {
151            state: *self.state.read(),
152            checkpoint_sequence: self.checkpoint_manager.read().current_seq(),
153            safe_lsn: manifest_safe_lsn,
154            last_checkpoint_error: self.last_checkpoint_error.read().clone(),
155            last_snapshot_error: self.last_snapshot_error.read().clone(),
156            temporary_checkpoint_count,
157            last_checkpoint_stats: self.last_checkpoint_stats.read().clone(),
158            catalog_lock_acquisitions: 0,
159            catalog_lock_wait_nanos: 0,
160            catalog_lock_hold_nanos: 0,
161            catalog_lock_contentions: 0,
162            catalog_lock_by_operation: Vec::new(),
163            table_lock_read_acquisitions: 0,
164            table_lock_read_wait_nanos: 0,
165            table_lock_read_contentions: 0,
166            table_lock_write_acquisitions: 0,
167            table_lock_write_wait_nanos: 0,
168            table_lock_write_contentions: 0,
169        }
170    }
171
172    pub fn mark_flushed(&self, lsn: Lsn) {
173        *self.last_flush_lsn.write() = lsn;
174        *self.last_flush_time.write() = std::time::Instant::now();
175    }
176
177    pub fn mark_checkpointed(&self, lsn: Lsn) {
178        *self.last_checkpoint_lsn.write() = lsn;
179        *self.last_checkpoint_time.write() = std::time::Instant::now();
180        *self.last_flush_lsn.write() = lsn;
181        *self.last_flush_time.write() = std::time::Instant::now();
182    }
183
184    pub fn checkpoint_diagnostics(&self, pending_checkpoint: bool) -> CheckpointDiagnostics {
185        CheckpointDiagnostics {
186            last_stats: self.last_checkpoint_stats.read().clone(),
187            pending_checkpoint,
188            last_trigger_age_secs: Some(self.last_checkpoint_time.read().elapsed().as_secs()),
189        }
190    }
191}