use super::cache::MetadataTableCache;
use super::error::ManifestLoadError;
use super::load::load_verified_manifest_tables_with_cache;
use super::record::read_checkpoint_record;
use crate::error::{CoreError, MetadataProjectionLoadError, Result};
use crate::metadata::MetadataView;
use loonfs_api::wire::control::{CheckpointRecordLifecycle, CheckpointRecordState};
use loonfs_api::wire::manifest::{lookup_keys, MetadataRow, MetadataTableFamily};
use loonfs_api::wire::sst_blocks::string_prefix_upper_bound;
use loonfs_api::{
ChangeSeq, CheckpointId, ContentRef, InodeId, InodeKind, NamespaceId, PageRequest, RevisionNo,
};
use loonfs_objectstore::ObjectStore;
const INODE_SCAN_WAVE_ROWS: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CheckpointFilesPageCursor {
pub after_inode_id: InodeId,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CheckpointFile {
pub inode_id: InodeId,
pub revision_no: RevisionNo,
pub content_ref: ContentRef,
pub size_bytes: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CheckpointFilesPage {
pub checkpoint_seq: ChangeSeq,
pub files: Vec<CheckpointFile>,
pub next_cursor: Option<CheckpointFilesPageCursor>,
}
pub(crate) async fn list_checkpoint_files_page<S: ObjectStore + ?Sized>(
store: &S,
table_cache: Option<&MetadataTableCache>,
namespace_id: &NamespaceId,
checkpoint_id: &CheckpointId,
request: PageRequest<CheckpointFilesPageCursor>,
) -> Result<CheckpointFilesPage> {
let record = read_pinning_checkpoint_record(store, namespace_id, checkpoint_id).await?;
let tables = load_verified_manifest_tables_with_cache(
store,
table_cache,
namespace_id,
&record.manifest_object_id,
)
.await
.map_err(|error| match error {
ManifestLoadError::MissingManifest { object_key } => CoreError::CheckpointUnavailable(
format!("checkpoint `{checkpoint_id}` pins manifest `{object_key}`, which is gone"),
),
other => CoreError::MetadataProjection(MetadataProjectionLoadError::ManifestLoad(other)),
})?;
let manifest = tables.manifest();
if manifest.payload_checksum != record.manifest_payload_checksum
|| manifest.payload.head_seq != record.manifest_head_seq
{
return Err(CoreError::NamespaceCorrupt(format!(
"checkpoint `{checkpoint_id}` basis does not match its manifest"
)));
}
let checkpoint_seq = record.manifest_head_seq;
let view = MetadataView::over_manifest_tables(&tables, checkpoint_seq);
let mut session = view.session();
let wanted = request.limit.limit_plus_one();
let wave_rows = wanted.max(INODE_SCAN_WAVE_ROWS);
let mut lower_bound = match request.cursor {
Some(cursor) => lookup_keys::inode_key_after(cursor.after_inode_id),
None => lookup_keys::INODE_ROW_PREFIX.to_owned(),
};
let upper_bound = string_prefix_upper_bound(lookup_keys::INODE_ROW_PREFIX);
let mut files = Vec::with_capacity(wanted);
while files.len() < wanted {
let rows = tables
.scan_range_page_with_keys(
MetadataTableFamily::Inodes,
&lower_bound,
upper_bound.as_deref(),
wave_rows,
)
.await
.map_err(|error| {
CoreError::MetadataProjection(MetadataProjectionLoadError::ManifestLoad(error))
})?;
let family_exhausted = rows.len() < wave_rows;
match rows.last() {
Some((row_key, _)) => lower_bound = format!("{row_key}\0"),
None => break,
}
for (row_key, row) in rows {
let MetadataRow::Inode {
inode_id,
inode_kind,
..
} = row
else {
return Err(CoreError::NamespaceCorrupt(format!(
"inodes family returned a non-inode row at `{row_key}`"
)));
};
if inode_kind != InodeKind::File {
continue;
}
if session.visible_inode(inode_id).await?.is_none() {
continue;
}
let Some(revision) = session.latest_revision_head_of_visible(inode_id).await? else {
continue;
};
files.push(CheckpointFile {
inode_id,
revision_no: revision.revision_no,
size_bytes: revision.content_ref.size_bytes,
content_ref: revision.content_ref,
});
if files.len() == wanted {
break;
}
}
if family_exhausted {
break;
}
}
let has_more = files.len() > request.limit.as_usize();
if has_more {
files.truncate(request.limit.as_usize());
}
let next_cursor = has_more.then(|| CheckpointFilesPageCursor {
after_inode_id: files
.last()
.expect("a non-zero page limit with more files must return a file")
.inode_id,
});
Ok(CheckpointFilesPage {
checkpoint_seq,
files,
next_cursor,
})
}
async fn read_pinning_checkpoint_record<S: ObjectStore + ?Sized>(
store: &S,
namespace_id: &NamespaceId,
checkpoint_id: &CheckpointId,
) -> Result<CheckpointRecordState> {
let Some(record) = read_checkpoint_record(store, namespace_id, checkpoint_id)
.await?
.map(|loaded| loaded.state)
else {
return Err(CoreError::CheckpointUnavailable(format!(
"checkpoint `{checkpoint_id}` does not exist in namespace `{namespace_id}`"
)));
};
if record.state != (CheckpointRecordLifecycle::Active {}) {
return Err(CoreError::CheckpointUnavailable(format!(
"checkpoint `{checkpoint_id}` is `{}` and no longer pins its basis",
record.state
)));
}
Ok(record)
}