1use std::fmt;
5
6use reifydb_value::value::datetime::DateTime;
7
8use crate::common::CommitVersion;
9
10#[derive(Copy, Clone, Debug, PartialEq, Eq)]
11pub enum Floor {
12 Version(CommitVersion),
13 Instant(DateTime),
14}
15
16impl Floor {
17 pub fn monotonic_key(&self) -> u64 {
18 match self {
19 Self::Version(version) => version.0,
20 Self::Instant(instant) => instant.to_order(),
21 }
22 }
23
24 pub fn version(&self) -> Option<CommitVersion> {
25 match self {
26 Self::Version(version) => Some(*version),
27 Self::Instant(_) => None,
28 }
29 }
30
31 pub fn instant(&self) -> Option<DateTime> {
32 match self {
33 Self::Instant(instant) => Some(*instant),
34 Self::Version(_) => None,
35 }
36 }
37
38 pub fn is_same_domain(&self, other: &Self) -> bool {
39 matches!((self, other), (Self::Version(_), Self::Version(_)) | (Self::Instant(_), Self::Instant(_)))
40 }
41}
42
43impl fmt::Display for Floor {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 match self {
46 Self::Version(version) => write!(f, "v{}", version.0),
47 Self::Instant(instant) => write!(f, "{instant}"),
48 }
49 }
50}
51
52#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
53pub enum FloorTerm {
54 RowExpiry,
55
56 QueryDoneUntil,
57
58 LeaseMin,
59
60 ConsumerCheckpoint,
61
62 ConsumerPosition,
63
64 RetentionHorizon,
65}
66
67impl FloorTerm {
68 pub fn protects(&self) -> &'static str {
69 match self {
70 Self::RowExpiry => "rows younger than their declared ttl",
71 Self::QueryDoneUntil => "an in-flight query reading at its snapshot version",
72 Self::LeaseMin => "a held operator-state lease",
73 Self::ConsumerCheckpoint => "a CDC log consumer that has not yet consumed the version",
74 Self::ConsumerPosition => "a live flow that has not yet consumed the version",
75 Self::RetentionHorizon => "epoch samples still needed to resolve the longest declared ttl",
76 }
77 }
78
79 pub fn is_clock_driven(&self) -> bool {
80 match self {
81 Self::RowExpiry => true,
82 Self::QueryDoneUntil
83 | Self::LeaseMin
84 | Self::ConsumerCheckpoint
85 | Self::ConsumerPosition
86 | Self::RetentionHorizon => false,
87 }
88 }
89
90 pub fn all() -> &'static [Self] {
91 &[
92 Self::RowExpiry,
93 Self::QueryDoneUntil,
94 Self::LeaseMin,
95 Self::ConsumerCheckpoint,
96 Self::ConsumerPosition,
97 Self::RetentionHorizon,
98 ]
99 }
100
101 pub fn index(&self) -> usize {
102 Self::all().iter().position(|term| term == self).expect("every term is listed in FloorTerm::all")
103 }
104
105 pub fn from_index(index: usize) -> Option<Self> {
106 Self::all().get(index).copied()
107 }
108}
109
110impl fmt::Display for FloorTerm {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 match self {
113 Self::RowExpiry => write!(f, "row-expiry"),
114 Self::QueryDoneUntil => write!(f, "query-done-until"),
115 Self::LeaseMin => write!(f, "lease-min"),
116 Self::ConsumerCheckpoint => write!(f, "consumer-checkpoint"),
117 Self::ConsumerPosition => write!(f, "consumer-position"),
118 Self::RetentionHorizon => write!(f, "retention-horizon"),
119 }
120 }
121}
122
123#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
124pub enum RetentionClass {
125 RowTtl,
126
127 BufferHistoricalGc,
128
129 PersistentFlush,
130
131 QueueLeaseReap,
132
133 QueueRetention,
134
135 CdcTruncate,
136
137 EpochLog,
138
139 CommitMerge,
140}
141
142impl RetentionClass {
143 pub fn all() -> &'static [Self] {
144 &[
145 Self::RowTtl,
146 Self::BufferHistoricalGc,
147 Self::PersistentFlush,
148 Self::QueueLeaseReap,
149 Self::QueueRetention,
150 Self::CdcTruncate,
151 Self::EpochLog,
152 Self::CommitMerge,
153 ]
154 }
155
156 pub fn name(&self) -> &'static str {
157 match self {
158 Self::RowTtl => "row-ttl-silent",
159 Self::BufferHistoricalGc => "buffer-historical-gc",
160 Self::PersistentFlush => "persistent-flush",
161 Self::QueueLeaseReap => "queue-lease-reap",
162 Self::QueueRetention => "queue-retention",
163 Self::CdcTruncate => "cdc-truncate",
164 Self::EpochLog => "epoch-log",
165 Self::CommitMerge => "commit-merge",
166 }
167 }
168
169 pub fn reclaims_versioned_data(&self) -> bool {
170 match self {
171 Self::RowTtl
172 | Self::BufferHistoricalGc
173 | Self::PersistentFlush
174 | Self::CdcTruncate
175 | Self::EpochLog
176 | Self::QueueRetention => true,
177 Self::QueueLeaseReap | Self::CommitMerge => false,
178 }
179 }
180
181 pub fn floor_terms(&self) -> &'static [FloorTerm] {
182 match self {
183 Self::RowTtl => &[FloorTerm::RowExpiry],
184 Self::BufferHistoricalGc => &[FloorTerm::QueryDoneUntil, FloorTerm::LeaseMin],
185 Self::PersistentFlush => {
186 &[FloorTerm::QueryDoneUntil, FloorTerm::LeaseMin, FloorTerm::ConsumerPosition]
187 }
188 Self::QueueLeaseReap | Self::CommitMerge => &[],
189 Self::QueueRetention => &[FloorTerm::RowExpiry],
190 Self::CdcTruncate => &[FloorTerm::ConsumerCheckpoint],
191 Self::EpochLog => &[FloorTerm::RetentionHorizon],
192 }
193 }
194
195 pub fn constrained_by(&self, term: FloorTerm) -> bool {
196 self.floor_terms().contains(&term)
197 }
198}
199
200impl fmt::Display for RetentionClass {
201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202 write!(f, "{}", self.name())
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::{FloorTerm, RetentionClass};
209
210 #[test]
211 fn every_class_declares_a_floor_term_for_exactly_the_data_it_reclaims() {
212 for class in RetentionClass::all() {
216 if class.reclaims_versioned_data() {
217 assert!(
218 !class.floor_terms().is_empty(),
219 "{class} declares no floor term, so nothing bounds what it deletes"
220 );
221 } else {
222 assert!(
223 class.floor_terms().is_empty(),
224 "{class} reclaims no versioned data, so a version floor cannot be what bounds it"
225 );
226 }
227 }
228 }
229
230 #[test]
231 fn class_names_are_unique_so_metrics_and_reports_cannot_collide() {
232 let mut names: Vec<&str> = RetentionClass::all().iter().map(|c| c.name()).collect();
233 let total = names.len();
234 names.sort_unstable();
235 names.dedup();
236
237 assert_eq!(names.len(), total, "two classes share a name; their metrics and report lines would merge");
238 }
239
240 #[test]
241 fn row_expiry_classes_are_not_hostage_to_readers_of_other_data() {
242 {
245 let class = RetentionClass::RowTtl;
246 assert!(
247 !class.constrained_by(FloorTerm::ConsumerCheckpoint),
248 "{class} must not be pinned by a CDC consumer; it reclaims rows no consumer reads"
249 );
250 assert!(
251 !class.constrained_by(FloorTerm::LeaseMin),
252 "{class} must not be pinned by an operator-state lease"
253 );
254 assert!(
255 !class.constrained_by(FloorTerm::QueryDoneUntil),
256 "{class} must not be pinned by an in-flight query; transactional discovery protects it"
257 );
258 }
259 }
260
261 #[test]
262 fn version_history_classes_respect_every_reader_of_a_snapshot() {
263 let class = RetentionClass::BufferHistoricalGc;
267
268 for term in [FloorTerm::QueryDoneUntil, FloorTerm::LeaseMin] {
269 assert!(
270 class.constrained_by(term),
271 "{class} must keep the {term} term: it protects {}",
272 term.protects()
273 );
274 }
275 }
276
277 #[test]
278 fn a_lagging_subscription_must_not_pin_buffer_history_between_batches() {
279 assert!(
283 !RetentionClass::BufferHistoricalGc.constrained_by(FloorTerm::ConsumerCheckpoint),
284 "a CDC log consumer reads cdc.db, not buffer history, and must not pin it"
285 );
286 assert!(
287 RetentionClass::BufferHistoricalGc.constrained_by(FloorTerm::LeaseMin),
288 "an in-flight subscription batch protects its reads through its lease, so LeaseMin must stay"
289 );
290 }
291
292 #[test]
293 fn the_flush_floor_tracks_live_positions_while_cdc_truncation_tracks_durable_checkpoints() {
294 assert!(
298 RetentionClass::PersistentFlush.constrained_by(FloorTerm::ConsumerPosition),
299 "flushing the in-memory buffer may only be held back by a reader that is live now"
300 );
301 assert!(
302 !RetentionClass::PersistentFlush.constrained_by(FloorTerm::ConsumerCheckpoint),
303 "a throttled durable checkpoint lags the real read position and must not pin the buffer"
304 );
305
306 assert!(
307 RetentionClass::CdcTruncate.constrained_by(FloorTerm::ConsumerCheckpoint),
308 "cdc.db must retain everything a consumer would replay from after a crash"
309 );
310 assert!(
311 !RetentionClass::CdcTruncate.constrained_by(FloorTerm::ConsumerPosition),
312 "a live position is lost on restart, so it cannot govern durable CDC truncation"
313 );
314 }
315
316 #[test]
317 fn cdc_truncation_is_pinned_only_by_its_consumers() {
318 let class = RetentionClass::CdcTruncate;
321
322 assert!(
323 class.constrained_by(FloorTerm::ConsumerCheckpoint),
324 "the slowest CDC log consumer legitimately pins CDC"
325 );
326 assert!(
327 !class.constrained_by(FloorTerm::QueryDoneUntil),
328 "an in-flight query does not read the CDC log"
329 );
330 assert!(
331 !class.constrained_by(FloorTerm::LeaseMin),
332 "an operator-state lease does not read the CDC log"
333 );
334 }
335
336 #[test]
337 fn the_epoch_log_is_bounded_by_the_longest_ttl_it_must_still_answer() {
338 assert!(
341 RetentionClass::EpochLog.constrained_by(FloorTerm::RetentionHorizon),
342 "pruning epoch samples inside the retention horizon would make long ttls unresolvable"
343 );
344 }
345}