1use std::ops::Range;
4use std::sync::Arc;
5
6use crate::lifecycle::durability::SyncRequirement;
7use crate::lifecycle::{PublishedSnapshot, Store, StoreError, StoreState};
8use crate::manifest::io::{MANIFEST_FILE, commit_manifest, load_manifest};
9use crate::segment::{ClusteringKeyRange, SegmentId, SegmentMeta};
10use crate::vfs::Vfs;
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub struct RetentionPolicy {
15 window: i64,
16}
17
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub struct RetentionPolicyError {
21 window: i64,
22}
23
24impl std::fmt::Display for RetentionPolicyError {
25 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 write!(
27 formatter,
28 "retention window {} must be positive",
29 self.window
30 )
31 }
32}
33
34impl std::error::Error for RetentionPolicyError {}
35
36impl RetentionPolicy {
37 pub const fn new(window: i64) -> Result<Self, RetentionPolicyError> {
39 if window <= 0 {
40 Err(RetentionPolicyError { window })
41 } else {
42 Ok(Self { window })
43 }
44 }
45
46 #[must_use]
48 pub const fn partition_to_drop(self, now_ts: i64) -> Range<i64> {
49 i64::MIN..now_ts.saturating_sub(self.window)
50 }
51}
52
53#[derive(Clone, Debug, Eq, PartialEq)]
55pub struct DropPartitionReport {
56 generation: u64,
57 manifest_committed: bool,
58 segments_dropped: Vec<SegmentId>,
59 bytes_reclaimed: u64,
60 orphaned_segments: Vec<SegmentId>,
61 straddlers_skipped: Vec<SegmentId>,
62}
63
64impl DropPartitionReport {
65 #[must_use]
67 pub const fn generation(&self) -> u64 {
68 self.generation
69 }
70
71 #[must_use]
73 pub const fn is_no_op(&self) -> bool {
74 !self.manifest_committed
75 }
76
77 #[must_use]
79 pub fn segments_dropped(&self) -> &[SegmentId] {
80 &self.segments_dropped
81 }
82
83 #[must_use]
85 pub const fn bytes_reclaimed(&self) -> u64 {
86 self.bytes_reclaimed
87 }
88
89 #[must_use]
91 pub fn orphaned_segments(&self) -> &[SegmentId] {
92 &self.orphaned_segments
93 }
94
95 #[must_use]
97 pub fn straddlers_skipped(&self) -> &[SegmentId] {
98 &self.straddlers_skipped
99 }
100}
101
102impl Store {
103 pub fn drop_partition(&self, key_range: Range<i64>) -> Result<DropPartitionReport, StoreError> {
105 let retained_through = key_range.end;
106 let report = self.drop_partition_on_vfs(key_range, self.vfs.as_ref())?;
107 self.record_retention(retained_through)?;
108 Ok(report)
109 }
110
111 pub fn apply_retention(
113 &self,
114 policy: RetentionPolicy,
115 now_ts: i64,
116 ) -> Result<DropPartitionReport, StoreError> {
117 let range = policy.partition_to_drop(now_ts);
118 #[cfg(any(test, feature = "test-support"))]
119 let receipt_plan = match self.ingest_retention_fault_controller.as_ref() {
120 Some(controller) => controller
121 .retention_clock_boundary_plan(now_ts, policy.window, range.end)
122 .map_err(|_| StoreError::Synchronization {
123 component: "ingest-retention controller",
124 })?,
125 None => None,
126 };
127 let report = self.drop_partition(range)?;
128 #[cfg(any(test, feature = "test-support"))]
129 if let Some(invocation_id) = receipt_plan
130 && let Some(controller) = self.ingest_retention_fault_controller.as_ref()
131 {
132 let dropped_count = u64::try_from(report.segments_dropped.len())
133 .map_err(|_| StoreError::PartitionBytesOverflow)?;
134 let straddler_count = u64::try_from(report.straddlers_skipped.len())
135 .map_err(|_| StoreError::PartitionBytesOverflow)?;
136 controller
137 .push_receipt(
138 super::IngestRetentionFaultReceiptV1::retention_clock_boundary(
139 invocation_id,
140 now_ts,
141 policy.window,
142 policy.partition_to_drop(now_ts).end,
143 report.generation,
144 dropped_count,
145 straddler_count,
146 report.manifest_committed,
147 ),
148 )
149 .map_err(|_| StoreError::Synchronization {
150 component: "ingest-retention controller",
151 })?;
152 }
153 Ok(report)
154 }
155
156 #[doc(hidden)]
158 pub fn drop_partition_on_vfs(
159 &self,
160 key_range: Range<i64>,
161 vfs: &dyn Vfs,
162 ) -> Result<DropPartitionReport, StoreError> {
163 let _data_read_audit =
164 crate::segment::reader::install_data_read_audit(vfs.segment_data_read_counter());
165 let _maintenance = self
166 .maintenance
167 .lock()
168 .map_err(|_| StoreError::Synchronization {
169 component: "partition retention",
170 })?;
171 let state = self
172 .state
173 .lock()
174 .map_err(|_| StoreError::Synchronization { component: "state" })?;
175 match *state {
176 StoreState::Open => {}
177 StoreState::Closing => return Err(StoreError::Closing),
178 StoreState::Closed => return Err(StoreError::Closed),
179 }
180 let writer_lock = self
181 .writer_lock
182 .lock()
183 .map_err(|_| StoreError::Synchronization {
184 component: "writer lock",
185 })?;
186 if writer_lock.is_none() {
187 return Err(StoreError::ReadOnly);
188 }
189 let wal = self
190 .wal_writer
191 .lock()
192 .map_err(|_| StoreError::Synchronization {
193 component: "WAL writer",
194 })?;
195 let durable_end = wal.as_ref().ok_or(StoreError::ReadOnly)?.durable_end();
196 let mut active = self
197 .active
198 .lock()
199 .map_err(|_| StoreError::Synchronization {
200 component: "active segment",
201 })?;
202 let active_state = active.as_mut().ok_or(StoreError::Closed)?;
203 if key_range.start >= key_range.end {
204 return Ok(no_op_report(active_state.generation, Vec::new()));
205 }
206 let manifest_path = self.directory.join(MANIFEST_FILE);
207 let mut manifest = match vfs.open(&manifest_path) {
208 Ok(_) => {
209 load_manifest(vfs, &manifest_path, durable_end).map_err(StoreError::Manifest)?
210 }
211 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
212 return Ok(no_op_report(active_state.generation, Vec::new()));
213 }
214 Err(source) => {
215 return Err(StoreError::Io {
216 path: manifest_path,
217 source,
218 });
219 }
220 };
221 let selection = select_segments(manifest.segments, &key_range)?;
222 if selection.dropped.is_empty() {
223 return Ok(no_op_report(
224 active_state.generation.max(manifest.generation),
225 selection.straddlers,
226 ));
227 }
228 let generation = active_state
229 .generation
230 .max(manifest.generation)
231 .checked_add(1)
232 .ok_or(StoreError::GenerationOverflow)?;
233 manifest.generation = generation;
234 manifest.segments = selection.retained;
235 manifest.epochs = self.epoch_registry(&manifest.epochs);
236 let remapped =
240 PublishedSnapshot::from_manifest(vfs, &self.directory, &manifest, &self.accounting)?;
241 commit_manifest(vfs, &self.directory, &manifest, self.durability_policy)
242 .map_err(StoreError::Manifest)?;
243
244 let mut published = self
248 .snapshot
249 .write()
250 .map_err(|_| StoreError::Synchronization {
251 component: "published snapshot",
252 })?;
253 let previous = published.replace(Arc::new(remapped));
254 active_state.generation = generation;
255 drop(published);
256 drop(previous);
257
258 let mut bytes_reclaimed = selection.bytes_reclaimed;
259 let mut orphaned_segments = Vec::new();
260 for segment in &selection.dropped {
261 let path = self.directory.join(segment.id.file_name());
262 if vfs.delete(&path).is_err() {
269 bytes_reclaimed = bytes_reclaimed.saturating_sub(segment.file_size);
270 orphaned_segments.push(segment.id);
271 }
272 }
273 if let SyncRequirement::Sync(kind) = self.durability_policy.directory_sync() {
274 let _ = vfs.sync(&self.directory, kind);
278 }
279 let dropped_ids = selection.dropped.iter().map(|segment| segment.id).collect();
280 drop(active);
281 drop(wal);
282 drop(writer_lock);
283 drop(state);
284 Ok(DropPartitionReport {
285 generation,
286 manifest_committed: true,
287 segments_dropped: dropped_ids,
288 bytes_reclaimed,
289 orphaned_segments,
290 straddlers_skipped: selection.straddlers,
291 })
292 }
293}
294
295fn no_op_report(generation: u64, straddlers_skipped: Vec<SegmentId>) -> DropPartitionReport {
296 DropPartitionReport {
297 generation,
298 manifest_committed: false,
299 segments_dropped: Vec::new(),
300 bytes_reclaimed: 0,
301 orphaned_segments: Vec::new(),
302 straddlers_skipped,
303 }
304}
305
306struct SegmentSelection {
307 retained: Vec<SegmentMeta>,
308 dropped: Vec<SegmentMeta>,
309 straddlers: Vec<SegmentId>,
310 bytes_reclaimed: u64,
311}
312
313fn select_segments(
314 segments: Vec<SegmentMeta>,
315 requested: &Range<i64>,
316) -> Result<SegmentSelection, StoreError> {
317 let mut retained = Vec::with_capacity(segments.len());
318 let mut dropped = Vec::new();
319 let mut straddlers = Vec::new();
320 let mut bytes_reclaimed = 0_u64;
321 for segment in segments {
322 match segment.clustering_key_range {
323 ClusteringKeyRange::Empty => {
327 bytes_reclaimed = bytes_reclaimed
328 .checked_add(segment.file_size)
329 .ok_or(StoreError::PartitionBytesOverflow)?;
330 dropped.push(segment);
331 }
332 ClusteringKeyRange::Bounded { min_ts, max_ts }
333 if min_ts >= requested.start && max_ts < requested.end =>
334 {
335 bytes_reclaimed = bytes_reclaimed
336 .checked_add(segment.file_size)
337 .ok_or(StoreError::PartitionBytesOverflow)?;
338 dropped.push(segment);
339 }
340 ClusteringKeyRange::Bounded { min_ts, max_ts }
341 if max_ts >= requested.start && min_ts < requested.end =>
342 {
343 straddlers.push(segment.id);
344 retained.push(segment);
345 }
346 ClusteringKeyRange::Unstamped | ClusteringKeyRange::Bounded { .. } => {
347 retained.push(segment);
348 }
349 }
350 }
351 Ok(SegmentSelection {
352 retained,
353 dropped,
354 straddlers,
355 bytes_reclaimed,
356 })
357}