1use crate::checkpoint::record::encode_checkpoint_record;
9use crate::context::MutationContext;
10use crate::error::{CoreError, Result};
11use loonfs_api::wire::control::{
12 decode_control_object, CheckpointOwner, CheckpointRecordLifecycle, CheckpointRecordState,
13 ControlObjectKind,
14};
15use loonfs_api::{GeneratedIdValidationError, ManifestObjectId, NamespaceId, RetainedReason};
16use loonfs_objectstore::{ObjectStore, ObjectStoreError};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub(super) enum CheckpointSweep {
20 Delete,
23 Released,
25 Retain,
26}
27
28pub(super) fn lease_expired(record: &CheckpointRecordState, now_ms: u64) -> bool {
32 record
33 .expires_at_ms
34 .is_some_and(|expires_at_ms| expires_at_ms <= now_ms)
35}
36
37pub(super) async fn sweep_checkpoint_record<S: ObjectStore + ?Sized>(
49 store: &S,
50 namespace_id: &NamespaceId,
51 key: &str,
52 grace_window_ms: u64,
53 namespace_deleted: bool,
54 context: &MutationContext,
55) -> Result<CheckpointSweep> {
56 let Some(body) = store
57 .get_with_metadata(key)
58 .await
59 .map_err(|error| CoreError::store(key, &error))?
60 else {
61 return Ok(CheckpointSweep::Retain);
62 };
63 let Ok(envelope) = decode_control_object::<CheckpointRecordState>(
64 &body.bytes,
65 ControlObjectKind::CheckpointRecord,
66 ) else {
67 return Ok(CheckpointSweep::Retain);
68 };
69 let record = envelope.state;
70 if record.namespace_id != *namespace_id {
71 return Ok(CheckpointSweep::Retain);
72 }
73 if let CheckpointRecordLifecycle::Released { released_at_ms } = record.state {
74 let aged = context.now_ms.saturating_sub(released_at_ms) >= grace_window_ms;
75 return Ok(if aged {
76 CheckpointSweep::Delete
77 } else {
78 CheckpointSweep::Retain
79 });
80 }
81 if matches!(record.owner, CheckpointOwner::Fork { .. }) {
85 return Ok(CheckpointSweep::Retain);
86 }
87 let releasable = lease_expired(&record, context.now_ms)
92 || (namespace_deleted
93 && context.now_ms.saturating_sub(record.created_at_ms) >= grace_window_ms);
94 if !releasable {
95 return Ok(CheckpointSweep::Retain);
96 }
97 let Some(etag) = body.metadata.etag.as_deref() else {
98 return Ok(CheckpointSweep::Retain);
99 };
100 let mut released = record;
101 released.state = CheckpointRecordLifecycle::Released {
102 released_at_ms: context.now_ms,
103 };
104 let bytes = encode_checkpoint_record(&released)?;
105 match store.compare_and_swap(key, etag, bytes).await {
106 Ok(_) => Ok(CheckpointSweep::Released),
107 Err(ObjectStoreError::PreconditionFailed { .. }) => {
108 tracing::debug!(
109 namespace_id = %namespace_id,
110 object_key = key,
111 "checkpoint release lost its inspected etag; retaining"
112 );
113 Ok(CheckpointSweep::Retain)
114 }
115 Err(error) => Err(CoreError::store(key, &error)),
116 }
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum AgedSweep {
122 Deleted,
124 AlreadyGone,
126 RetainedInGraceWindow,
128 RetainedWithoutTimestamp,
131}
132
133impl AgedSweep {
134 pub fn deleted(self) -> bool {
136 self == Self::Deleted
137 }
138
139 pub fn retained_reason(self) -> Option<RetainedReason> {
143 match self {
144 Self::Deleted | Self::AlreadyGone => None,
145 Self::RetainedInGraceWindow => Some(RetainedReason::GraceWindow),
146 Self::RetainedWithoutTimestamp => Some(RetainedReason::NoProviderTimestamp),
147 }
148 }
149}
150
151pub async fn delete_if_aged<S: ObjectStore + ?Sized>(
159 store: &S,
160 key: &str,
161 grace_window_ms: u64,
162 now_ms: u64,
163) -> std::result::Result<AgedSweep, ObjectStoreError> {
164 let Some(metadata) = store.head(key).await? else {
165 return Ok(AgedSweep::AlreadyGone);
166 };
167 let Some(last_modified_ms) = metadata.last_modified_ms else {
168 return Ok(AgedSweep::RetainedWithoutTimestamp);
169 };
170 if now_ms.saturating_sub(last_modified_ms) < grace_window_ms {
171 return Ok(AgedSweep::RetainedInGraceWindow);
172 }
173 store.delete(key).await?;
174 Ok(AgedSweep::Deleted)
175}
176
177pub(super) fn manifest_object_id_of(
178 key: &str,
179) -> Option<std::result::Result<ManifestObjectId, GeneratedIdValidationError>> {
180 let name = key.rsplit('/').next()?;
181 let object_id = name.strip_suffix(".manifest.json")?;
182 Some(ManifestObjectId::parse(object_id))
183}