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}