loonfs_core/checkpoint/
files.rs1use super::cache::MetadataTableCache;
11use super::error::ManifestLoadError;
12use super::load::load_verified_manifest_tables_with_cache;
13use super::record::read_checkpoint_record;
14use crate::error::{CoreError, MetadataProjectionLoadError, Result};
15use crate::metadata::MetadataView;
16use loonfs_api::wire::control::{CheckpointRecordLifecycle, CheckpointRecordState};
17use loonfs_api::wire::manifest::{lookup_keys, MetadataRow, MetadataTableFamily};
18use loonfs_api::wire::sst_blocks::string_prefix_upper_bound;
19use loonfs_api::{
20 ChangeSeq, CheckpointId, ContentRef, InodeId, InodeKind, NamespaceId, PageRequest, RevisionNo,
21};
22use loonfs_objectstore::ObjectStore;
23
24const INODE_SCAN_WAVE_ROWS: usize = 64;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct CheckpointFilesPageCursor {
36 pub after_inode_id: InodeId,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct CheckpointFile {
44 pub inode_id: InodeId,
46 pub revision_no: RevisionNo,
48 pub content_ref: ContentRef,
50 pub size_bytes: u64,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct CheckpointFilesPage {
58 pub checkpoint_seq: ChangeSeq,
62 pub files: Vec<CheckpointFile>,
64 pub next_cursor: Option<CheckpointFilesPageCursor>,
66}
67
68pub(crate) async fn list_checkpoint_files_page<S: ObjectStore + ?Sized>(
88 store: &S,
89 table_cache: Option<&MetadataTableCache>,
90 namespace_id: &NamespaceId,
91 checkpoint_id: &CheckpointId,
92 request: PageRequest<CheckpointFilesPageCursor>,
93) -> Result<CheckpointFilesPage> {
94 let record = read_pinning_checkpoint_record(store, namespace_id, checkpoint_id).await?;
95 let tables = load_verified_manifest_tables_with_cache(
96 store,
97 table_cache,
98 namespace_id,
99 &record.manifest_object_id,
100 )
101 .await
102 .map_err(|error| match error {
103 ManifestLoadError::MissingManifest { object_key } => CoreError::CheckpointUnavailable(
104 format!("checkpoint `{checkpoint_id}` pins manifest `{object_key}`, which is gone"),
105 ),
106 other => CoreError::MetadataProjection(MetadataProjectionLoadError::ManifestLoad(other)),
107 })?;
108 let manifest = tables.manifest();
109 if manifest.payload_checksum != record.manifest_payload_checksum
110 || manifest.payload.head_seq != record.manifest_head_seq
111 {
112 return Err(CoreError::NamespaceCorrupt(format!(
113 "checkpoint `{checkpoint_id}` basis does not match its manifest"
114 )));
115 }
116
117 let checkpoint_seq = record.manifest_head_seq;
118 let view = MetadataView::over_manifest_tables(&tables, checkpoint_seq);
119 let mut session = view.session();
120
121 let wanted = request.limit.limit_plus_one();
124 let wave_rows = wanted.max(INODE_SCAN_WAVE_ROWS);
125 let mut lower_bound = match request.cursor {
126 Some(cursor) => lookup_keys::inode_key_after(cursor.after_inode_id),
127 None => lookup_keys::INODE_ROW_PREFIX.to_owned(),
128 };
129 let upper_bound = string_prefix_upper_bound(lookup_keys::INODE_ROW_PREFIX);
130 let mut files = Vec::with_capacity(wanted);
131 while files.len() < wanted {
132 let rows = tables
133 .scan_range_page_with_keys(
134 MetadataTableFamily::Inodes,
135 &lower_bound,
136 upper_bound.as_deref(),
137 wave_rows,
138 )
139 .await
140 .map_err(|error| {
141 CoreError::MetadataProjection(MetadataProjectionLoadError::ManifestLoad(error))
142 })?;
143 let family_exhausted = rows.len() < wave_rows;
144 match rows.last() {
147 Some((row_key, _)) => lower_bound = format!("{row_key}\0"),
148 None => break,
149 }
150 for (row_key, row) in rows {
151 let MetadataRow::Inode {
152 inode_id,
153 inode_kind,
154 ..
155 } = row
156 else {
157 return Err(CoreError::NamespaceCorrupt(format!(
158 "inodes family returned a non-inode row at `{row_key}`"
159 )));
160 };
161 if inode_kind != InodeKind::File {
162 continue;
163 }
164 if session.visible_inode(inode_id).await?.is_none() {
165 continue;
166 }
167 let Some(revision) = session.latest_revision_head_of_visible(inode_id).await? else {
168 continue;
169 };
170 files.push(CheckpointFile {
171 inode_id,
172 revision_no: revision.revision_no,
173 size_bytes: revision.content_ref.size_bytes,
174 content_ref: revision.content_ref,
175 });
176 if files.len() == wanted {
177 break;
178 }
179 }
180 if family_exhausted {
181 break;
182 }
183 }
184
185 let has_more = files.len() > request.limit.as_usize();
186 if has_more {
187 files.truncate(request.limit.as_usize());
188 }
189 let next_cursor = has_more.then(|| CheckpointFilesPageCursor {
190 after_inode_id: files
191 .last()
192 .expect("a non-zero page limit with more files must return a file")
193 .inode_id,
194 });
195 Ok(CheckpointFilesPage {
196 checkpoint_seq,
197 files,
198 next_cursor,
199 })
200}
201
202async fn read_pinning_checkpoint_record<S: ObjectStore + ?Sized>(
206 store: &S,
207 namespace_id: &NamespaceId,
208 checkpoint_id: &CheckpointId,
209) -> Result<CheckpointRecordState> {
210 let Some(record) = read_checkpoint_record(store, namespace_id, checkpoint_id)
211 .await?
212 .map(|loaded| loaded.state)
213 else {
214 return Err(CoreError::CheckpointUnavailable(format!(
215 "checkpoint `{checkpoint_id}` does not exist in namespace `{namespace_id}`"
216 )));
217 };
218 if record.state != (CheckpointRecordLifecycle::Active {}) {
219 return Err(CoreError::CheckpointUnavailable(format!(
220 "checkpoint `{checkpoint_id}` is `{}` and no longer pins its basis",
221 record.state
222 )));
223 }
224 Ok(record)
225}