1use crate::{
2 db::{
3 Db, ensure_recovered,
4 identity::EntityName,
5 index::IndexKey,
6 store::{DataKey, StorageKey},
7 },
8 error::InternalError,
9 traits::CanisterKind,
10 value::Value,
11};
12use candid::CandidType;
13use serde::{Deserialize, Serialize};
14use std::collections::BTreeMap;
15
16#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
22pub struct StorageReport {
23 pub storage_data: Vec<DataStoreSnapshot>,
24 pub storage_index: Vec<IndexStoreSnapshot>,
25 pub entity_storage: Vec<EntitySnapshot>,
26 pub corrupted_keys: u64,
27 pub corrupted_entries: u64,
28}
29
30#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
36pub struct DataStoreSnapshot {
37 pub path: String,
38 pub entries: u64,
39 pub memory_bytes: u64,
40}
41
42#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
48pub struct IndexStoreSnapshot {
49 pub path: String,
50 pub entries: u64,
51 pub memory_bytes: u64,
52}
53
54#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
60pub struct EntitySnapshot {
61 pub store: String,
63
64 pub path: String,
66
67 pub entries: u64,
69
70 pub memory_bytes: u64,
72
73 pub min_key: Option<Value>,
75
76 pub max_key: Option<Value>,
78}
79
80#[derive(Default)]
86struct EntityStats {
87 entries: u64,
88 memory_bytes: u64,
89 min_key: Option<StorageKey>,
90 max_key: Option<StorageKey>,
91}
92
93impl EntityStats {
94 fn update(&mut self, dk: &DataKey, value_len: u64) {
95 self.entries = self.entries.saturating_add(1);
96 self.memory_bytes = self
97 .memory_bytes
98 .saturating_add(DataKey::entry_size_bytes(value_len));
99
100 let k = dk.storage_key();
101
102 match &mut self.min_key {
103 Some(min) if k < *min => *min = k,
104 None => self.min_key = Some(k),
105 _ => {}
106 }
107
108 match &mut self.max_key {
109 Some(max) if k > *max => *max = k,
110 None => self.max_key = Some(k),
111 _ => {}
112 }
113 }
114}
115
116pub fn storage_report<C: CanisterKind>(
118 db: &Db<C>,
119 name_to_path: &[(&'static str, &'static str)],
120) -> Result<StorageReport, InternalError> {
121 ensure_recovered(db)?;
122 let name_map: BTreeMap<&'static str, &str> = name_to_path.iter().copied().collect();
124 let mut data = Vec::new();
125 let mut index = Vec::new();
126 let mut entity_storage: Vec<EntitySnapshot> = Vec::new();
127 let mut corrupted_keys = 0u64;
128 let mut corrupted_entries = 0u64;
129
130 db.with_store_registry(|reg| {
131 reg.iter().for_each(|(path, store_handle)| {
132 store_handle.with_data(|store| {
133 data.push(DataStoreSnapshot {
134 path: path.to_string(),
135 entries: store.len(),
136 memory_bytes: store.memory_bytes(),
137 });
138
139 let mut by_entity: BTreeMap<EntityName, EntityStats> = BTreeMap::new();
141
142 for entry in store.iter() {
143 let Ok(dk) = DataKey::try_from_raw(entry.key()) else {
144 corrupted_keys = corrupted_keys.saturating_add(1);
145 continue;
146 };
147
148 let value_len = entry.value().len() as u64;
149
150 by_entity
151 .entry(*dk.entity_name())
152 .or_default()
153 .update(&dk, value_len);
154 }
155
156 for (entity_name, stats) in by_entity {
157 let path_name = name_map.get(entity_name.as_str()).copied().unwrap_or("");
158 entity_storage.push(EntitySnapshot {
159 store: path.to_string(),
160 path: path_name.to_string(),
161 entries: stats.entries,
162 memory_bytes: stats.memory_bytes,
163 min_key: stats.min_key.map(|key| key.as_value()),
164 max_key: stats.max_key.map(|key| key.as_value()),
165 });
166 }
167 });
168 });
169 });
170
171 db.with_store_registry(|reg| {
172 reg.iter().for_each(|(path, store_handle)| {
173 store_handle.with_index(|store| {
174 index.push(IndexStoreSnapshot {
175 path: path.to_string(),
176 entries: store.len(),
177 memory_bytes: store.memory_bytes(),
178 });
179
180 for (key, value) in store.entries() {
181 if IndexKey::try_from_raw(&key).is_err() {
182 corrupted_entries = corrupted_entries.saturating_add(1);
183 continue;
184 }
185 if value.validate().is_err() {
186 corrupted_entries = corrupted_entries.saturating_add(1);
187 }
188 }
189 });
190 });
191 });
192
193 Ok(StorageReport {
194 storage_data: data,
195 storage_index: index,
196 entity_storage,
197 corrupted_keys,
198 corrupted_entries,
199 })
200}
201
202#[cfg(test)]
203mod tests {
204 use crate::{
205 db::{
206 Db,
207 index::{IndexId, IndexKey, IndexStore, RawIndexEntry},
208 init_commit_store_for_tests,
209 store::{DataKey, DataStore, RawRow, StoreRegistry},
210 },
211 obs::snapshot::storage_report,
212 test_support::test_memory,
213 traits::{CanisterKind, Path, Storable},
214 };
215 use std::{borrow::Cow, cell::RefCell};
216
217 const STORE_PATH: &str = "snapshot_tests::Store";
218
219 struct SnapshotTestCanister;
220
221 impl Path for SnapshotTestCanister {
222 const PATH: &'static str = "snapshot_tests::Canister";
223 }
224
225 impl CanisterKind for SnapshotTestCanister {}
226
227 thread_local! {
228 static SNAPSHOT_DATA_STORE: RefCell<DataStore> = RefCell::new(DataStore::init(test_memory(101)));
229 static SNAPSHOT_INDEX_STORE: RefCell<IndexStore> =
230 RefCell::new(IndexStore::init(test_memory(102)));
231 static SNAPSHOT_STORE_REGISTRY: StoreRegistry = {
232 let mut reg = StoreRegistry::new();
233 reg.register_store(STORE_PATH, &SNAPSHOT_DATA_STORE, &SNAPSHOT_INDEX_STORE)
234 .expect("snapshot store registration should succeed");
235 reg
236 };
237 }
238
239 static DB: Db<SnapshotTestCanister> = Db::new(&SNAPSHOT_STORE_REGISTRY);
240
241 fn with_snapshot_store<R>(f: impl FnOnce(crate::db::store::StoreHandle) -> R) -> R {
242 DB.with_store_registry(|reg| reg.try_get_store(STORE_PATH).map(f))
243 .expect("snapshot store access should succeed")
244 }
245
246 fn reset_snapshot_state() {
247 init_commit_store_for_tests().expect("commit store init should succeed");
248
249 with_snapshot_store(|store| {
250 store.with_data_mut(DataStore::clear);
251 store.with_index_mut(IndexStore::clear);
252 });
253 }
254
255 #[test]
256 fn storage_report_lists_registered_store_snapshots() {
257 reset_snapshot_state();
258
259 let report = storage_report(&DB, &[]).expect("storage report should succeed");
260 assert_eq!(report.storage_data.len(), 1);
261 assert_eq!(report.storage_data[0].path, STORE_PATH);
262 assert_eq!(report.storage_data[0].entries, 0);
263 assert_eq!(report.storage_index.len(), 1);
264 assert_eq!(report.storage_index[0].path, STORE_PATH);
265 assert_eq!(report.storage_index[0].entries, 0);
266 assert!(report.entity_storage.is_empty());
267 assert_eq!(report.corrupted_keys, 0);
268 assert_eq!(report.corrupted_entries, 0);
269 }
270
271 #[test]
272 fn storage_report_counts_entity_rows_and_corrupted_index_entries() {
273 reset_snapshot_state();
274
275 let data_key = DataKey::max_storable()
276 .to_raw()
277 .expect("max storable data key should encode");
278 let row = RawRow::try_new(vec![1, 2, 3]).expect("row bytes should be valid");
279 with_snapshot_store(|store| {
280 store.with_data_mut(|data_store| {
281 data_store.insert(data_key, row);
282 });
283 });
284
285 let index_key = IndexKey::empty(IndexId::max_storable()).to_raw();
286 let malformed_index_entry = RawIndexEntry::from_bytes(Cow::Owned(vec![0, 0, 0, 0]));
287 with_snapshot_store(|store| {
288 store.with_index_mut(|index_store| {
289 index_store.insert(index_key, malformed_index_entry);
290 });
291 });
292
293 let report = storage_report(&DB, &[]).expect("storage report should succeed");
294 assert_eq!(report.storage_data[0].entries, 1);
295 assert_eq!(report.storage_index[0].entries, 1);
296 assert_eq!(report.entity_storage.len(), 1);
297 assert_eq!(report.entity_storage[0].path, "");
298 assert_eq!(report.entity_storage[0].entries, 1);
299 assert!(report.entity_storage[0].min_key.is_some());
300 assert!(report.entity_storage[0].max_key.is_some());
301 assert_eq!(report.corrupted_entries, 1);
302 assert_eq!(report.corrupted_keys, 0);
303 }
304}