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_nanos(),
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
140impl RetentionClass {
141 pub fn all() -> &'static [Self] {
142 &[
143 Self::RowTtl,
144 Self::BufferHistoricalGc,
145 Self::PersistentFlush,
146 Self::QueueLeaseReap,
147 Self::QueueRetention,
148 Self::CdcTruncate,
149 Self::EpochLog,
150 ]
151 }
152
153 pub fn name(&self) -> &'static str {
154 match self {
155 Self::RowTtl => "row-ttl-silent",
156 Self::BufferHistoricalGc => "buffer-historical-gc",
157 Self::PersistentFlush => "persistent-flush",
158 Self::QueueLeaseReap => "queue-lease-reap",
159 Self::QueueRetention => "queue-retention",
160 Self::CdcTruncate => "cdc-truncate",
161 Self::EpochLog => "epoch-log",
162 }
163 }
164
165 pub fn reclaims_versioned_data(&self) -> bool {
166 match self {
167 Self::RowTtl
168 | Self::BufferHistoricalGc
169 | Self::PersistentFlush
170 | Self::CdcTruncate
171 | Self::EpochLog
172 | Self::QueueRetention => true,
173 Self::QueueLeaseReap => false,
174 }
175 }
176
177 pub fn floor_terms(&self) -> &'static [FloorTerm] {
178 match self {
179 Self::RowTtl => &[FloorTerm::RowExpiry],
180 Self::BufferHistoricalGc => &[FloorTerm::QueryDoneUntil, FloorTerm::LeaseMin],
181 Self::PersistentFlush => {
182 &[FloorTerm::QueryDoneUntil, FloorTerm::LeaseMin, FloorTerm::ConsumerPosition]
183 }
184 Self::QueueLeaseReap => &[],
185 Self::QueueRetention => &[FloorTerm::RowExpiry],
186 Self::CdcTruncate => &[FloorTerm::ConsumerCheckpoint],
187 Self::EpochLog => &[FloorTerm::RetentionHorizon],
188 }
189 }
190
191 pub fn constrained_by(&self, term: FloorTerm) -> bool {
192 self.floor_terms().contains(&term)
193 }
194}
195
196impl fmt::Display for RetentionClass {
197 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198 write!(f, "{}", self.name())
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::{FloorTerm, RetentionClass};
205
206 #[test]
207 fn every_class_declares_a_floor_term_for_exactly_the_data_it_reclaims() {
208 for class in RetentionClass::all() {
212 if class.reclaims_versioned_data() {
213 assert!(
214 !class.floor_terms().is_empty(),
215 "{class} declares no floor term, so nothing bounds what it deletes"
216 );
217 } else {
218 assert!(
219 class.floor_terms().is_empty(),
220 "{class} reclaims no versioned data, so a version floor cannot be what bounds it"
221 );
222 }
223 }
224 }
225
226 #[test]
227 fn class_names_are_unique_so_metrics_and_reports_cannot_collide() {
228 let mut names: Vec<&str> = RetentionClass::all().iter().map(|c| c.name()).collect();
229 let total = names.len();
230 names.sort_unstable();
231 names.dedup();
232
233 assert_eq!(names.len(), total, "two classes share a name; their metrics and report lines would merge");
234 }
235
236 #[test]
237 fn row_expiry_classes_are_not_hostage_to_readers_of_other_data() {
238 for class in [RetentionClass::RowTtl] {
241 assert!(
242 !class.constrained_by(FloorTerm::ConsumerCheckpoint),
243 "{class} must not be pinned by a CDC consumer; it reclaims rows no consumer reads"
244 );
245 assert!(
246 !class.constrained_by(FloorTerm::LeaseMin),
247 "{class} must not be pinned by an operator-state lease"
248 );
249 assert!(
250 !class.constrained_by(FloorTerm::QueryDoneUntil),
251 "{class} must not be pinned by an in-flight query; transactional discovery protects it"
252 );
253 }
254 }
255
256 #[test]
257 fn version_history_classes_respect_every_reader_of_a_snapshot() {
258 let class = RetentionClass::BufferHistoricalGc;
262
263 for term in [FloorTerm::QueryDoneUntil, FloorTerm::LeaseMin] {
264 assert!(
265 class.constrained_by(term),
266 "{class} must keep the {term} term: it protects {}",
267 term.protects()
268 );
269 }
270 }
271
272 #[test]
273 fn a_lagging_subscription_must_not_pin_buffer_history_between_batches() {
274 assert!(
278 !RetentionClass::BufferHistoricalGc.constrained_by(FloorTerm::ConsumerCheckpoint),
279 "a CDC log consumer reads cdc.db, not buffer history, and must not pin it"
280 );
281 assert!(
282 RetentionClass::BufferHistoricalGc.constrained_by(FloorTerm::LeaseMin),
283 "an in-flight subscription batch protects its reads through its lease, so LeaseMin must stay"
284 );
285 }
286
287 #[test]
288 fn the_flush_floor_tracks_live_positions_while_cdc_truncation_tracks_durable_checkpoints() {
289 assert!(
293 RetentionClass::PersistentFlush.constrained_by(FloorTerm::ConsumerPosition),
294 "flushing the in-memory buffer may only be held back by a reader that is live now"
295 );
296 assert!(
297 !RetentionClass::PersistentFlush.constrained_by(FloorTerm::ConsumerCheckpoint),
298 "a throttled durable checkpoint lags the real read position and must not pin the buffer"
299 );
300
301 assert!(
302 RetentionClass::CdcTruncate.constrained_by(FloorTerm::ConsumerCheckpoint),
303 "cdc.db must retain everything a consumer would replay from after a crash"
304 );
305 assert!(
306 !RetentionClass::CdcTruncate.constrained_by(FloorTerm::ConsumerPosition),
307 "a live position is lost on restart, so it cannot govern durable CDC truncation"
308 );
309 }
310
311 #[test]
312 fn cdc_truncation_is_pinned_only_by_its_consumers() {
313 let class = RetentionClass::CdcTruncate;
316
317 assert!(
318 class.constrained_by(FloorTerm::ConsumerCheckpoint),
319 "the slowest CDC log consumer legitimately pins CDC"
320 );
321 assert!(
322 !class.constrained_by(FloorTerm::QueryDoneUntil),
323 "an in-flight query does not read the CDC log"
324 );
325 assert!(
326 !class.constrained_by(FloorTerm::LeaseMin),
327 "an operator-state lease does not read the CDC log"
328 );
329 }
330
331 #[test]
332 fn the_epoch_log_is_bounded_by_the_longest_ttl_it_must_still_answer() {
333 assert!(
336 RetentionClass::EpochLog.constrained_by(FloorTerm::RetentionHorizon),
337 "pruning epoch samples inside the retention horizon would make long ttls unresolvable"
338 );
339 }
340}