use super::indexes::MetadataIndexes;
use loonfs_api::wire::manifest::lookup_keys;
use loonfs_api::{
ChangeSeq, CommitId, ContentRef, DisplayName, InodeId, InodeKind, NameKey, RevisionNo,
};
use serde::{Deserialize, Serialize};
use std::mem::{size_of, size_of_val};
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct MetadataState {
#[serde(default)]
pub(super) inodes: Vec<InodeRecord>,
#[serde(default)]
pub(super) direntry_binds: Vec<DirentryBindRecord>,
#[serde(default)]
pub(super) direntry_unbinds: Vec<DirentryUnbindRecord>,
#[serde(default)]
pub(super) revisions: Vec<RevisionRecord>,
#[serde(default)]
pub(super) subtree_tombstones: Vec<SubtreeTombstoneRecord>,
#[serde(default)]
pub(super) commit_receipts: Vec<CommitReceiptRecord>,
#[serde(skip)]
pub(super) row_count: usize,
#[serde(skip)]
pub(super) decoded_bytes: usize,
#[serde(skip)]
pub(super) indexes: MetadataIndexes,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
struct MetadataStateRows {
#[serde(default)]
inodes: Vec<InodeRecord>,
#[serde(default)]
direntry_binds: Vec<DirentryBindRecord>,
#[serde(default)]
direntry_unbinds: Vec<DirentryUnbindRecord>,
#[serde(default)]
revisions: Vec<RevisionRecord>,
#[serde(default)]
subtree_tombstones: Vec<SubtreeTombstoneRecord>,
#[serde(default)]
commit_receipts: Vec<CommitReceiptRecord>,
}
impl Default for MetadataState {
fn default() -> Self {
Self::from_rows(
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
)
}
}
impl<'de> Deserialize<'de> for MetadataState {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let rows = MetadataStateRows::deserialize(deserializer)?;
Ok(Self::from_rows(
rows.inodes,
rows.direntry_binds,
rows.direntry_unbinds,
rows.revisions,
rows.subtree_tombstones,
rows.commit_receipts,
))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InodeRecord {
pub inode_id: InodeId,
pub inode_kind: InodeKind,
pub created_seq: ChangeSeq,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DirentryBindRecord {
pub parent_inode_id: InodeId,
pub name_key: NameKey,
pub display_name: DisplayName,
pub child_inode_id: InodeId,
pub bind_seq: ChangeSeq,
pub bind_delta_index: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DirentryUnbindRecord {
pub parent_inode_id: InodeId,
pub name_key: NameKey,
pub display_name: DisplayName,
pub child_inode_id: InodeId,
pub bind_seq: ChangeSeq,
pub bind_delta_index: u32,
pub unbind_seq: ChangeSeq,
pub unbind_delta_index: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RevisionRecord {
pub inode_id: InodeId,
pub revision_no: RevisionNo,
pub committed_seq: ChangeSeq,
pub committed_at_ms: u64,
pub revision_delta_index: u32,
pub content_ref: ContentRef,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SubtreeTombstoneRecord {
pub root_inode_id: InodeId,
pub tombstone_seq: ChangeSeq,
pub tombstone_delta_index: u32,
pub deleted_at_ms: u64,
pub parent_inode_id: Option<InodeId>,
pub name_key: Option<NameKey>,
pub display_name: Option<DisplayName>,
pub action: SubtreeTombstoneAction,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SubtreeTombstoneAction {
Set,
Revoke {
target_seq: ChangeSeq,
target_delta_index: u32,
},
}
pub(crate) fn active_tombstone_from_records(
records: impl IntoIterator<Item = SubtreeTombstoneRecord>,
visible_seq: ChangeSeq,
) -> Option<SubtreeTombstoneRecord> {
records
.into_iter()
.filter(|tombstone| tombstone.tombstone_seq <= visible_seq)
.max_by_key(|tombstone| (tombstone.tombstone_seq, tombstone.tombstone_delta_index))
.filter(|tombstone| matches!(tombstone.action, SubtreeTombstoneAction::Set))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ActiveDeletionRecord {
pub(crate) root_inode_id: InodeId,
pub(crate) deleted_at_seq: ChangeSeq,
pub(crate) action: ActiveDeletionAction,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ActiveDeletionAction {
Listed {
deleted_at_ms: u64,
parent_inode_id: Option<InodeId>,
name_key: Option<NameKey>,
display_name: Option<DisplayName>,
},
Removed { revoked_at_seq: ChangeSeq },
}
pub(crate) fn active_deletion_from_tombstone(
tombstone: &SubtreeTombstoneRecord,
) -> ActiveDeletionRecord {
match &tombstone.action {
SubtreeTombstoneAction::Set => ActiveDeletionRecord {
root_inode_id: tombstone.root_inode_id,
deleted_at_seq: tombstone.tombstone_seq,
action: ActiveDeletionAction::Listed {
deleted_at_ms: tombstone.deleted_at_ms,
parent_inode_id: tombstone.parent_inode_id,
name_key: tombstone.name_key.clone(),
display_name: tombstone.display_name.clone(),
},
},
SubtreeTombstoneAction::Revoke { target_seq, .. } => ActiveDeletionRecord {
root_inode_id: tombstone.root_inode_id,
deleted_at_seq: *target_seq,
action: ActiveDeletionAction::Removed {
revoked_at_seq: tombstone.tombstone_seq,
},
},
}
}
impl ActiveDeletionRecord {
pub(crate) fn row_key(&self) -> String {
lookup_keys::active_deletion_row_key(
self.deleted_at_seq,
self.root_inode_id,
match &self.action {
ActiveDeletionAction::Listed { .. } => lookup_keys::ACTIVE_DELETION_RANK_LISTED,
ActiveDeletionAction::Removed { .. } => lookup_keys::ACTIVE_DELETION_RANK_REMOVED,
},
)
}
pub(crate) fn into_recoverable(self) -> Option<RecoverableDeletion> {
match self.action {
ActiveDeletionAction::Listed {
deleted_at_ms,
parent_inode_id,
name_key,
display_name,
} => Some(RecoverableDeletion {
root_inode_id: self.root_inode_id,
deleted_at_seq: self.deleted_at_seq,
deleted_at_ms,
parent_inode_id,
name_key,
display_name,
}),
ActiveDeletionAction::Removed { .. } => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct RecoverableDeletion {
pub(crate) root_inode_id: InodeId,
pub(crate) deleted_at_seq: ChangeSeq,
pub(crate) deleted_at_ms: u64,
pub(crate) parent_inode_id: Option<InodeId>,
pub(crate) name_key: Option<NameKey>,
pub(crate) display_name: Option<DisplayName>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CommitReceiptRecord {
pub commit_id: CommitId,
pub semantic_commit_fingerprint: String,
pub committed_seq: ChangeSeq,
pub committed_at_ms: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
impl MetadataState {
pub(crate) fn from_rows(
inodes: Vec<InodeRecord>,
direntry_binds: Vec<DirentryBindRecord>,
direntry_unbinds: Vec<DirentryUnbindRecord>,
revisions: Vec<RevisionRecord>,
subtree_tombstones: Vec<SubtreeTombstoneRecord>,
commit_receipts: Vec<CommitReceiptRecord>,
) -> Self {
let mut state = Self {
inodes,
direntry_binds,
direntry_unbinds,
revisions,
subtree_tombstones,
commit_receipts,
row_count: 0,
decoded_bytes: 0,
indexes: MetadataIndexes::default(),
};
state.rebuild_indexes();
state
}
pub fn indexed_seq(&self) -> ChangeSeq {
self.indexes.indexed_seq()
}
pub fn inodes(&self) -> &[InodeRecord] {
&self.inodes
}
pub fn direntry_binds(&self) -> &[DirentryBindRecord] {
&self.direntry_binds
}
pub fn direntry_unbinds(&self) -> &[DirentryUnbindRecord] {
&self.direntry_unbinds
}
pub fn revisions(&self) -> &[RevisionRecord] {
&self.revisions
}
pub fn subtree_tombstones(&self) -> &[SubtreeTombstoneRecord] {
&self.subtree_tombstones
}
pub fn commit_receipts(&self) -> &[CommitReceiptRecord] {
&self.commit_receipts
}
pub fn row_count(&self) -> usize {
self.row_count
}
pub fn decoded_bytes(&self) -> usize {
self.decoded_bytes
}
pub fn find_commit_receipt(&self, commit_id: &CommitId) -> Option<&CommitReceiptRecord> {
self.indexes.commit_receipt(commit_id)
}
fn rebuild_indexes(&mut self) {
self.row_count = metadata_row_count(
&self.inodes,
&self.direntry_binds,
&self.direntry_unbinds,
&self.revisions,
&self.subtree_tombstones,
&self.commit_receipts,
);
self.decoded_bytes = metadata_decoded_bytes(
&self.inodes,
&self.direntry_binds,
&self.direntry_unbinds,
&self.revisions,
&self.subtree_tombstones,
&self.commit_receipts,
);
self.indexes = MetadataIndexes::rebuild(
&self.inodes,
&self.direntry_binds,
&self.direntry_unbinds,
&self.revisions,
&self.subtree_tombstones,
&self.commit_receipts,
);
}
pub(crate) fn push_inode_record(&mut self, record: InodeRecord) {
self.indexes.record_inode(&record);
self.record_row_weight(size_of::<InodeRecord>());
self.inodes.push(record);
}
pub(crate) fn push_direntry_bind_record(&mut self, record: DirentryBindRecord) {
self.indexes.record_bind(&record);
self.record_row_weight(direntry_bind_decoded_bytes(&record));
self.direntry_binds.push(record);
}
pub(crate) fn push_direntry_unbind_record(&mut self, record: DirentryUnbindRecord) {
self.indexes.record_unbind(&record);
self.record_row_weight(direntry_unbind_decoded_bytes(&record));
self.direntry_unbinds.push(record);
}
pub(crate) fn push_revision_record(&mut self, record: RevisionRecord) {
self.indexes.record_revision(&record);
self.record_row_weight(revision_decoded_bytes(&record));
self.revisions.push(record);
}
pub(crate) fn push_subtree_tombstone_record(&mut self, record: SubtreeTombstoneRecord) {
self.indexes.record_tombstone(&record);
self.record_row_weight(size_of::<SubtreeTombstoneRecord>());
self.subtree_tombstones.push(record);
}
pub(crate) fn push_commit_receipt_record(&mut self, record: CommitReceiptRecord) {
self.indexes.record_commit_receipt(&record);
self.record_row_weight(commit_receipt_decoded_bytes(&record));
self.commit_receipts.push(record);
}
fn record_row_weight(&mut self, decoded_bytes: usize) {
self.row_count = self.row_count.saturating_add(1);
self.decoded_bytes = self.decoded_bytes.saturating_add(decoded_bytes);
}
}
#[cfg(test)]
#[derive(Debug, Default)]
pub(crate) struct MetadataStateBuilder {
state: MetadataState,
}
#[cfg(test)]
impl MetadataStateBuilder {
pub(crate) fn push_inode(&mut self, record: InodeRecord) {
self.state.push_inode_record(record);
}
pub(crate) fn push_direntry_bind(&mut self, record: DirentryBindRecord) {
self.state.push_direntry_bind_record(record);
}
pub(crate) fn push_direntry_unbind(&mut self, record: DirentryUnbindRecord) {
self.state.push_direntry_unbind_record(record);
}
pub(crate) fn push_revision(&mut self, record: RevisionRecord) {
self.state.push_revision_record(record);
}
pub(crate) fn push_subtree_tombstone(&mut self, record: SubtreeTombstoneRecord) {
self.state.push_subtree_tombstone_record(record);
}
pub(crate) fn push_commit_receipt(&mut self, record: CommitReceiptRecord) {
self.state.push_commit_receipt_record(record);
}
pub(crate) fn finish(mut self) -> MetadataState {
self.state.rebuild_indexes();
self.state
}
}
fn metadata_row_count(
inodes: &[InodeRecord],
direntry_binds: &[DirentryBindRecord],
direntry_unbinds: &[DirentryUnbindRecord],
revisions: &[RevisionRecord],
subtree_tombstones: &[SubtreeTombstoneRecord],
commit_receipts: &[CommitReceiptRecord],
) -> usize {
inodes
.len()
.saturating_add(direntry_binds.len())
.saturating_add(direntry_unbinds.len())
.saturating_add(revisions.len())
.saturating_add(subtree_tombstones.len())
.saturating_add(commit_receipts.len())
}
fn metadata_decoded_bytes(
inodes: &[InodeRecord],
direntry_binds: &[DirentryBindRecord],
direntry_unbinds: &[DirentryUnbindRecord],
revisions: &[RevisionRecord],
subtree_tombstones: &[SubtreeTombstoneRecord],
commit_receipts: &[CommitReceiptRecord],
) -> usize {
size_of_val(inodes)
.saturating_add(
direntry_binds
.iter()
.map(direntry_bind_decoded_bytes)
.sum::<usize>(),
)
.saturating_add(
direntry_unbinds
.iter()
.map(direntry_unbind_decoded_bytes)
.sum::<usize>(),
)
.saturating_add(revisions.iter().map(revision_decoded_bytes).sum::<usize>())
.saturating_add(size_of_val(subtree_tombstones))
.saturating_add(
commit_receipts
.iter()
.map(commit_receipt_decoded_bytes)
.sum::<usize>(),
)
}
fn direntry_bind_decoded_bytes(record: &DirentryBindRecord) -> usize {
size_of::<DirentryBindRecord>()
+ record.name_key.as_str().len()
+ record.display_name.as_str().len()
}
fn direntry_unbind_decoded_bytes(record: &DirentryUnbindRecord) -> usize {
size_of::<DirentryUnbindRecord>() + record.name_key.as_str().len()
}
fn revision_decoded_bytes(record: &RevisionRecord) -> usize {
size_of::<RevisionRecord>() + content_ref_decoded_bytes(&record.content_ref)
}
fn commit_receipt_decoded_bytes(record: &CommitReceiptRecord) -> usize {
size_of::<CommitReceiptRecord>()
+ record.commit_id.as_str().len()
+ record.semantic_commit_fingerprint.len()
+ record.message.as_ref().map_or(0, String::len)
}
fn content_ref_decoded_bytes(content_ref: &ContentRef) -> usize {
size_of::<ContentRef>() + content_ref_evidence_bytes(content_ref)
}
pub(crate) fn content_ref_evidence_bytes(content_ref: &ContentRef) -> usize {
content_ref.content_id.as_str().len()
+ content_ref.storage_checksum.value.len()
+ content_ref
.whole_file_sha256
.as_ref()
.map_or(0, String::len)
}