Skip to main content

icydb_core/db/diagnostics/
mod.rs

1//! Module: diagnostics
2//! Responsibility: read-only storage footprint and integrity snapshots.
3//! Does not own: recovery, write-path mutation, or query planning semantics.
4//! Boundary: consumes `Db`/store read APIs and returns DTO snapshots.
5
6#[cfg(test)]
7mod tests;
8
9use crate::{
10    db::{
11        Db, EntityName,
12        data::{DataKey, StorageKey},
13        index::IndexKey,
14    },
15    error::InternalError,
16    traits::CanisterKind,
17    value::Value,
18};
19use candid::CandidType;
20use serde::{Deserialize, Serialize};
21use std::collections::BTreeMap;
22
23///
24/// StorageReport
25/// Live storage snapshot report
26///
27
28#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
29pub struct StorageReport {
30    pub storage_data: Vec<DataStoreSnapshot>,
31    pub storage_index: Vec<IndexStoreSnapshot>,
32    pub entity_storage: Vec<EntitySnapshot>,
33    pub corrupted_keys: u64,
34    pub corrupted_entries: u64,
35}
36
37///
38/// DataStoreSnapshot
39/// Store-level snapshot metrics.
40///
41
42#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
43pub struct DataStoreSnapshot {
44    pub path: String,
45    pub entries: u64,
46    pub memory_bytes: u64,
47}
48
49///
50/// IndexStoreSnapshot
51/// Index-store snapshot metrics
52///
53
54#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
55pub struct IndexStoreSnapshot {
56    pub path: String,
57    pub entries: u64,
58    pub user_entries: u64,
59    pub system_entries: u64,
60    pub memory_bytes: u64,
61}
62
63///
64/// EntitySnapshot
65/// Per-entity storage breakdown across stores
66///
67
68#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
69pub struct EntitySnapshot {
70    /// Store path (e.g., icydb_schema_tests::schema::TestDataStore)
71    pub store: String,
72
73    /// Entity path (e.g., icydb_schema_tests::canister::db::Index)
74    pub path: String,
75
76    /// Number of rows for this entity in the store
77    pub entries: u64,
78
79    /// Approximate bytes used (key + value)
80    pub memory_bytes: u64,
81
82    /// Minimum primary key for this entity (entity-local ordering)
83    pub min_key: Option<Value>,
84
85    /// Maximum primary key for this entity (entity-local ordering)
86    pub max_key: Option<Value>,
87}
88
89///
90/// EntityStats
91/// Internal struct for building per-entity stats before snapshotting.
92///
93
94#[derive(Default)]
95struct EntityStats {
96    entries: u64,
97    memory_bytes: u64,
98    min_key: Option<StorageKey>,
99    max_key: Option<StorageKey>,
100}
101
102impl EntityStats {
103    // Accumulate per-entity counters and keep min/max over entity-local storage keys.
104    fn update(&mut self, dk: &DataKey, value_len: u64) {
105        self.entries = self.entries.saturating_add(1);
106        self.memory_bytes = self
107            .memory_bytes
108            .saturating_add(DataKey::entry_size_bytes(value_len));
109
110        let k = dk.storage_key();
111
112        match &mut self.min_key {
113            Some(min) if k < *min => *min = k,
114            None => self.min_key = Some(k),
115            _ => {}
116        }
117
118        match &mut self.max_key {
119            Some(max) if k > *max => *max = k,
120            None => self.max_key = Some(k),
121            _ => {}
122        }
123    }
124}
125
126/// Build one deterministic storage snapshot with per-entity rollups.
127///
128/// This path is read-only and fail-closed on decode/validation errors by counting
129/// corrupted keys/entries instead of panicking.
130pub(crate) fn storage_report<C: CanisterKind>(
131    db: &Db<C>,
132    name_to_path: &[(&'static str, &'static str)],
133) -> Result<StorageReport, InternalError> {
134    db.ensure_recovered_state()?;
135    // Build name→path map once, reuse across stores.
136    let name_map: BTreeMap<&'static str, &str> = name_to_path.iter().copied().collect();
137    let mut data = Vec::new();
138    let mut index = Vec::new();
139    let mut entity_storage: Vec<EntitySnapshot> = Vec::new();
140    let mut corrupted_keys = 0u64;
141    let mut corrupted_entries = 0u64;
142
143    db.with_store_registry(|reg| {
144        // Keep diagnostics snapshots deterministic by traversing stores in path order.
145        let mut stores = reg.iter().collect::<Vec<_>>();
146        stores.sort_by_key(|(path, _)| *path);
147
148        for (path, store_handle) in stores {
149            // Phase 1: collect data-store snapshots and per-entity stats.
150            store_handle.with_data(|store| {
151                data.push(DataStoreSnapshot {
152                    path: path.to_string(),
153                    entries: store.len(),
154                    memory_bytes: store.memory_bytes(),
155                });
156
157                // Track per-entity counts, memory, and min/max Keys (not DataKeys)
158                let mut by_entity: BTreeMap<EntityName, EntityStats> = BTreeMap::new();
159
160                for entry in store.iter() {
161                    let Ok(dk) = DataKey::try_from_raw(entry.key()) else {
162                        corrupted_keys = corrupted_keys.saturating_add(1);
163                        continue;
164                    };
165
166                    let value_len = entry.value().len() as u64;
167
168                    by_entity
169                        .entry(*dk.entity_name())
170                        .or_default()
171                        .update(&dk, value_len);
172                }
173
174                for (entity_name, stats) in by_entity {
175                    let path_name = name_map
176                        .get(entity_name.as_str())
177                        .copied()
178                        .unwrap_or(entity_name.as_str());
179                    entity_storage.push(EntitySnapshot {
180                        store: path.to_string(),
181                        path: path_name.to_string(),
182                        entries: stats.entries,
183                        memory_bytes: stats.memory_bytes,
184                        min_key: stats.min_key.map(|key| key.as_value()),
185                        max_key: stats.max_key.map(|key| key.as_value()),
186                    });
187                }
188            });
189
190            // Phase 2: collect index-store snapshots and integrity counters.
191            store_handle.with_index(|store| {
192                let mut user_entries = 0u64;
193                let mut system_entries = 0u64;
194
195                for (key, value) in store.entries() {
196                    let Ok(decoded_key) = IndexKey::try_from_raw(&key) else {
197                        corrupted_entries = corrupted_entries.saturating_add(1);
198                        continue;
199                    };
200
201                    if decoded_key.uses_system_namespace() {
202                        system_entries = system_entries.saturating_add(1);
203                    } else {
204                        user_entries = user_entries.saturating_add(1);
205                    }
206
207                    if value.validate().is_err() {
208                        corrupted_entries = corrupted_entries.saturating_add(1);
209                    }
210                }
211
212                index.push(IndexStoreSnapshot {
213                    path: path.to_string(),
214                    entries: store.len(),
215                    user_entries,
216                    system_entries,
217                    memory_bytes: store.memory_bytes(),
218                });
219            });
220        }
221    });
222
223    // Phase 3: enforce deterministic entity snapshot emission order.
224    // This remains stable even if outer store traversal internals change.
225    entity_storage.sort_by(|left, right| {
226        (left.store.as_str(), left.path.as_str()).cmp(&(right.store.as_str(), right.path.as_str()))
227    });
228
229    Ok(StorageReport {
230        storage_data: data,
231        storage_index: index,
232        entity_storage,
233        corrupted_keys,
234        corrupted_entries,
235    })
236}