Skip to main content

loonfs_core/namespace/
basis.rs

1//! Where a namespace's materialized metadata starts.
2//!
3//! A namespace publishes `metadata/root.json` at its first flush, not at
4//! creation, so the basis is resolved from the head plus that root when it
5//! exists (format spec, "Resolving the metadata basis"):
6//!
7//! 1. `metadata/root.json` present: the basis is the manifest it names,
8//!    under this namespace's own prefix.
9//! 2. Root absent, head carries no fork basis: the basis is the built-in
10//!    genesis state — one root-inode row at sequence zero. No manifest
11//!    object exists, and none was ever written.
12//! 3. Root absent, head carries a fork basis: the basis is the source
13//!    namespace's manifest, read under the source's prefix and validated
14//!    against the identity and checksum the head recorded. A mismatch is
15//!    corruption, never a fallback.
16
17use crate::error::CoreError;
18use crate::namespace::control::{
19    read_head_and_metadata_root_if_present, read_wal_floor_object, ControlObjectLoadError,
20    LoadedHeadObject,
21};
22use loonfs_api::wire::control::HeadState;
23use loonfs_api::{manifest_object_id_manifest_id, ChangeSeq, ManifestId, ManifestObjectId};
24use loonfs_api::{NamespaceId, ROOT_INODE_ID};
25use loonfs_objectstore::ObjectStore;
26
27/// The materialized starting point every read and flush builds on.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum MetadataBasis {
30    /// The built-in genesis state: one root-inode row at sequence zero,
31    /// synthesized rather than loaded. A created namespace reads from this
32    /// until its first flush publishes a manifest.
33    Genesis,
34    /// A manifest object, owned by this namespace or — while the head still
35    /// authorizes it — by the fork source.
36    Manifest(BasisManifest),
37}
38
39/// The manifest a basis resolves to, and who owns the objects it names.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct BasisManifest {
42    /// Namespace under whose prefix the manifest and its tables live.
43    pub owner_namespace_id: NamespaceId,
44    /// Logical manifest position, which the object id encodes.
45    pub manifest_id: ManifestId,
46    pub manifest_object_id: ManifestObjectId,
47    /// The `payload_checksum` the loaded manifest must carry.
48    pub manifest_payload_checksum: String,
49}
50
51impl MetadataBasis {
52    pub fn manifest(&self) -> Option<&BasisManifest> {
53        match self {
54            MetadataBasis::Genesis => None,
55            MetadataBasis::Manifest(manifest) => Some(manifest),
56        }
57    }
58
59    /// Logical position this basis sits at. Genesis is position zero: the
60    /// namespace's first published manifest is one past it.
61    pub fn manifest_id(&self) -> ManifestId {
62        match self {
63            MetadataBasis::Genesis => ManifestId(0),
64            MetadataBasis::Manifest(manifest) => manifest.manifest_id,
65        }
66    }
67
68    /// Whether the basis is a manifest this namespace itself published, so
69    /// its own `metadata/root.json` exists.
70    pub fn is_owned_by(&self, namespace_id: &NamespaceId) -> bool {
71        self.manifest()
72            .is_some_and(|manifest| manifest.owner_namespace_id == *namespace_id)
73    }
74}
75
76/// The head and its resolved basis, read together.
77pub(crate) struct LoadedNamespaceBasis {
78    pub(crate) head: LoadedHeadObject,
79    pub(crate) basis: MetadataBasis,
80}
81
82/// Reads the head and resolves the basis it authorizes.
83pub(crate) async fn read_head_and_metadata_basis<S: ObjectStore + ?Sized>(
84    store: &S,
85    namespace_id: &NamespaceId,
86) -> Result<LoadedNamespaceBasis, ControlObjectLoadError> {
87    let (head, root) = read_head_and_metadata_root_if_present(store, namespace_id).await?;
88    let basis = match root {
89        Some(root) => MetadataBasis::Manifest(BasisManifest {
90            owner_namespace_id: namespace_id.clone(),
91            manifest_id: root.envelope.state.manifest_id,
92            manifest_object_id: root.envelope.state.manifest_object_id,
93            manifest_payload_checksum: root.envelope.state.manifest_payload_checksum,
94        }),
95        None => metadata_basis_without_root(&head.envelope.state)?,
96    };
97    Ok(LoadedNamespaceBasis { head, basis })
98}
99
100/// Resolves the basis of a namespace whose `metadata/root.json` is absent:
101/// the built-in genesis state, or the fork source's manifest the head
102/// authorizes.
103pub(crate) fn metadata_basis_without_root(
104    head: &HeadState,
105) -> Result<MetadataBasis, ControlObjectLoadError> {
106    let Some(fork_basis) = &head.fork_basis else {
107        return Ok(MetadataBasis::Genesis);
108    };
109    let manifest_id = manifest_object_id_manifest_id(fork_basis.source_manifest_object_id.as_str())
110        .ok_or_else(|| ControlObjectLoadError::Codec {
111            object_key: loonfs_objectstore::keys::wal_head(head.namespace_id.as_str()),
112            message: format!(
113                "fork basis manifest object id `{}` does not encode a manifest id",
114                fork_basis.source_manifest_object_id
115            ),
116        })?;
117    Ok(MetadataBasis::Manifest(BasisManifest {
118        owner_namespace_id: fork_basis.source_namespace_id.clone(),
119        manifest_id,
120        manifest_object_id: fork_basis.source_manifest_object_id.clone(),
121        manifest_payload_checksum: fork_basis.source_manifest_checksum.clone(),
122    }))
123}
124
125/// The genesis head fields a synthesized basis replays from: sequence zero,
126/// the genesis commit, and the root inode already reserved.
127pub(crate) fn genesis_next_inode_id() -> loonfs_api::InodeId {
128    loonfs_api::InodeId(ROOT_INODE_ID.0 + 1)
129}
130
131/// The sequence a namespace's own history begins at: zero for a created
132/// namespace, the fork point for a fork target.
133pub(crate) fn namespace_birth_seq(head: &HeadState) -> ChangeSeq {
134    head.fork_basis
135        .as_ref()
136        .map_or(ChangeSeq(0), |fork_basis| fork_basis.fork_seq)
137}
138
139/// Reads the retention floor, treating a missing floor object as the
140/// namespace's birth sequence.
141///
142/// A namespace has no WAL history below its birth sequence, so "retain from
143/// birth" is the most conservative reading of an absent floor: create and
144/// fork write no floor, and the first advance publishes one.
145pub(crate) async fn resolve_retention_floor_seq<S: ObjectStore + ?Sized>(
146    store: &S,
147    head: &HeadState,
148) -> Result<ChangeSeq, ControlObjectLoadError> {
149    match read_wal_floor_object(store, &head.namespace_id).await {
150        Ok(loaded) => Ok(loaded.envelope.state.floor_seq),
151        Err(ControlObjectLoadError::MissingObject { .. }) => Ok(namespace_birth_seq(head)),
152        Err(error) => Err(error),
153    }
154}
155
156/// Reports a basis that resolved past a materialized root that is not there.
157///
158/// The retention invariant keeps the floor at or below the materialized
159/// root, so a floor above the namespace's birth sequence with no root object
160/// means the root was lost, not that the namespace is young.
161pub(crate) fn advanced_floor_without_root(
162    namespace_id: &NamespaceId,
163    floor_seq: ChangeSeq,
164) -> CoreError {
165    CoreError::NamespaceCorrupt(format!(
166        "namespace `{namespace_id}` has no materialized metadata root but its retention floor \
167         stands at `{floor_seq}`; the root object is missing"
168    ))
169}