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 FlushWatermark,
65
66 RetentionHorizon,
67}
68
69impl FloorTerm {
70 pub fn protects(&self) -> &'static str {
71 match self {
72 Self::RowExpiry => "rows younger than their declared ttl",
73 Self::QueryDoneUntil => "an in-flight query reading at its snapshot version",
74 Self::LeaseMin => "a held operator-state lease",
75 Self::ConsumerCheckpoint => "a CDC log consumer that has not yet consumed the version",
76 Self::ConsumerPosition => "a live flow that has not yet consumed the version",
77 Self::FlushWatermark => "a write that has not yet reached the persistent tier",
78 Self::RetentionHorizon => "epoch samples still needed to resolve the longest declared ttl",
79 }
80 }
81
82 pub fn is_clock_driven(&self) -> bool {
83 match self {
84 Self::RowExpiry => true,
85 Self::QueryDoneUntil
86 | Self::LeaseMin
87 | Self::ConsumerCheckpoint
88 | Self::ConsumerPosition
89 | Self::FlushWatermark
90 | Self::RetentionHorizon => false,
91 }
92 }
93
94 pub fn all() -> &'static [Self] {
95 &[
96 Self::RowExpiry,
97 Self::QueryDoneUntil,
98 Self::LeaseMin,
99 Self::ConsumerCheckpoint,
100 Self::ConsumerPosition,
101 Self::FlushWatermark,
102 Self::RetentionHorizon,
103 ]
104 }
105
106 pub fn index(&self) -> usize {
107 Self::all().iter().position(|term| term == self).expect("every term is listed in FloorTerm::all")
108 }
109
110 pub fn from_index(index: usize) -> Option<Self> {
111 Self::all().get(index).copied()
112 }
113}
114
115impl fmt::Display for FloorTerm {
116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117 match self {
118 Self::RowExpiry => write!(f, "row-expiry"),
119 Self::QueryDoneUntil => write!(f, "query-done-until"),
120 Self::LeaseMin => write!(f, "lease-min"),
121 Self::ConsumerCheckpoint => write!(f, "consumer-checkpoint"),
122 Self::ConsumerPosition => write!(f, "consumer-position"),
123 Self::FlushWatermark => write!(f, "flush-watermark"),
124 Self::RetentionHorizon => write!(f, "retention-horizon"),
125 }
126 }
127}
128
129#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
130pub enum RetentionClass {
131 RowTtl,
132
133 BufferHistoricalGc,
134
135 PersistentFlush,
136
137 TombstoneReap,
138
139 CdcTruncate,
140
141 EpochLog,
142}
143
144impl RetentionClass {
145 pub fn all() -> &'static [Self] {
146 &[
147 Self::RowTtl,
148 Self::BufferHistoricalGc,
149 Self::PersistentFlush,
150 Self::TombstoneReap,
151 Self::CdcTruncate,
152 Self::EpochLog,
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::TombstoneReap => "tombstone-reap",
162 Self::CdcTruncate => "cdc-truncate",
163 Self::EpochLog => "epoch-log",
164 }
165 }
166
167 pub fn reclaims_versioned_data(&self) -> bool {
168 match self {
169 Self::RowTtl
170 | Self::BufferHistoricalGc
171 | Self::PersistentFlush
172 | Self::TombstoneReap
173 | Self::CdcTruncate
174 | Self::EpochLog => true,
175 }
176 }
177
178 pub fn floor_terms(&self) -> &'static [FloorTerm] {
179 match self {
180 Self::RowTtl => &[FloorTerm::RowExpiry],
181 Self::BufferHistoricalGc => &[FloorTerm::QueryDoneUntil, FloorTerm::LeaseMin],
182 Self::PersistentFlush => {
183 &[FloorTerm::QueryDoneUntil, FloorTerm::LeaseMin, FloorTerm::ConsumerPosition]
184 }
185 Self::TombstoneReap => &[FloorTerm::FlushWatermark],
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}