1#![allow(clippy::disallowed_types)] use std::sync::Arc;
6
7use async_trait::async_trait;
8use bytes::Bytes;
9use object_store::{ObjectStore, PutPayload};
10
11use crate::checkpoint::checkpoint_manifest::{
12 checkpoint_artifact_intent_sha256, checkpoint_descriptor_sha256, ByteRange, CheckpointManifest,
13 PreparedSinkArtifactIntent, PreparedSinkDescriptor, StateChunkId,
14};
15use crate::checkpoint::{canonical_json_bytes, OutputSegmentRef};
16use crate::state::{KeyGroupCount, DEFAULT_KEY_GROUP_COUNT, LOCAL_NODE_ID};
17
18mod abort_seal;
19mod artifact_identity;
20mod artifact_intent;
21mod artifact_payload;
22mod conditional_probe;
23mod object_store_io;
24mod subscription_segments;
25mod validation;
26
27pub use artifact_identity::checkpoint_artifact_identity_sha256;
28pub use artifact_intent::{
29 CheckpointSinkArtifactIntent, MAX_CHECKPOINT_SINK_ARTIFACT_INTENT_AGGREGATE_BYTES,
30 MAX_CHECKPOINT_SINK_ARTIFACT_INTENT_BYTES,
31};
32pub use conditional_probe::{
33 probe_object_store_conditional_create, probe_object_store_conditional_update,
34};
35pub use subscription_segments::SubscriptionOrphanCleanup;
36pub use validation::validate_max_checkpoint_node_data_bytes;
37use validation::{
38 checkpoint_artifact_abort_seal_bytes, ensure_manifest_valid, normalize_prefix, sha256,
39 validate_abort_seal_request, validate_node_data_layout, ManifestAbortState,
40};
41
42const MAX_MANIFEST_BYTES: u64 = 16 * 1024 * 1024;
43const MAX_ABORT_SEAL_BYTES: u64 =
44 MAX_MANIFEST_BYTES + artifact_intent::MAX_CHECKPOINT_ARTIFACT_INTENT_RECORD_BYTES + 64 * 1024;
45const CHECKPOINT_ARTIFACT_ABORT_SEAL_VERSION: u32 = 3;
46const CHECKPOINT_ARTIFACT_ABORT_SEAL_VERSION_V2: u32 = 2;
47const CHECKPOINT_ARTIFACT_ABORT_SEAL_VERSION_V1: u32 = 1;
48
49pub const DEFAULT_MAX_CHECKPOINT_NODE_DATA_BYTES: u64 = 512 * 1024 * 1024;
51
52#[derive(Debug, thiserror::Error)]
54pub enum CheckpointStoreError {
55 #[error("object store: {0}")]
57 ObjectStore(#[from] object_store::Error),
58 #[error("manifest serialization: {0}")]
60 Serde(#[from] serde_json::Error),
61 #[error("invalid checkpoint: {0}")]
63 Invalid(String),
64 #[error("checkpoint {0} not found")]
66 NotFound(u64),
67}
68
69pub fn checkpoint_manifest_bytes(
74 manifest: &CheckpointManifest,
75) -> Result<Vec<u8>, serde_json::Error> {
76 canonical_json_bytes(manifest)
77}
78
79#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
80#[serde(deny_unknown_fields)]
81struct CheckpointArtifactAbortSeal {
82 version: u32,
83 artifact_identity_sha256: String,
84 chunk: StateChunkId,
85 original_manifest: Option<CheckpointManifest>,
86 #[serde(default, skip_serializing_if = "is_false")]
87 sink_artifact_intent_protocol: bool,
88 #[serde(default, skip_serializing_if = "Vec::is_empty")]
89 open_sink_artifact_intents: Vec<CheckpointSinkArtifactIntent>,
90 #[serde(default, skip_serializing_if = "is_false")]
91 sink_cleanup_complete: bool,
92}
93
94const fn is_false(value: &bool) -> bool {
95 !*value
96}
97
98#[derive(Clone, PartialEq)]
100pub struct CheckpointManifestAbortSeal {
101 pub original_manifest: Option<(CheckpointManifest, Bytes)>,
103 pub sink_artifact_intent_protocol: bool,
105 pub open_sink_artifact_intents: Vec<CheckpointSinkArtifactIntent>,
107 pub sink_cleanup_complete: bool,
109}
110
111impl std::fmt::Debug for CheckpointManifestAbortSeal {
112 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 formatter
114 .debug_struct("CheckpointManifestAbortSeal")
115 .field("has_original_manifest", &self.original_manifest.is_some())
116 .field(
117 "sink_artifact_intent_protocol",
118 &self.sink_artifact_intent_protocol,
119 )
120 .field(
121 "open_sink_artifact_intent_count",
122 &self.open_sink_artifact_intents.len(),
123 )
124 .field("sink_cleanup_complete", &self.sink_cleanup_complete)
125 .finish()
126 }
127}
128
129#[async_trait]
131pub trait CheckpointStore: Send + Sync {
132 fn max_node_data_bytes(&self) -> u64;
134
135 fn key_group_count(&self) -> KeyGroupCount {
137 DEFAULT_KEY_GROUP_COUNT
138 }
139
140 fn participant_id(&self) -> u64 {
142 LOCAL_NODE_ID.0
143 }
144
145 async fn save_checkpoint(
148 &self,
149 manifest: &CheckpointManifest,
150 node_data: &[Bytes],
151 ) -> Result<Bytes, CheckpointStoreError>;
152
153 async fn save_sink_artifact_intents(
155 &self,
156 chunk: StateChunkId,
157 expected_artifact_identity_sha256: &str,
158 intents: Vec<CheckpointSinkArtifactIntent>,
159 ) -> Result<(), CheckpointStoreError>;
160
161 async fn seal_aborted_manifest(
165 &self,
166 chunk: StateChunkId,
167 expected_artifact_identity_sha256: &str,
168 sink_artifact_intent_protocol: bool,
169 ) -> Result<CheckpointManifestAbortSeal, CheckpointStoreError>;
170
171 async fn complete_aborted_sink_cleanup(
173 &self,
174 chunk: StateChunkId,
175 expected_artifact_identity_sha256: &str,
176 ) -> Result<CheckpointManifestAbortSeal, CheckpointStoreError>;
177
178 async fn seal_aborted_node_data(
180 &self,
181 chunk: StateChunkId,
182 expected_artifact_identity_sha256: &str,
183 ) -> Result<(), CheckpointStoreError>;
184
185 async fn load_manifest(
187 &self,
188 checkpoint_id: u64,
189 ) -> Result<Option<CheckpointManifest>, CheckpointStoreError> {
190 self.load_manifest_for_participant(self.participant_id(), checkpoint_id)
191 .await
192 }
193
194 async fn load_manifest_for_participant(
196 &self,
197 participant_id: u64,
198 checkpoint_id: u64,
199 ) -> Result<Option<CheckpointManifest>, CheckpointStoreError>;
200
201 async fn load_manifest_verified(
203 &self,
204 participant_id: u64,
205 checkpoint_id: u64,
206 expected_len: u64,
207 expected_sha256: &str,
208 ) -> Result<Option<CheckpointManifest>, CheckpointStoreError>;
209
210 async fn load_node_data_ranges(
212 &self,
213 chunk: StateChunkId,
214 expected_object_length: u64,
215 ranges: &[ByteRange],
216 ) -> Result<Option<Vec<Bytes>>, CheckpointStoreError>;
217
218 async fn delete_manifest(&self, chunk: StateChunkId) -> Result<(), CheckpointStoreError>;
221
222 async fn delete_node_data(&self, chunk: StateChunkId) -> Result<(), CheckpointStoreError>;
224
225 async fn load_prepared_sink_descriptor(
227 &self,
228 manifest: &CheckpointManifest,
229 descriptor: &PreparedSinkDescriptor,
230 ) -> Result<Option<Bytes>, CheckpointStoreError> {
231 artifact_payload::load_optional(
232 self,
233 manifest,
234 &descriptor.sink_name,
235 descriptor.payload,
236 &descriptor.sha256,
237 checkpoint_descriptor_sha256,
238 "prepared sink descriptor",
239 )
240 .await
241 }
242
243 async fn load_sink_artifact_intent(
245 &self,
246 manifest: &CheckpointManifest,
247 intent: &PreparedSinkArtifactIntent,
248 ) -> Result<Option<Bytes>, CheckpointStoreError> {
249 artifact_payload::load_optional(
250 self,
251 manifest,
252 &intent.sink_name,
253 intent.payload,
254 &intent.sha256,
255 checkpoint_artifact_intent_sha256,
256 "sink artifact intent",
257 )
258 .await
259 }
260
261 async fn save_subscription_segment(
263 &self,
264 _segment: &OutputSegmentRef,
265 _payload: Bytes,
266 ) -> Result<(), CheckpointStoreError> {
267 Err(subscription_segments::unsupported_store_error())
268 }
269
270 async fn load_subscription_segment(
272 &self,
273 _segment: &OutputSegmentRef,
274 ) -> Result<Option<Bytes>, CheckpointStoreError> {
275 Err(subscription_segments::unsupported_store_error())
276 }
277
278 async fn delete_subscription_segment(
280 &self,
281 _object_key: &str,
282 ) -> Result<(), CheckpointStoreError> {
283 Err(subscription_segments::unsupported_store_error())
284 }
285
286 async fn delete_subscription_orphans(
290 &self,
291 _reachable: &std::collections::BTreeSet<String>,
292 _through_checkpoint_id: u64,
293 _grace_before_ms: i64,
294 ) -> Result<SubscriptionOrphanCleanup, CheckpointStoreError> {
295 Err(subscription_segments::unsupported_store_error())
296 }
297}
298
299pub struct ObjectStoreCheckpointStore {
301 store: Arc<dyn ObjectStore>,
302 prefix: String,
303 key_group_count: KeyGroupCount,
304 participant_id: u64,
305 max_node_data_bytes: u64,
306 exclusive_writer: bool,
307}
308
309#[async_trait]
310impl CheckpointStore for ObjectStoreCheckpointStore {
311 fn max_node_data_bytes(&self) -> u64 {
312 self.max_node_data_bytes
313 }
314
315 fn key_group_count(&self) -> KeyGroupCount {
316 self.key_group_count
317 }
318
319 fn participant_id(&self) -> u64 {
320 self.participant_id
321 }
322
323 async fn save_checkpoint(
324 &self,
325 manifest: &CheckpointManifest,
326 node_data: &[Bytes],
327 ) -> Result<Bytes, CheckpointStoreError> {
328 ensure_manifest_valid(
329 manifest,
330 self.participant_id,
331 self.key_group_count,
332 self.max_node_data_bytes,
333 )?;
334 validate_node_data_layout(manifest, node_data, self.max_node_data_bytes)?;
335
336 let encoded = Bytes::from(checkpoint_manifest_bytes(manifest)?);
337 if u64::try_from(encoded.len()).unwrap_or(u64::MAX) > MAX_MANIFEST_BYTES {
338 return Err(CheckpointStoreError::Invalid(format!(
339 "manifest exceeds the {MAX_MANIFEST_BYTES}-byte limit"
340 )));
341 }
342
343 let payload: PutPayload = node_data.iter().cloned().collect();
344 let path = self.node_data_path(manifest.node_data.chunk);
345 if !self.create_immutable(&path, payload).await?
346 && !self.existing_node_data_matches(&manifest.node_data).await?
347 {
348 return Err(CheckpointStoreError::Invalid(format!(
349 "node data for participant {} checkpoint {} already exists with different immutable content",
350 manifest.node_data.chunk.participant_id, manifest.node_data.chunk.checkpoint_id
351 )));
352 }
353
354 let path = self.manifest_path(manifest.node_data.chunk);
357 if !self
358 .create_immutable(&path, PutPayload::from_bytes(encoded.clone()))
359 .await?
360 {
361 match self
362 .load_manifest_or_abort_seal_bytes(manifest.node_data.chunk)
363 .await?
364 {
365 Some(existing) if existing == encoded => {}
366 Some(existing)
367 if self
368 .promote_artifact_intent_to_manifest(manifest, encoded.clone(), &existing)
369 .await? => {}
370 Some(_) => {
371 return Err(CheckpointStoreError::Invalid(format!(
372 "checkpoint {} manifest already exists with different immutable content",
373 manifest.checkpoint_id
374 )));
375 }
376 None => {
377 return Err(CheckpointStoreError::Invalid(format!(
378 "checkpoint {} manifest create reported a conflict but no object exists",
379 manifest.checkpoint_id
380 )));
381 }
382 }
383 }
384 Ok(encoded)
385 }
386
387 async fn save_sink_artifact_intents(
388 &self,
389 chunk: StateChunkId,
390 expected_artifact_identity_sha256: &str,
391 intents: Vec<CheckpointSinkArtifactIntent>,
392 ) -> Result<(), CheckpointStoreError> {
393 self.save_artifact_intent_record(chunk, expected_artifact_identity_sha256, intents)
394 .await
395 }
396
397 async fn seal_aborted_manifest(
398 &self,
399 chunk: StateChunkId,
400 expected_artifact_identity_sha256: &str,
401 sink_artifact_intent_protocol: bool,
402 ) -> Result<CheckpointManifestAbortSeal, CheckpointStoreError> {
403 validate_abort_seal_request(chunk, expected_artifact_identity_sha256)?;
404 let empty_seal = CheckpointArtifactAbortSeal {
405 version: CHECKPOINT_ARTIFACT_ABORT_SEAL_VERSION,
406 artifact_identity_sha256: expected_artifact_identity_sha256.to_owned(),
407 chunk,
408 original_manifest: None,
409 sink_artifact_intent_protocol,
410 open_sink_artifact_intents: Vec::new(),
411 sink_cleanup_complete: false,
412 };
413 let empty_seal_bytes = checkpoint_artifact_abort_seal_bytes(&empty_seal)?;
414 let path = self.manifest_path(chunk);
415 if self
416 .create_immutable(&path, PutPayload::from_bytes(empty_seal_bytes.clone()))
417 .await?
418 {
419 return Ok(CheckpointManifestAbortSeal {
420 original_manifest: None,
421 sink_artifact_intent_protocol,
422 open_sink_artifact_intents: Vec::new(),
423 sink_cleanup_complete: false,
424 });
425 }
426
427 let current = self
428 .load_manifest_or_abort_seal_bytes(chunk)
429 .await?
430 .ok_or_else(|| {
431 CheckpointStoreError::Invalid(format!(
432 "participant {} checkpoint {} manifest seal create conflicted but no object exists",
433 chunk.participant_id, chunk.checkpoint_id
434 ))
435 })?;
436 match self.decode_manifest_abort_state(
437 chunk,
438 expected_artifact_identity_sha256,
439 ¤t,
440 )? {
441 ManifestAbortState::Sealed {
442 original_manifest,
443 sink_artifact_intent_protocol,
444 open_sink_artifact_intents,
445 sink_cleanup_complete,
446 } => Ok(CheckpointManifestAbortSeal {
447 original_manifest,
448 sink_artifact_intent_protocol,
449 open_sink_artifact_intents,
450 sink_cleanup_complete,
451 }),
452 ManifestAbortState::Manifest(manifest, canonical) => {
453 let sink_artifact_intent_protocol = !manifest.sink_artifact_intents.is_empty();
454 let seal = CheckpointArtifactAbortSeal {
455 version: CHECKPOINT_ARTIFACT_ABORT_SEAL_VERSION,
456 artifact_identity_sha256: expected_artifact_identity_sha256.to_owned(),
457 chunk,
458 original_manifest: Some(manifest.clone()),
459 sink_artifact_intent_protocol,
460 open_sink_artifact_intents: Vec::new(),
461 sink_cleanup_complete: false,
462 };
463 self.replace_exact(
464 &path,
465 ¤t,
466 checkpoint_artifact_abort_seal_bytes(&seal)?,
467 )
468 .await?;
469 Ok(CheckpointManifestAbortSeal {
470 original_manifest: Some((manifest, canonical)),
471 sink_artifact_intent_protocol,
472 open_sink_artifact_intents: Vec::new(),
473 sink_cleanup_complete: false,
474 })
475 }
476 ManifestAbortState::Intent(record) => {
477 let open_sink_artifact_intents = record.sink_intents().to_vec();
478 let seal = CheckpointArtifactAbortSeal {
479 version: CHECKPOINT_ARTIFACT_ABORT_SEAL_VERSION,
480 artifact_identity_sha256: expected_artifact_identity_sha256.to_owned(),
481 chunk,
482 original_manifest: None,
483 sink_artifact_intent_protocol: true,
484 open_sink_artifact_intents: open_sink_artifact_intents.clone(),
485 sink_cleanup_complete: false,
486 };
487 self.replace_exact(
488 &path,
489 ¤t,
490 checkpoint_artifact_abort_seal_bytes(&seal)?,
491 )
492 .await?;
493 Ok(CheckpointManifestAbortSeal {
494 original_manifest: None,
495 sink_artifact_intent_protocol: true,
496 open_sink_artifact_intents,
497 sink_cleanup_complete: false,
498 })
499 }
500 }
501 }
502
503 async fn complete_aborted_sink_cleanup(
504 &self,
505 chunk: StateChunkId,
506 expected_artifact_identity_sha256: &str,
507 ) -> Result<CheckpointManifestAbortSeal, CheckpointStoreError> {
508 validate_abort_seal_request(chunk, expected_artifact_identity_sha256)?;
509 let path = self.manifest_path(chunk);
510 let current = self
511 .load_manifest_or_abort_seal_bytes(chunk)
512 .await?
513 .ok_or_else(|| {
514 CheckpointStoreError::Invalid(format!(
515 "participant {} checkpoint {} has no manifest abort seal",
516 chunk.participant_id, chunk.checkpoint_id
517 ))
518 })?;
519 let ManifestAbortState::Sealed {
520 original_manifest,
521 sink_artifact_intent_protocol,
522 open_sink_artifact_intents,
523 sink_cleanup_complete,
524 } = self.decode_manifest_abort_state(chunk, expected_artifact_identity_sha256, ¤t)?
525 else {
526 return Err(CheckpointStoreError::Invalid(format!(
527 "participant {} checkpoint {} manifest is not sealed before sink cleanup",
528 chunk.participant_id, chunk.checkpoint_id
529 )));
530 };
531 if sink_cleanup_complete {
532 return Ok(CheckpointManifestAbortSeal {
533 original_manifest,
534 sink_artifact_intent_protocol,
535 open_sink_artifact_intents,
536 sink_cleanup_complete,
537 });
538 }
539 let seal = CheckpointArtifactAbortSeal {
540 version: CHECKPOINT_ARTIFACT_ABORT_SEAL_VERSION,
541 artifact_identity_sha256: expected_artifact_identity_sha256.to_owned(),
542 chunk,
543 original_manifest: original_manifest
544 .as_ref()
545 .map(|(manifest, _)| manifest.clone()),
546 sink_artifact_intent_protocol,
547 open_sink_artifact_intents: open_sink_artifact_intents.clone(),
548 sink_cleanup_complete: true,
549 };
550 self.replace_exact(
551 &path,
552 ¤t,
553 checkpoint_artifact_abort_seal_bytes(&seal)?,
554 )
555 .await?;
556 Ok(CheckpointManifestAbortSeal {
557 original_manifest,
558 sink_artifact_intent_protocol,
559 open_sink_artifact_intents,
560 sink_cleanup_complete: true,
561 })
562 }
563
564 async fn seal_aborted_node_data(
565 &self,
566 chunk: StateChunkId,
567 expected_artifact_identity_sha256: &str,
568 ) -> Result<(), CheckpointStoreError> {
569 validate_abort_seal_request(chunk, expected_artifact_identity_sha256)?;
570 let manifest_bytes = self
571 .load_manifest_or_abort_seal_bytes(chunk)
572 .await?
573 .ok_or_else(|| {
574 CheckpointStoreError::Invalid(format!(
575 "participant {} checkpoint {} has no manifest abort seal",
576 chunk.participant_id, chunk.checkpoint_id
577 ))
578 })?;
579 match self.decode_manifest_abort_state(
580 chunk,
581 expected_artifact_identity_sha256,
582 &manifest_bytes,
583 )? {
584 ManifestAbortState::Sealed {
585 sink_cleanup_complete: true,
586 ..
587 } => {}
588 ManifestAbortState::Sealed { .. } => {
589 return Err(CheckpointStoreError::Invalid(format!(
590 "participant {} checkpoint {} sink cleanup is incomplete",
591 chunk.participant_id, chunk.checkpoint_id
592 )));
593 }
594 ManifestAbortState::Manifest(..) => {
595 return Err(CheckpointStoreError::Invalid(format!(
596 "participant {} checkpoint {} manifest is not sealed",
597 chunk.participant_id, chunk.checkpoint_id
598 )));
599 }
600 ManifestAbortState::Intent(..) => {
601 return Err(CheckpointStoreError::Invalid(format!(
602 "participant {} checkpoint {} sink intent is not sealed",
603 chunk.participant_id, chunk.checkpoint_id
604 )));
605 }
606 }
607 let seal = CheckpointArtifactAbortSeal {
608 version: CHECKPOINT_ARTIFACT_ABORT_SEAL_VERSION,
609 artifact_identity_sha256: expected_artifact_identity_sha256.to_owned(),
610 chunk,
611 original_manifest: None,
612 sink_artifact_intent_protocol: false,
613 open_sink_artifact_intents: Vec::new(),
614 sink_cleanup_complete: false,
615 };
616 let encoded = checkpoint_artifact_abort_seal_bytes(&seal)?;
617 let path = self.node_data_path(chunk);
618 if self
619 .create_immutable(&path, PutPayload::from_bytes(encoded.clone()))
620 .await?
621 || self.node_data_is_exact_abort_seal(chunk, &encoded).await?
622 {
623 return Ok(());
624 }
625 self.overwrite(&path, encoded).await
626 }
627
628 async fn load_manifest_for_participant(
629 &self,
630 participant_id: u64,
631 checkpoint_id: u64,
632 ) -> Result<Option<CheckpointManifest>, CheckpointStoreError> {
633 if participant_id == 0 || checkpoint_id == 0 {
634 return Err(CheckpointStoreError::Invalid(
635 "manifest identity must use nonzero participant and checkpoint ids".into(),
636 ));
637 }
638 self.load_bounded_manifest(StateChunkId {
639 participant_id,
640 checkpoint_id,
641 })
642 .await
643 }
644
645 async fn load_manifest_verified(
646 &self,
647 participant_id: u64,
648 checkpoint_id: u64,
649 expected_len: u64,
650 expected_sha256: &str,
651 ) -> Result<Option<CheckpointManifest>, CheckpointStoreError> {
652 if participant_id == 0 || checkpoint_id == 0 {
653 return Err(CheckpointStoreError::Invalid(
654 "manifest identity must use nonzero participant and checkpoint ids".into(),
655 ));
656 }
657 let chunk = StateChunkId {
658 participant_id,
659 checkpoint_id,
660 };
661 let Some(bytes) = self.load_bounded_manifest_bytes(chunk).await? else {
662 return Ok(None);
663 };
664 let actual_len = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
665 let actual_sha256 = sha256(&bytes);
666 if actual_len != expected_len || actual_sha256 != expected_sha256 {
667 return Err(CheckpointStoreError::Invalid(format!(
668 "participant {participant_id} checkpoint {checkpoint_id} manifest differs from the committed reference"
669 )));
670 }
671 self.decode_manifest(chunk, &bytes).map(Some)
672 }
673
674 async fn load_node_data_ranges(
675 &self,
676 chunk: StateChunkId,
677 expected_object_length: u64,
678 ranges: &[ByteRange],
679 ) -> Result<Option<Vec<Bytes>>, CheckpointStoreError> {
680 self.load_node_data_ranges_inner(chunk, expected_object_length, ranges)
681 .await
682 }
683
684 async fn delete_manifest(&self, chunk: StateChunkId) -> Result<(), CheckpointStoreError> {
685 self.delete_exact(&self.manifest_path(chunk)).await
686 }
687
688 async fn delete_node_data(&self, chunk: StateChunkId) -> Result<(), CheckpointStoreError> {
689 self.delete_exact(&self.node_data_path(chunk)).await
690 }
691
692 async fn save_subscription_segment(
693 &self,
694 segment: &OutputSegmentRef,
695 payload: Bytes,
696 ) -> Result<(), CheckpointStoreError> {
697 subscription_segments::save(self, segment, payload).await
698 }
699
700 async fn load_subscription_segment(
701 &self,
702 segment: &OutputSegmentRef,
703 ) -> Result<Option<Bytes>, CheckpointStoreError> {
704 subscription_segments::load(self, segment).await
705 }
706
707 async fn delete_subscription_segment(
708 &self,
709 object_key: &str,
710 ) -> Result<(), CheckpointStoreError> {
711 subscription_segments::delete(self, object_key).await
712 }
713
714 async fn delete_subscription_orphans(
715 &self,
716 reachable: &std::collections::BTreeSet<String>,
717 through_checkpoint_id: u64,
718 grace_before_ms: i64,
719 ) -> Result<SubscriptionOrphanCleanup, CheckpointStoreError> {
720 subscription_segments::delete_orphans(
721 self,
722 reachable,
723 through_checkpoint_id,
724 grace_before_ms,
725 )
726 .await
727 }
728}
729
730#[cfg(test)]
731mod tests;