ic_query/cache/model.rs
1//! Module: cache::model
2//!
3//! Responsibility: define shared cache-validation and local inventory report contracts.
4//! Does not own: filesystem traversal, cache-family validation, or CLI output.
5//! Boundary: names family validation outcomes and exposes generic header, age,
6//! recovery-policy, and lock evidence without performing family-specific validation.
7
8use serde::Serialize;
9use std::{fmt, path::PathBuf};
10
11/// Current serialized schema version for cache-status reports.
12pub const CACHE_STATUS_REPORT_SCHEMA_VERSION: u32 = 1;
13
14///
15/// CacheValidationStatus
16///
17/// Semantic validation result for an existing family-specific cache.
18///
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
21#[serde(rename_all = "snake_case")]
22pub enum CacheValidationStatus {
23 /// The cache passed its family-specific schema, identity, and completeness checks.
24 #[serde(rename = "ok")]
25 Valid,
26 /// The cache exists but failed family-specific validation.
27 Invalid,
28}
29
30impl CacheValidationStatus {
31 /// Return the stable serialized status label.
32 #[must_use]
33 pub const fn as_str(self) -> &'static str {
34 match self {
35 Self::Valid => "ok",
36 Self::Invalid => "invalid",
37 }
38 }
39}
40
41impl fmt::Display for CacheValidationStatus {
42 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43 formatter.write_str(self.as_str())
44 }
45}
46
47///
48/// CacheRefreshAttemptStatus
49///
50/// Lifecycle state for a complete-cache refresh attempt.
51///
52
53#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
54#[serde(rename_all = "snake_case")]
55pub enum CacheRefreshAttemptStatus {
56 /// A refresh started or published intermediate collection progress.
57 Running,
58 /// A refresh exhausted its source and published a complete cache.
59 Complete,
60 /// A refresh terminated without replacing the complete cache.
61 Failed,
62}
63
64impl CacheRefreshAttemptStatus {
65 /// Return the stable serialized lifecycle label.
66 #[must_use]
67 pub const fn as_str(self) -> &'static str {
68 match self {
69 Self::Running => "running",
70 Self::Complete => "complete",
71 Self::Failed => "failed",
72 }
73 }
74
75 #[cfg(any(
76 feature = "icrc-host",
77 feature = "nns-host",
78 feature = "sns-host",
79 test
80 ))]
81 pub(crate) fn from_label(label: &str) -> Option<Self> {
82 match label {
83 "running" => Some(Self::Running),
84 "complete" => Some(Self::Complete),
85 "failed" => Some(Self::Failed),
86 _ => None,
87 }
88 }
89}
90
91impl fmt::Display for CacheRefreshAttemptStatus {
92 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
93 formatter.write_str(self.as_str())
94 }
95}
96
97///
98/// CacheHeaderStatus
99///
100/// Generic header-integrity classification for one complete cache file.
101///
102
103#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
104#[serde(rename_all = "snake_case")]
105pub enum CacheHeaderStatus {
106 /// The generic cache header is readable.
107 Readable,
108 /// The generic cache header cannot be read or parsed.
109 Invalid,
110}
111
112impl CacheHeaderStatus {
113 /// Return the stable serialized status label.
114 #[must_use]
115 pub const fn as_str(self) -> &'static str {
116 match self {
117 Self::Readable => "readable",
118 Self::Invalid => "invalid",
119 }
120 }
121}
122
123///
124/// CacheAgeStatus
125///
126/// Caller-relative age classification for one complete cache file.
127///
128
129#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
130#[serde(rename_all = "snake_case")]
131pub enum CacheAgeStatus {
132 /// The cache is within its registered family stale threshold.
133 Fresh,
134 /// The cache is older than its registered family stale threshold.
135 Stale,
136 /// The cache has a readable age but no registered family stale threshold.
137 Unmanaged,
138 /// The cache age cannot be calculated from its generic timestamp evidence.
139 Unknown,
140}
141
142impl CacheAgeStatus {
143 /// Return the stable serialized status label.
144 #[must_use]
145 pub const fn as_str(self) -> &'static str {
146 match self {
147 Self::Fresh => "fresh",
148 Self::Stale => "stale",
149 Self::Unmanaged => "unmanaged",
150 Self::Unknown => "unknown",
151 }
152 }
153}
154
155///
156/// CacheRecoveryPolicy
157///
158/// Owner policy for replacing recoverable invalid content at a canonical cache path.
159///
160
161#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
162#[serde(rename_all = "snake_case")]
163pub enum CacheRecoveryPolicy {
164 /// An ordinary owner read-through replaces recoverable invalid content.
165 Automatic,
166 /// Recovery requires an explicitly selected refresh operation.
167 Explicit,
168 /// Ordinary read-through creates a missing cache but does not replace invalid content.
169 MissingOnly,
170 /// The path does not identify a current canonical cache owner.
171 Unknown,
172}
173
174impl CacheRecoveryPolicy {
175 /// Return the stable serialized policy label.
176 #[must_use]
177 pub const fn as_str(self) -> &'static str {
178 match self {
179 Self::Automatic => "automatic",
180 Self::Explicit => "explicit",
181 Self::MissingOnly => "missing_only",
182 Self::Unknown => "unknown",
183 }
184 }
185}
186
187///
188/// CacheRefreshLockStatus
189///
190/// Generic age or validity classification for one refresh lock.
191///
192
193#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
194#[serde(rename_all = "snake_case")]
195pub enum CacheRefreshLockStatus {
196 /// The lock is within the stale threshold recorded by its owner.
197 Active,
198 /// The lock is older than the stale threshold recorded by its owner.
199 Stale,
200 /// The lock is unreadable, malformed, or future-dated.
201 Invalid,
202}
203
204impl CacheRefreshLockStatus {
205 /// Return the stable serialized status label.
206 #[must_use]
207 pub const fn as_str(self) -> &'static str {
208 match self {
209 Self::Active => "active",
210 Self::Stale => "stale",
211 Self::Invalid => "invalid",
212 }
213 }
214}
215
216///
217/// CacheStatusRequest
218///
219/// Local cache-root inspection request with a caller-supplied observation time.
220///
221
222#[derive(Clone, Debug, Eq, PartialEq)]
223pub struct CacheStatusRequest {
224 /// User-level cache root to inspect.
225 pub cache_root: PathBuf,
226 /// Observation time used to calculate cache ages.
227 pub now_unix_secs: u64,
228}
229
230impl CacheStatusRequest {
231 /// Construct a local cache-status request.
232 #[must_use]
233 pub fn new(cache_root: impl Into<PathBuf>, now_unix_secs: u64) -> Self {
234 Self {
235 cache_root: cache_root.into(),
236 now_unix_secs,
237 }
238 }
239}
240
241///
242/// CacheStatusReport
243///
244/// Bounded local inventory of known complete caches and refresh locks.
245///
246
247#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
248pub struct CacheStatusReport {
249 /// Cache-status report schema version.
250 pub schema_version: u32,
251 /// Inspected user-level cache root.
252 pub cache_root: String,
253 /// UTC timestamp at which local inspection was requested.
254 pub inspected_at: String,
255 /// Whether the cache root existed.
256 pub cache_root_found: bool,
257 /// Maximum number of cache and refresh-lock files inspected in one report.
258 pub scan_limit: usize,
259 /// Whether additional cache or refresh-lock candidates existed beyond the scan limit.
260 pub truncated: bool,
261 /// Whether the generic inventory performed family-specific semantic validation.
262 pub family_validation_performed: bool,
263 /// Number of cache rows returned.
264 pub cache_count: usize,
265 /// Number of caches with readable generic headers.
266 pub readable_header_count: usize,
267 /// Number of caches with unreadable or malformed generic headers.
268 pub invalid_header_count: usize,
269 /// Number of caches fresh under an explicit family policy.
270 pub fresh_count: usize,
271 /// Number of caches stale under an explicit family policy.
272 pub stale_count: usize,
273 /// Number of readable caches whose family has no registered age policy.
274 pub unmanaged_age_count: usize,
275 /// Number of caches whose age cannot be calculated from generic evidence.
276 pub unknown_age_count: usize,
277 /// Sum of filesystem sizes for returned cache files.
278 pub total_size_bytes: u64,
279 /// Canonically path-ordered cache rows.
280 pub caches: Vec<CacheStatusRow>,
281 /// Number of refresh-lock rows returned.
282 pub refresh_lock_count: usize,
283 /// Number of locks still active under their recorded stale policy.
284 pub active_refresh_lock_count: usize,
285 /// Number of locks older than their recorded stale policy.
286 pub stale_refresh_lock_count: usize,
287 /// Number of unreadable, malformed, or future-dated locks.
288 pub invalid_refresh_lock_count: usize,
289 /// Sum of filesystem sizes for returned refresh-lock files.
290 pub refresh_lock_size_bytes: u64,
291 /// Canonically path-ordered refresh-lock rows.
292 pub refresh_locks: Vec<CacheRefreshLockStatusRow>,
293}
294
295///
296/// CacheStatusRow
297///
298/// Generic local metadata and caller-relative age for one complete cache file.
299///
300
301#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
302pub struct CacheStatusRow {
303 /// Stable component label inferred from cache identity or canonical path.
304 pub component: String,
305 /// Absolute cache-file path.
306 pub cache_path: String,
307 /// Cache-root-relative path.
308 pub relative_path: String,
309 /// Generic cache-header integrity without family-specific semantic validation.
310 pub header_status: CacheHeaderStatus,
311 /// Caller-relative age classification kept separate from header integrity.
312 pub age_status: CacheAgeStatus,
313 /// Owner policy for recovering invalid content at this canonical path.
314 pub recovery_policy: CacheRecoveryPolicy,
315 /// Serialized cache schema version when readable.
316 pub schema_version: Option<u32>,
317 /// Serialized network identity when present.
318 pub network: Option<String>,
319 /// Cache collection timestamp when readable.
320 pub fetched_at: Option<String>,
321 /// Caller-relative age when the timestamp is valid and not in the future.
322 pub age_seconds: Option<u64>,
323 /// Family age threshold when one is explicitly defined.
324 pub stale_after_seconds: Option<u64>,
325 /// Filesystem size of this cache file.
326 pub size_bytes: u64,
327 /// Generic header or timestamp inspection error; family-specific validation is separate.
328 pub inspection_error: Option<String>,
329}
330
331///
332/// CacheRefreshLockStatusRow
333///
334/// Local identity, ownership, age, and stale policy for one refresh lock.
335///
336
337#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
338pub struct CacheRefreshLockStatusRow {
339 /// Stable component label inferred from the recorded cache target.
340 pub component: String,
341 /// Absolute refresh-lock path.
342 pub refresh_lock_path: String,
343 /// Cache-root-relative refresh-lock path.
344 pub relative_path: String,
345 /// Generic lock age or validity classification.
346 pub status: CacheRefreshLockStatus,
347 /// Serialized refresh-lock schema version when readable.
348 pub schema_version: Option<u32>,
349 /// Serialized network identity when readable.
350 pub network: Option<String>,
351 /// Operating-system process id recorded by the lock owner when readable.
352 pub pid: Option<u32>,
353 /// Raw Unix-millisecond acquisition time when readable.
354 pub started_at_unix_ms: Option<u64>,
355 /// UTC acquisition timestamp when readable.
356 pub started_at: Option<String>,
357 /// Caller-relative lock age when the timestamp is not in the future.
358 pub age_seconds: Option<u64>,
359 /// Stale threshold recorded by the lock owner.
360 pub stale_after_seconds: Option<u64>,
361 /// Cache target recorded by the lock owner when readable.
362 pub target_path: Option<String>,
363 /// Filesystem size of this refresh-lock file.
364 pub size_bytes: u64,
365 /// Lock parse, shape, or timestamp error.
366 pub error: Option<String>,
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[test]
374 fn typed_statuses_keep_stable_json_labels() {
375 for (status, expected) in [
376 (CacheValidationStatus::Valid, "ok"),
377 (CacheValidationStatus::Invalid, "invalid"),
378 ] {
379 assert_eq!(status.as_str(), expected);
380 assert_eq!(status.to_string(), expected);
381 assert_eq!(
382 serde_json::to_value(status).expect("serialize cache validation status"),
383 serde_json::json!(expected)
384 );
385 }
386 for (status, expected) in [
387 (CacheRefreshAttemptStatus::Running, "running"),
388 (CacheRefreshAttemptStatus::Complete, "complete"),
389 (CacheRefreshAttemptStatus::Failed, "failed"),
390 ] {
391 assert_eq!(status.as_str(), expected);
392 assert_eq!(status.to_string(), expected);
393 assert_eq!(
394 CacheRefreshAttemptStatus::from_label(expected),
395 Some(status)
396 );
397 assert_eq!(
398 serde_json::to_value(status).expect("serialize refresh-attempt status"),
399 serde_json::json!(expected)
400 );
401 }
402 assert_eq!(CacheRefreshAttemptStatus::from_label("unknown"), None);
403 for (status, expected) in [
404 (CacheHeaderStatus::Readable, "readable"),
405 (CacheHeaderStatus::Invalid, "invalid"),
406 ] {
407 assert_eq!(status.as_str(), expected);
408 assert_eq!(
409 serde_json::to_value(status).expect("serialize cache header status"),
410 serde_json::json!(expected)
411 );
412 }
413 for (status, expected) in [
414 (CacheAgeStatus::Fresh, "fresh"),
415 (CacheAgeStatus::Stale, "stale"),
416 (CacheAgeStatus::Unmanaged, "unmanaged"),
417 (CacheAgeStatus::Unknown, "unknown"),
418 ] {
419 assert_eq!(status.as_str(), expected);
420 assert_eq!(
421 serde_json::to_value(status).expect("serialize cache age status"),
422 serde_json::json!(expected)
423 );
424 }
425 for (policy, expected) in [
426 (CacheRecoveryPolicy::Automatic, "automatic"),
427 (CacheRecoveryPolicy::Explicit, "explicit"),
428 (CacheRecoveryPolicy::MissingOnly, "missing_only"),
429 (CacheRecoveryPolicy::Unknown, "unknown"),
430 ] {
431 assert_eq!(policy.as_str(), expected);
432 assert_eq!(
433 serde_json::to_value(policy).expect("serialize cache recovery policy"),
434 serde_json::json!(expected)
435 );
436 }
437 for (status, expected) in [
438 (CacheRefreshLockStatus::Active, "active"),
439 (CacheRefreshLockStatus::Stale, "stale"),
440 (CacheRefreshLockStatus::Invalid, "invalid"),
441 ] {
442 assert_eq!(status.as_str(), expected);
443 assert_eq!(
444 serde_json::to_value(status).expect("serialize refresh-lock status"),
445 serde_json::json!(expected)
446 );
447 }
448 }
449}