Skip to main content

loonfs_core/namespace/
status.rs

1//! Read-only namespace status: summarizes the head, its materialized
2//! basis, the WAL tail, and the retention floor.
3
4use crate::checkpoint::load_namespace_manifest_envelope;
5use crate::error::MetadataProjectionLoadError;
6use crate::error::{CoreError, Result};
7use crate::namespace::basis::{read_head_and_metadata_basis, resolve_retention_floor_seq};
8use crate::wal::{count_visible_wal_tail_segments, WalChainLoadRequest};
9use loonfs_api::wire::control::NamespaceState;
10use loonfs_api::{ChangeSeq, ManifestId, NamespaceId};
11use loonfs_objectstore::ObjectStore;
12use serde::{Deserialize, Serialize};
13
14/// Lightweight namespace head status.
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct NamespaceHeadSummary {
17    pub namespace_id: NamespaceId,
18    pub head_seq: ChangeSeq,
19    /// Manifest this namespace has materialized for itself, or `None` when
20    /// it has published none yet: a fresh namespace reads from the genesis
21    /// state, and a fresh fork target reads from its source's manifest.
22    pub current_manifest_id: Option<ManifestId>,
23    /// Number of visible WAL segments after the current manifest.
24    ///
25    /// Counted from the head's chain pointers (`recent_segments`, published
26    /// under the same CAS as the tip); segment bodies are fetched and
27    /// validated only for a tail extending past the hinted window. An
28    /// inspection count for maintenance gating and operators — replay
29    /// consumers load the validated chain instead.
30    pub wal_tail_segments: u64,
31    pub retention_floor_seq: ChangeSeq,
32}
33
34pub async fn load_namespace_head_summary<S: ObjectStore + ?Sized>(
35    store: &S,
36    expected_namespace_id: &NamespaceId,
37) -> Result<NamespaceHeadSummary> {
38    let loaded = read_head_and_metadata_basis(store, expected_namespace_id)
39        .await
40        .map_err(|error| {
41            CoreError::MetadataProjection(MetadataProjectionLoadError::LoadHead(error))
42        })?;
43    let head = loaded.head.envelope.state;
44    if head.state == NamespaceState::Deleted {
45        return Err(CoreError::NamespaceDeleted {
46            namespace_id: expected_namespace_id.clone(),
47        });
48    }
49    // The tail is counted from the basis manifest's coverage, whoever owns
50    // it: a fork target that has not flushed counts from its fork point.
51    let (current_manifest_id, basis_head_seq) = match loaded.basis.manifest() {
52        Some(basis) => {
53            let manifest = load_namespace_manifest_envelope(
54                store,
55                &basis.owner_namespace_id,
56                &basis.manifest_object_id,
57            )
58            .await
59            .map_err(|error| {
60                CoreError::MetadataProjection(MetadataProjectionLoadError::ManifestLoad(error))
61            })?;
62            let own_manifest_id = loaded
63                .basis
64                .is_owned_by(expected_namespace_id)
65                .then_some(basis.manifest_id);
66            (own_manifest_id, manifest.payload.head_seq)
67        }
68        None => (None, ChangeSeq(0)),
69    };
70    let wal_tail_segments = count_visible_wal_tail_segments(
71        store,
72        WalChainLoadRequest {
73            namespace_id: expected_namespace_id,
74            chain_base_seq: basis_head_seq,
75            head_seq: head.seq,
76            visible_tip: head.visible_wal_tip.clone(),
77            stop_after_seq: None,
78            recent_segments: &head.recent_segments,
79        },
80    )
81    .await
82    .map_err(|error| {
83        CoreError::MetadataProjection(MetadataProjectionLoadError::WalChainLoad(error))
84    })?;
85    let retention_floor_seq = resolve_retention_floor_seq(store, &head)
86        .await
87        .map_err(|error| {
88            CoreError::MetadataProjection(MetadataProjectionLoadError::LoadHead(error))
89        })?;
90    Ok(NamespaceHeadSummary {
91        namespace_id: head.namespace_id,
92        head_seq: head.seq,
93        current_manifest_id,
94        wal_tail_segments,
95        retention_floor_seq,
96    })
97}
98
99/// Summarizes a namespace whose head is a deletion tombstone.
100///
101/// Reads only the two control objects that outlive reclamation — the head
102/// and the WAL floor — because garbage collection may already have reaped
103/// the manifest and chain a live summary would consult. Callers reach for
104/// this only after [`load_namespace_head_summary`] reported the deletion;
105/// a live head here is an invariant breach, not a state to serve.
106pub async fn load_deleted_namespace_head_summary<S: ObjectStore + ?Sized>(
107    store: &S,
108    expected_namespace_id: &NamespaceId,
109) -> Result<NamespaceHeadSummary> {
110    let head = crate::namespace::control::read_head_object(store, expected_namespace_id)
111        .await
112        .map_err(|error| {
113            CoreError::MetadataProjection(MetadataProjectionLoadError::LoadHead(error))
114        })?
115        .envelope
116        .state;
117    if head.state != NamespaceState::Deleted {
118        return Err(CoreError::Internal(format!(
119            "namespace `{expected_namespace_id}` is not deleted; the live head summary serves it"
120        )));
121    }
122    let retention_floor_seq = resolve_retention_floor_seq(store, &head)
123        .await
124        .map_err(|error| {
125            CoreError::MetadataProjection(MetadataProjectionLoadError::LoadHead(error))
126        })?;
127    Ok(NamespaceHeadSummary {
128        namespace_id: head.namespace_id,
129        head_seq: head.seq,
130        current_manifest_id: None,
131        wal_tail_segments: 0,
132        retention_floor_seq,
133    })
134}