Skip to main content

io_pimdir/client/
diagnostics.rs

1//! What a consistency check asks about the index rather than through it.
2//!
3//! These reads observe invariants the store maintains: whether a refcount
4//! matches the references justifying it, whether a row points at
5//! something absent, what the bodies weigh. They live here rather than in
6//! the operator CLI because the library owns the repairs
7//! (`recompute_refcounts`, `clear_dangling_bindings`), and a repair that
8//! cannot report what it found is a worse seam than a diagnostic that
9//! can.
10//!
11//! Every statement here is a `SELECT`, running on the handle the caller
12//! already holds, read-only or owning.
13
14use alloc::{format, string::String, vec::Vec};
15use std::collections::BTreeSet;
16
17use rusqlite::{OptionalExtension, named_params};
18use serde::Serialize;
19
20use crate::{
21    client::{PimdirError, reader::PimdirReader, rows},
22    sql,
23};
24
25/// How many objects the index holds and what they weigh.
26#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
27pub struct PimdirObjectStats {
28    /// Indexed objects.
29    pub count: u64,
30    /// Their total size in bytes.
31    pub bytes: u64,
32}
33
34/// One object whose stored refcount disagrees with the references that
35/// justify it: items, conflict copies, per-source bases and queue pins.
36#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
37pub struct PimdirRefcountDrift {
38    /// The object's content hash.
39    pub hash: String,
40    /// The refcount the index stores.
41    pub stored: i64,
42    /// The refcount its references add up to.
43    pub expected: i64,
44}
45
46/// How many minted keys one collection holds: the second copies of
47/// identities a source hands over twice, each filed as an item of its own
48/// (spec ยง9).
49#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
50pub struct PimdirMinted {
51    /// The collection holding them.
52    pub collection: String,
53    /// How many of its live items carry a minted key.
54    pub items: i64,
55}
56
57/// One row referencing something that is not there.
58#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
59pub struct PimdirDangling {
60    /// What kind of row dangles (`binding`, `item-object`, `queue-object`).
61    pub kind: &'static str,
62    /// The row, as an operator would name it.
63    pub row: String,
64    /// What it points at and cannot find.
65    pub target: String,
66}
67
68impl PimdirReader {
69    /// How many objects are indexed and what they weigh in total.
70    pub fn object_stats(&self) -> Result<PimdirObjectStats, PimdirError> {
71        let (count, bytes) = self.conn.query_row(sql::OBJECT_STATS, [], |r| {
72            Ok((r.get::<_, i64>(0)?, r.get::<_, i64>(1)?))
73        })?;
74        Ok(PimdirObjectStats {
75            count: count.max(0) as u64,
76            bytes: bytes.max(0) as u64,
77        })
78    }
79
80    /// The bytes held by objects at least one live item still binds.
81    ///
82    /// An object a live and a retained item share counts here, since
83    /// purging the retained one would not free it.
84    pub fn live_bytes(&self) -> Result<u64, PimdirError> {
85        let bytes: i64 = self.conn.query_row(sql::LIVE_BYTES, [], |r| r.get(0))?;
86        Ok(bytes.max(0) as u64)
87    }
88
89    /// One object's stored size.
90    pub fn object_size(&self, hash: &str) -> Result<Option<u64>, PimdirError> {
91        let size: Option<i64> = self
92            .conn
93            .query_row(sql::OBJECT_SIZE, named_params! { ":hash": hash }, |r| {
94                r.get(0)
95            })
96            .optional()?;
97        Ok(size.map(|size| size.max(0) as u64))
98    }
99
100    /// What a purge with this cutoff would retire: how many retained
101    /// items, and the bytes their bodies weigh.
102    ///
103    /// A preview, so a confirmation can say what is at stake; the purge itself
104    /// is the authority, and the collector is what frees the bytes.
105    pub fn retained_before(&self, cutoff: &str) -> Result<(u64, u64), PimdirError> {
106        let (count, bytes) = self.conn.query_row(
107            sql::COUNT_RETAINED_BEFORE,
108            named_params! { ":cutoff": cutoff },
109            |r| Ok((r.get::<_, i64>(0)?, r.get::<_, i64>(1)?)),
110        )?;
111        Ok((count.max(0) as u64, bytes.max(0) as u64))
112    }
113
114    /// Every hash the index knows, to diff against the blob directory: the
115    /// index half of what [`PimdirBlobs::files`](crate::client::PimdirBlobs::files)
116    /// reads from disk.
117    pub fn indexed_hashes(&self) -> Result<BTreeSet<String>, PimdirError> {
118        Ok(rows(&self.conn, sql::LIST_OBJECT_HASHES, [], |r| r.get(0))?
119            .into_iter()
120            .collect())
121    }
122
123    /// The objects whose stored refcount disagrees with their references.
124    ///
125    /// The expected count is exactly what the write path maintains
126    /// incrementally: an item's body, an item's conflict copy, each source's
127    /// stored base, and each queue row pinning a body it enqueued.
128    /// [`recompute_refcounts`](crate::client::PimdirStore::recompute_refcounts) is what
129    /// settles what this reports.
130    pub fn refcount_drift(&self) -> Result<Vec<PimdirRefcountDrift>, PimdirError> {
131        Ok(rows(&self.conn, sql::REFCOUNT_DRIFT, [], |r| {
132            Ok(PimdirRefcountDrift {
133                hash: r.get(0)?,
134                stored: r.get(1)?,
135                expected: r.get(2)?,
136            })
137        })?)
138    }
139
140    /// The minted keys each collection holds, where it holds any.
141    ///
142    /// Not a defect and nothing to repair: two copies of one identity is
143    /// redundancy, and the store holds both rather than judging them. It
144    /// is reported because a collection whose count climbs every sync is
145    /// a source handing over the same duplicate under a new handle each
146    /// run, which an operator has no other way to see.
147    pub fn minted_keys(&self) -> Result<Vec<PimdirMinted>, PimdirError> {
148        Ok(rows(&self.conn, sql::MINTED_KEYS, [], |r| {
149            Ok(PimdirMinted {
150                collection: r.get(0)?,
151                items: r.get(1)?,
152            })
153        })?)
154    }
155
156    /// Every row pointing at something absent: a binding whose item is gone, an
157    /// item or a queue row whose object is not indexed.
158    ///
159    /// Only the first is repairable ([`clear_dangling_bindings`](crate::client::PimdirStore::clear_dangling_bindings));
160    /// the other two still hold data, so they are reported and left alone.
161    pub fn dangling(&self) -> Result<Vec<PimdirDangling>, PimdirError> {
162        let mut dangling = rows(&self.conn, sql::DANGLING_BINDINGS, [], |r| {
163            Ok(PimdirDangling {
164                kind: "binding",
165                row: format!(
166                    "{}/{} @{}",
167                    r.get::<_, String>(0)?,
168                    r.get::<_, String>(1)?,
169                    r.get::<_, String>(2)?
170                ),
171                target: format!("item {}/{}", r.get::<_, String>(0)?, r.get::<_, String>(1)?),
172            })
173        })?;
174
175        dangling.extend(rows(&self.conn, sql::DANGLING_ITEM_OBJECTS, [], |r| {
176            Ok(PimdirDangling {
177                kind: "item-object",
178                row: format!("{}/{}", r.get::<_, String>(0)?, r.get::<_, String>(1)?),
179                target: format!("object {}", r.get::<_, String>(2)?),
180            })
181        })?);
182
183        dangling.extend(rows(&self.conn, sql::DANGLING_QUEUE_OBJECTS, [], |r| {
184            Ok(PimdirDangling {
185                kind: "queue-object",
186                row: format!("queue {} ({})", r.get::<_, i64>(0)?, r.get::<_, String>(1)?),
187                target: format!("object {}", r.get::<_, String>(2)?),
188            })
189        })?);
190
191        Ok(dangling)
192    }
193}