Skip to main content

offline_protocol_data/
policy.rs

1//! Size caps and the compaction trigger.
2//!
3//! Pure arithmetic over byte counts, deliberately free of engine and storage
4//! types so the thresholds can be tested and reasoned about on their own.
5//! Every constant here is derived from measurements recorded in the data
6//! layer design record, not chosen by feel.
7
8/// Hard cap on the compacted encoding of one document, 1 MiB.
9///
10/// The physical ceiling is the sealed protocol-state record (4 MiB). The
11/// working budget is a quarter of it, and the gap is not slack: it absorbs
12/// the delta log accumulating between compactions, seal and framing
13/// overhead, and the growth between the write that breaches the cap and the
14/// next compaction pass. A document that only checked itself against the
15/// physical ceiling would become unpersistable while the user was still
16/// typing into it.
17pub const MAX_DOC_BYTES: usize = 1024 * 1024;
18
19/// Size at which a document starts warning, 768 KiB (75% of the cap).
20///
21/// The warning exists so an application hears about growth while it can
22/// still act (archive, split, prune). A cap with no approach signal turns
23/// into a cliff the app meets for the first time in production.
24pub const DOC_SIZE_WARN_BYTES: usize = 768 * 1024;
25
26/// Floor under the compaction trigger, 64 KiB.
27///
28/// Without it a small document compacts on nearly every commit: measured
29/// history growth is ~6.7 bytes per operation while a shallow snapshot of a
30/// small document is under a kilobyte, so the ratio rule alone would fire
31/// constantly and rewrite a record per keystroke.
32pub const COMPACT_MIN_DELTA_LOG_BYTES: usize = 64 * 1024;
33
34/// Compact once the delta log is this many times the compacted document.
35///
36/// Bounds worst-case storage at roughly five times the document's state
37/// size, which is the trade being made: replay work and disk against how
38/// often a full snapshot is rewritten.
39pub const COMPACT_DELTA_LOG_RATIO: usize = 4;
40
41/// Compact after this many commits regardless of byte ratios, 1024.
42///
43/// Caps replay work after a crash. The ratio rule alone can idle for a very
44/// long time on a document whose commits are tiny.
45pub const COMPACT_MAX_COMMITS: u32 = 1024;
46
47/// Whether a document should be rewritten as a fresh compacted snapshot.
48///
49/// `compacted_doc_bytes` is the size of the last snapshot written for the
50/// document (zero if it has never been compacted).
51pub fn should_compact(
52    delta_log_bytes: usize,
53    compacted_doc_bytes: usize,
54    commits_since_compaction: u32,
55) -> bool {
56    if commits_since_compaction >= COMPACT_MAX_COMMITS {
57        return true;
58    }
59    let ratio_threshold = compacted_doc_bytes.saturating_mul(COMPACT_DELTA_LOG_RATIO);
60    delta_log_bytes > ratio_threshold.max(COMPACT_MIN_DELTA_LOG_BYTES)
61}
62
63/// Where a compacted document sits against the cap.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum SizeVerdict {
66    /// Comfortably under the warning threshold.
67    Ok,
68    /// At or past [`DOC_SIZE_WARN_BYTES`], still under the cap.
69    Warn,
70    /// Past [`MAX_DOC_BYTES`]. The commit that produced this must fail.
71    TooLarge,
72}
73
74/// Classify a compacted document size.
75pub fn size_verdict(compacted_bytes: usize) -> SizeVerdict {
76    if compacted_bytes > MAX_DOC_BYTES {
77        SizeVerdict::TooLarge
78    } else if compacted_bytes >= DOC_SIZE_WARN_BYTES {
79        SizeVerdict::Warn
80    } else {
81        SizeVerdict::Ok
82    }
83}
84
85/// Whether the document is close enough to the warning threshold that its
86/// true compacted size has to be measured before the next commit is accepted.
87///
88/// Measuring means encoding a shallow snapshot, which is far too expensive to
89/// do on every commit. Estimating from the last real measurement plus the
90/// bytes written since it is always an over-estimate of the compacted size
91/// (history compacts, it does not expand), so a document can never cross the
92/// cap without this answering true first. That is the property that keeps the
93/// check from lagging the truth by more than a single flush.
94pub fn needs_size_check(last_measured_bytes: usize, delta_bytes_since: usize) -> bool {
95    last_measured_bytes.saturating_add(delta_bytes_since) >= DOC_SIZE_WARN_BYTES
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn small_documents_do_not_compact_on_every_commit() {
104        // A 900 byte document with 8 KiB of log: the ratio rule would fire
105        // (8192 > 3600) but the floor holds it back.
106        assert!(!should_compact(8 * 1024, 900, 3));
107    }
108
109    #[test]
110    fn the_floor_is_not_a_ceiling_for_large_documents() {
111        // 100 KiB of log against a 200 KiB document stays under 4x.
112        assert!(!should_compact(100 * 1024, 200 * 1024, 10));
113        // 900 KiB of log against the same document is past it.
114        assert!(should_compact(900 * 1024, 200 * 1024, 10));
115    }
116
117    #[test]
118    fn a_never_compacted_document_uses_the_floor() {
119        assert!(!should_compact(COMPACT_MIN_DELTA_LOG_BYTES, 0, 1));
120        assert!(should_compact(COMPACT_MIN_DELTA_LOG_BYTES + 1, 0, 1));
121    }
122
123    #[test]
124    fn commit_count_forces_compaction_regardless_of_size() {
125        assert!(should_compact(1, 10 * 1024 * 1024, COMPACT_MAX_COMMITS));
126    }
127
128    #[test]
129    fn size_verdict_boundaries() {
130        assert_eq!(size_verdict(0), SizeVerdict::Ok);
131        assert_eq!(size_verdict(DOC_SIZE_WARN_BYTES - 1), SizeVerdict::Ok);
132        assert_eq!(size_verdict(DOC_SIZE_WARN_BYTES), SizeVerdict::Warn);
133        assert_eq!(size_verdict(MAX_DOC_BYTES), SizeVerdict::Warn);
134        assert_eq!(size_verdict(MAX_DOC_BYTES + 1), SizeVerdict::TooLarge);
135    }
136
137    #[test]
138    fn the_cap_leaves_room_under_the_record_ceiling() {
139        // The sealed protocol-state record is 4 MiB. The working cap must
140        // stay well under it so the delta log, seal and framing all fit.
141        assert!(MAX_DOC_BYTES * 4 <= 4 * 1024 * 1024);
142        assert!(DOC_SIZE_WARN_BYTES < MAX_DOC_BYTES);
143    }
144
145    #[test]
146    fn size_checks_engage_before_the_cap_can_be_crossed() {
147        // Just under the warning line with nothing written since: no check.
148        assert!(!needs_size_check(DOC_SIZE_WARN_BYTES - 1, 0));
149        // The same document after a single byte of delta: check.
150        assert!(needs_size_check(DOC_SIZE_WARN_BYTES - 1, 1));
151        // A small document with a large uncompacted log: check.
152        assert!(needs_size_check(1024, DOC_SIZE_WARN_BYTES));
153    }
154}