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, 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-seams"))]
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-seams"))]
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 #[cfg(feature = "graph-cypher")]
172 let mut native_writer = self.native_document_writer()?;
173 #[cfg(feature = "graph-cypher")]
174 if let Some(writer) = native_writer.as_mut().and_then(|slot| slot.as_mut()) {
175 let ids = {
176 let snapshot = self
177 .snapshot
178 .read()
179 .map_err(|_| StoreError::Synchronization {
180 component: "published snapshot",
181 })?;
182 let snapshot = snapshot.as_ref().ok_or(StoreError::Closed)?;
183 let mut ids = Vec::new();
184 if key_range.start < key_range.end {
185 for segment in snapshot.segments() {
186 if matches!(segment.meta().clustering_key_range,
187 ClusteringKeyRange::Bounded { min_ts, max_ts }
188 if min_ts >= key_range.start && max_ts < key_range.end)
189 {
190 for row in segment.query_alive()?.alive_bitmap().iter() {
191 if let Some(version) = segment
192 .document_version(row as usize)
193 .map_err(StoreError::Segment)?
194 {
195 ids.push(version.doc_id());
196 }
197 }
198 }
199 }
200 }
201 ids
202 };
203 if !ids.is_empty()
204 && self
205 .documents_have_native_nodes(&ids)
206 .map_err(|error| StoreError::DocumentMutation(Box::new(error)))?
207 {
208 self.delete_native_documents_locked(&super::DeleteBatch::new(ids), writer)
209 .map_err(|error| StoreError::DocumentMutation(Box::new(error)))?;
210 }
211 }
212 let state = self
213 .state
214 .lock()
215 .map_err(|_| StoreError::Synchronization { component: "state" })?;
216 match *state {
217 StoreState::Open => {}
218 StoreState::Closing => return Err(StoreError::Closing),
219 StoreState::Closed => return Err(StoreError::Closed),
220 }
221 let writer_lock = self
222 .writer_lock
223 .lock()
224 .map_err(|_| StoreError::Synchronization {
225 component: "writer lock",
226 })?;
227 if writer_lock.is_none() {
228 return Err(StoreError::ReadOnly);
229 }
230 let wal = self
231 .wal_writer
232 .lock()
233 .map_err(|_| StoreError::Synchronization {
234 component: "WAL writer",
235 })?;
236 let mut publication = wal
237 .as_ref()
238 .ok_or(StoreError::ReadOnly)?
239 .manifest_publication()?;
240 let durable_end = wal.as_ref().ok_or(StoreError::ReadOnly)?.durable_end();
241 let mut active = self
242 .active
243 .lock()
244 .map_err(|_| StoreError::Synchronization {
245 component: "active segment",
246 })?;
247 let active_state = active.as_mut().ok_or(StoreError::Closed)?;
248 if key_range.start >= key_range.end {
249 return Ok(no_op_report(active_state.generation, Vec::new()));
250 }
251 let manifest_path = self.directory.join(MANIFEST_FILE);
252 let mut manifest = match vfs.open(&manifest_path) {
253 Ok(_) => {
254 load_manifest(vfs, &manifest_path, durable_end).map_err(StoreError::Manifest)?
255 }
256 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
257 return Ok(no_op_report(active_state.generation, Vec::new()));
258 }
259 Err(source) => {
260 return Err(StoreError::Io {
261 path: manifest_path,
262 source,
263 });
264 }
265 };
266 let selection = select_segments(manifest.segments, &key_range)?;
267 if selection.dropped.is_empty() {
268 return Ok(no_op_report(
269 active_state.generation.max(manifest.generation),
270 selection.straddlers,
271 ));
272 }
273 let generation = active_state
274 .generation
275 .max(manifest.generation)
276 .checked_add(1)
277 .ok_or(StoreError::GenerationOverflow)?;
278 manifest.generation = generation;
279 manifest.segments = selection.retained;
280 #[cfg(feature = "graph-cypher")]
281 manifest
282 .record_generation_bump(durable_end)
283 .map_err(StoreError::Manifest)?;
284 manifest.epochs = self.epoch_registry(&manifest.epochs);
285 let remapped =
289 PublishedSnapshot::from_manifest(vfs, &self.directory, &manifest, &self.accounting)?;
290 publication
291 .commit_manifest(vfs, &self.directory, &manifest, self.durability_policy)
292 .map_err(StoreError::Manifest)?;
293
294 let mut published = self
298 .snapshot
299 .write()
300 .map_err(|_| StoreError::Synchronization {
301 component: "published snapshot",
302 })?;
303 let previous = published.replace(Arc::new(remapped));
304 active_state.generation = generation;
305 #[cfg(feature = "graph-cypher")]
306 {
307 drop(published);
308 self.publish_native_documents(active_state)?;
309 }
310 publication.complete();
311 #[cfg(not(feature = "graph-cypher"))]
312 drop(published);
313 drop(previous);
314
315 let mut bytes_reclaimed = selection.bytes_reclaimed;
316 let mut orphaned_segments = Vec::new();
317 for segment in &selection.dropped {
318 let path = self.directory.join(segment.id.file_name());
319 if self.has_snapshot_views() || vfs.delete(&path).is_err() {
326 bytes_reclaimed = bytes_reclaimed.saturating_sub(segment.file_size);
327 orphaned_segments.push(segment.id);
328 }
329 }
330 if let SyncRequirement::Sync(kind) = self.durability_policy.directory_sync() {
331 let _ = vfs.sync(&self.directory, kind);
335 }
336 let dropped_ids = selection.dropped.iter().map(|segment| segment.id).collect();
337 drop(active);
338 drop(wal);
339 drop(writer_lock);
340 drop(state);
341 Ok(DropPartitionReport {
342 generation,
343 manifest_committed: true,
344 segments_dropped: dropped_ids,
345 bytes_reclaimed,
346 orphaned_segments,
347 straddlers_skipped: selection.straddlers,
348 })
349 }
350}
351
352fn no_op_report(generation: u64, straddlers_skipped: Vec<SegmentId>) -> DropPartitionReport {
353 DropPartitionReport {
354 generation,
355 manifest_committed: false,
356 segments_dropped: Vec::new(),
357 bytes_reclaimed: 0,
358 orphaned_segments: Vec::new(),
359 straddlers_skipped,
360 }
361}
362
363struct SegmentSelection {
364 retained: Vec<SegmentMeta>,
365 dropped: Vec<SegmentMeta>,
366 straddlers: Vec<SegmentId>,
367 bytes_reclaimed: u64,
368}
369
370fn select_segments(
371 segments: Vec<SegmentMeta>,
372 requested: &Range<i64>,
373) -> Result<SegmentSelection, StoreError> {
374 let mut retained = Vec::with_capacity(segments.len());
375 let mut dropped = Vec::new();
376 let mut straddlers = Vec::new();
377 let mut bytes_reclaimed = 0_u64;
378 for segment in segments {
379 match segment.clustering_key_range {
380 ClusteringKeyRange::Empty => {
384 bytes_reclaimed = bytes_reclaimed
385 .checked_add(segment.file_size)
386 .ok_or(StoreError::PartitionBytesOverflow)?;
387 dropped.push(segment);
388 }
389 ClusteringKeyRange::Bounded { min_ts, max_ts }
390 if min_ts >= requested.start && max_ts < requested.end =>
391 {
392 bytes_reclaimed = bytes_reclaimed
393 .checked_add(segment.file_size)
394 .ok_or(StoreError::PartitionBytesOverflow)?;
395 dropped.push(segment);
396 }
397 ClusteringKeyRange::Bounded { min_ts, max_ts }
398 if max_ts >= requested.start && min_ts < requested.end =>
399 {
400 straddlers.push(segment.id);
401 retained.push(segment);
402 }
403 ClusteringKeyRange::Unstamped | ClusteringKeyRange::Bounded { .. } => {
404 retained.push(segment);
405 }
406 }
407 }
408 Ok(SegmentSelection {
409 retained,
410 dropped,
411 straddlers,
412 bytes_reclaimed,
413 })
414}