1use super::indexes::MetadataIndexes;
6use loonfs_api::wire::manifest::lookup_keys;
7use loonfs_api::{
8 ChangeSeq, CommitId, ContentRef, DisplayName, InodeId, InodeKind, NameKey, RevisionNo,
9};
10use serde::{Deserialize, Serialize};
11use std::mem::{size_of, size_of_val};
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
14pub struct MetadataState {
15 #[serde(default)]
16 pub(super) inodes: Vec<InodeRecord>,
17 #[serde(default)]
18 pub(super) direntry_binds: Vec<DirentryBindRecord>,
19 #[serde(default)]
20 pub(super) direntry_unbinds: Vec<DirentryUnbindRecord>,
21 #[serde(default)]
22 pub(super) revisions: Vec<RevisionRecord>,
23 #[serde(default)]
24 pub(super) subtree_tombstones: Vec<SubtreeTombstoneRecord>,
25 #[serde(default)]
26 pub(super) commit_receipts: Vec<CommitReceiptRecord>,
27 #[serde(skip)]
28 pub(super) row_count: usize,
29 #[serde(skip)]
30 pub(super) decoded_bytes: usize,
31 #[serde(skip)]
32 pub(super) indexes: MetadataIndexes,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
36struct MetadataStateRows {
37 #[serde(default)]
38 inodes: Vec<InodeRecord>,
39 #[serde(default)]
40 direntry_binds: Vec<DirentryBindRecord>,
41 #[serde(default)]
42 direntry_unbinds: Vec<DirentryUnbindRecord>,
43 #[serde(default)]
44 revisions: Vec<RevisionRecord>,
45 #[serde(default)]
46 subtree_tombstones: Vec<SubtreeTombstoneRecord>,
47 #[serde(default)]
48 commit_receipts: Vec<CommitReceiptRecord>,
49}
50
51impl Default for MetadataState {
52 fn default() -> Self {
53 Self::from_rows(
54 Vec::new(),
55 Vec::new(),
56 Vec::new(),
57 Vec::new(),
58 Vec::new(),
59 Vec::new(),
60 )
61 }
62}
63
64impl<'de> Deserialize<'de> for MetadataState {
65 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
66 where
67 D: serde::Deserializer<'de>,
68 {
69 let rows = MetadataStateRows::deserialize(deserializer)?;
70 Ok(Self::from_rows(
71 rows.inodes,
72 rows.direntry_binds,
73 rows.direntry_unbinds,
74 rows.revisions,
75 rows.subtree_tombstones,
76 rows.commit_receipts,
77 ))
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82pub struct InodeRecord {
83 pub inode_id: InodeId,
84 pub inode_kind: InodeKind,
85 pub created_seq: ChangeSeq,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89pub struct DirentryBindRecord {
90 pub parent_inode_id: InodeId,
91 pub name_key: NameKey,
92 pub display_name: DisplayName,
93 pub child_inode_id: InodeId,
94 pub bind_seq: ChangeSeq,
95 pub bind_delta_index: u32,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99pub struct DirentryUnbindRecord {
100 pub parent_inode_id: InodeId,
101 pub name_key: NameKey,
102 pub display_name: DisplayName,
105 pub child_inode_id: InodeId,
106 pub bind_seq: ChangeSeq,
107 pub bind_delta_index: u32,
108 pub unbind_seq: ChangeSeq,
109 pub unbind_delta_index: u32,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113pub struct RevisionRecord {
114 pub inode_id: InodeId,
115 pub revision_no: RevisionNo,
116 pub committed_seq: ChangeSeq,
117 pub committed_at_ms: u64,
120 pub revision_delta_index: u32,
121 pub content_ref: ContentRef,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125pub struct SubtreeTombstoneRecord {
126 pub root_inode_id: InodeId,
127 pub tombstone_seq: ChangeSeq,
130 pub tombstone_delta_index: u32,
131 pub deleted_at_ms: u64,
133 pub parent_inode_id: Option<InodeId>,
137 pub name_key: Option<NameKey>,
138 pub display_name: Option<DisplayName>,
139 pub action: SubtreeTombstoneAction,
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149pub enum SubtreeTombstoneAction {
150 Set,
152 Revoke {
156 target_seq: ChangeSeq,
157 target_delta_index: u32,
158 },
159}
160
161pub(crate) fn active_tombstone_from_records(
174 records: impl IntoIterator<Item = SubtreeTombstoneRecord>,
175 visible_seq: ChangeSeq,
176) -> Option<SubtreeTombstoneRecord> {
177 records
178 .into_iter()
179 .filter(|tombstone| tombstone.tombstone_seq <= visible_seq)
180 .max_by_key(|tombstone| (tombstone.tombstone_seq, tombstone.tombstone_delta_index))
181 .filter(|tombstone| matches!(tombstone.action, SubtreeTombstoneAction::Set))
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
191pub(crate) struct ActiveDeletionRecord {
192 pub(crate) root_inode_id: InodeId,
193 pub(crate) deleted_at_seq: ChangeSeq,
196 pub(crate) action: ActiveDeletionAction,
197}
198
199#[derive(Debug, Clone, PartialEq, Eq)]
201pub(crate) enum ActiveDeletionAction {
202 Listed {
205 deleted_at_ms: u64,
206 parent_inode_id: Option<InodeId>,
207 name_key: Option<NameKey>,
208 display_name: Option<DisplayName>,
209 },
210 Removed { revoked_at_seq: ChangeSeq },
212}
213
214pub(crate) fn active_deletion_from_tombstone(
225 tombstone: &SubtreeTombstoneRecord,
226) -> ActiveDeletionRecord {
227 match &tombstone.action {
228 SubtreeTombstoneAction::Set => ActiveDeletionRecord {
229 root_inode_id: tombstone.root_inode_id,
230 deleted_at_seq: tombstone.tombstone_seq,
231 action: ActiveDeletionAction::Listed {
232 deleted_at_ms: tombstone.deleted_at_ms,
233 parent_inode_id: tombstone.parent_inode_id,
234 name_key: tombstone.name_key.clone(),
235 display_name: tombstone.display_name.clone(),
236 },
237 },
238 SubtreeTombstoneAction::Revoke { target_seq, .. } => ActiveDeletionRecord {
239 root_inode_id: tombstone.root_inode_id,
240 deleted_at_seq: *target_seq,
241 action: ActiveDeletionAction::Removed {
242 revoked_at_seq: tombstone.tombstone_seq,
243 },
244 },
245 }
246}
247
248impl ActiveDeletionRecord {
249 pub(crate) fn row_key(&self) -> String {
252 lookup_keys::active_deletion_row_key(
253 self.deleted_at_seq,
254 self.root_inode_id,
255 match &self.action {
256 ActiveDeletionAction::Listed { .. } => lookup_keys::ACTIVE_DELETION_RANK_LISTED,
257 ActiveDeletionAction::Removed { .. } => lookup_keys::ACTIVE_DELETION_RANK_REMOVED,
258 },
259 )
260 }
261
262 pub(crate) fn into_recoverable(self) -> Option<RecoverableDeletion> {
265 match self.action {
266 ActiveDeletionAction::Listed {
267 deleted_at_ms,
268 parent_inode_id,
269 name_key,
270 display_name,
271 } => Some(RecoverableDeletion {
272 root_inode_id: self.root_inode_id,
273 deleted_at_seq: self.deleted_at_seq,
274 deleted_at_ms,
275 parent_inode_id,
276 name_key,
277 display_name,
278 }),
279 ActiveDeletionAction::Removed { .. } => None,
280 }
281 }
282}
283
284#[derive(Debug, Clone, PartialEq, Eq)]
286pub(crate) struct RecoverableDeletion {
287 pub(crate) root_inode_id: InodeId,
288 pub(crate) deleted_at_seq: ChangeSeq,
289 pub(crate) deleted_at_ms: u64,
290 pub(crate) parent_inode_id: Option<InodeId>,
291 pub(crate) name_key: Option<NameKey>,
292 pub(crate) display_name: Option<DisplayName>,
293}
294
295#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
296pub struct CommitReceiptRecord {
297 pub commit_id: CommitId,
298 pub semantic_commit_fingerprint: String,
299 pub committed_seq: ChangeSeq,
300 pub committed_at_ms: u64,
303 #[serde(default, skip_serializing_if = "Option::is_none")]
304 pub message: Option<String>,
305}
306
307impl MetadataState {
308 pub(crate) fn from_rows(
309 inodes: Vec<InodeRecord>,
310 direntry_binds: Vec<DirentryBindRecord>,
311 direntry_unbinds: Vec<DirentryUnbindRecord>,
312 revisions: Vec<RevisionRecord>,
313 subtree_tombstones: Vec<SubtreeTombstoneRecord>,
314 commit_receipts: Vec<CommitReceiptRecord>,
315 ) -> Self {
316 let mut state = Self {
317 inodes,
318 direntry_binds,
319 direntry_unbinds,
320 revisions,
321 subtree_tombstones,
322 commit_receipts,
323 row_count: 0,
324 decoded_bytes: 0,
325 indexes: MetadataIndexes::default(),
326 };
327 state.rebuild_indexes();
328 state
329 }
330
331 pub fn indexed_seq(&self) -> ChangeSeq {
339 self.indexes.indexed_seq()
340 }
341
342 pub fn inodes(&self) -> &[InodeRecord] {
343 &self.inodes
344 }
345
346 pub fn direntry_binds(&self) -> &[DirentryBindRecord] {
347 &self.direntry_binds
348 }
349
350 pub fn direntry_unbinds(&self) -> &[DirentryUnbindRecord] {
351 &self.direntry_unbinds
352 }
353
354 pub fn revisions(&self) -> &[RevisionRecord] {
355 &self.revisions
356 }
357
358 pub fn subtree_tombstones(&self) -> &[SubtreeTombstoneRecord] {
359 &self.subtree_tombstones
360 }
361
362 pub fn commit_receipts(&self) -> &[CommitReceiptRecord] {
363 &self.commit_receipts
364 }
365
366 pub fn row_count(&self) -> usize {
367 self.row_count
368 }
369
370 pub fn decoded_bytes(&self) -> usize {
371 self.decoded_bytes
372 }
373
374 pub fn find_commit_receipt(&self, commit_id: &CommitId) -> Option<&CommitReceiptRecord> {
375 self.indexes.commit_receipt(commit_id)
376 }
377
378 fn rebuild_indexes(&mut self) {
379 self.row_count = metadata_row_count(
380 &self.inodes,
381 &self.direntry_binds,
382 &self.direntry_unbinds,
383 &self.revisions,
384 &self.subtree_tombstones,
385 &self.commit_receipts,
386 );
387 self.decoded_bytes = metadata_decoded_bytes(
388 &self.inodes,
389 &self.direntry_binds,
390 &self.direntry_unbinds,
391 &self.revisions,
392 &self.subtree_tombstones,
393 &self.commit_receipts,
394 );
395 self.indexes = MetadataIndexes::rebuild(
396 &self.inodes,
397 &self.direntry_binds,
398 &self.direntry_unbinds,
399 &self.revisions,
400 &self.subtree_tombstones,
401 &self.commit_receipts,
402 );
403 }
404
405 pub(crate) fn push_inode_record(&mut self, record: InodeRecord) {
406 self.indexes.record_inode(&record);
407 self.record_row_weight(size_of::<InodeRecord>());
408 self.inodes.push(record);
409 }
410
411 pub(crate) fn push_direntry_bind_record(&mut self, record: DirentryBindRecord) {
412 self.indexes.record_bind(&record);
413 self.record_row_weight(direntry_bind_decoded_bytes(&record));
414 self.direntry_binds.push(record);
415 }
416
417 pub(crate) fn push_direntry_unbind_record(&mut self, record: DirentryUnbindRecord) {
418 self.indexes.record_unbind(&record);
419 self.record_row_weight(direntry_unbind_decoded_bytes(&record));
420 self.direntry_unbinds.push(record);
421 }
422
423 pub(crate) fn push_revision_record(&mut self, record: RevisionRecord) {
424 self.indexes.record_revision(&record);
425 self.record_row_weight(revision_decoded_bytes(&record));
426 self.revisions.push(record);
427 }
428
429 pub(crate) fn push_subtree_tombstone_record(&mut self, record: SubtreeTombstoneRecord) {
430 self.indexes.record_tombstone(&record);
431 self.record_row_weight(size_of::<SubtreeTombstoneRecord>());
432 self.subtree_tombstones.push(record);
433 }
434
435 pub(crate) fn push_commit_receipt_record(&mut self, record: CommitReceiptRecord) {
436 self.indexes.record_commit_receipt(&record);
437 self.record_row_weight(commit_receipt_decoded_bytes(&record));
438 self.commit_receipts.push(record);
439 }
440
441 fn record_row_weight(&mut self, decoded_bytes: usize) {
442 self.row_count = self.row_count.saturating_add(1);
443 self.decoded_bytes = self.decoded_bytes.saturating_add(decoded_bytes);
444 }
445}
446
447#[cfg(test)]
448#[derive(Debug, Default)]
449pub(crate) struct MetadataStateBuilder {
450 state: MetadataState,
451}
452
453#[cfg(test)]
454impl MetadataStateBuilder {
455 pub(crate) fn push_inode(&mut self, record: InodeRecord) {
456 self.state.push_inode_record(record);
457 }
458
459 pub(crate) fn push_direntry_bind(&mut self, record: DirentryBindRecord) {
460 self.state.push_direntry_bind_record(record);
461 }
462
463 pub(crate) fn push_direntry_unbind(&mut self, record: DirentryUnbindRecord) {
464 self.state.push_direntry_unbind_record(record);
465 }
466
467 pub(crate) fn push_revision(&mut self, record: RevisionRecord) {
468 self.state.push_revision_record(record);
469 }
470
471 pub(crate) fn push_subtree_tombstone(&mut self, record: SubtreeTombstoneRecord) {
472 self.state.push_subtree_tombstone_record(record);
473 }
474
475 pub(crate) fn push_commit_receipt(&mut self, record: CommitReceiptRecord) {
476 self.state.push_commit_receipt_record(record);
477 }
478
479 pub(crate) fn finish(mut self) -> MetadataState {
480 self.state.rebuild_indexes();
481 self.state
482 }
483}
484
485fn metadata_row_count(
486 inodes: &[InodeRecord],
487 direntry_binds: &[DirentryBindRecord],
488 direntry_unbinds: &[DirentryUnbindRecord],
489 revisions: &[RevisionRecord],
490 subtree_tombstones: &[SubtreeTombstoneRecord],
491 commit_receipts: &[CommitReceiptRecord],
492) -> usize {
493 inodes
494 .len()
495 .saturating_add(direntry_binds.len())
496 .saturating_add(direntry_unbinds.len())
497 .saturating_add(revisions.len())
498 .saturating_add(subtree_tombstones.len())
499 .saturating_add(commit_receipts.len())
500}
501
502fn metadata_decoded_bytes(
503 inodes: &[InodeRecord],
504 direntry_binds: &[DirentryBindRecord],
505 direntry_unbinds: &[DirentryUnbindRecord],
506 revisions: &[RevisionRecord],
507 subtree_tombstones: &[SubtreeTombstoneRecord],
508 commit_receipts: &[CommitReceiptRecord],
509) -> usize {
510 size_of_val(inodes)
511 .saturating_add(
512 direntry_binds
513 .iter()
514 .map(direntry_bind_decoded_bytes)
515 .sum::<usize>(),
516 )
517 .saturating_add(
518 direntry_unbinds
519 .iter()
520 .map(direntry_unbind_decoded_bytes)
521 .sum::<usize>(),
522 )
523 .saturating_add(revisions.iter().map(revision_decoded_bytes).sum::<usize>())
524 .saturating_add(size_of_val(subtree_tombstones))
525 .saturating_add(
526 commit_receipts
527 .iter()
528 .map(commit_receipt_decoded_bytes)
529 .sum::<usize>(),
530 )
531}
532
533fn direntry_bind_decoded_bytes(record: &DirentryBindRecord) -> usize {
534 size_of::<DirentryBindRecord>()
535 + record.name_key.as_str().len()
536 + record.display_name.as_str().len()
537}
538
539fn direntry_unbind_decoded_bytes(record: &DirentryUnbindRecord) -> usize {
540 size_of::<DirentryUnbindRecord>() + record.name_key.as_str().len()
541}
542
543fn revision_decoded_bytes(record: &RevisionRecord) -> usize {
544 size_of::<RevisionRecord>() + content_ref_decoded_bytes(&record.content_ref)
545}
546
547fn commit_receipt_decoded_bytes(record: &CommitReceiptRecord) -> usize {
548 size_of::<CommitReceiptRecord>()
549 + record.commit_id.as_str().len()
550 + record.semantic_commit_fingerprint.len()
551 + record.message.as_ref().map_or(0, String::len)
552}
553
554fn content_ref_decoded_bytes(content_ref: &ContentRef) -> usize {
555 size_of::<ContentRef>() + content_ref_evidence_bytes(content_ref)
556}
557
558pub(crate) fn content_ref_evidence_bytes(content_ref: &ContentRef) -> usize {
561 content_ref.content_id.as_str().len()
562 + content_ref.storage_checksum.value.len()
563 + content_ref
564 .whole_file_sha256
565 .as_ref()
566 .map_or(0, String::len)
567}