Skip to main content

loonfs_core/namespace/
bootstrap.rs

1//! Namespace creation: one conditional write of a complete head.
2//!
3//! The head is the namespace. Nothing is written before it and nothing
4//! after it, so a create either lands entirely or leaves the namespace
5//! absent — there is no partial state to classify, complete, or repair
6//! (format spec, "Creating a namespace").
7
8use crate::context::MutationContext;
9use crate::error::CoreError;
10use crate::metadata::{InodeRecord, MetadataState};
11use crate::namespace::control::{read_head_object, ControlObjectLoadError};
12use bytes::Bytes;
13use loonfs_api::wire::control::{
14    encode_control_object, ControlObjectKind, HeadState, HeadStateEnvelope, NamespaceState,
15    WriterBlock,
16};
17use loonfs_api::{
18    ChangeSeq, ContentStoreId, ErrorCode, InodeKind, NamespaceId, NamespaceSummary, ROOT_INODE_ID,
19};
20use loonfs_objectstore::keys::wal_head;
21use loonfs_objectstore::{ObjectStore, ObjectStoreError};
22use thiserror::Error;
23
24#[derive(Debug, Clone, Error)]
25pub enum BootstrapNamespaceError {
26    #[error("holder id must not be empty")]
27    EmptyHolderId,
28    #[error("namespace `{namespace_id}` already exists")]
29    NamespaceAlreadyExists { namespace_id: NamespaceId },
30    #[error("namespace `{namespace_id}` is deleted and its id is retired")]
31    NamespaceDeleted { namespace_id: NamespaceId },
32    #[error(transparent)]
33    Head(#[from] ControlObjectLoadError),
34    /// A failure inside the installation protocol shared with fork — the
35    /// head write itself, or engine plumbing such as assembling the
36    /// mutation context. The wire code delegates to
37    /// [`CoreError::code`](crate::Error::code), so store failures keep
38    /// their failure class.
39    #[error(transparent)]
40    Core(#[from] CoreError),
41}
42
43impl BootstrapNamespaceError {
44    /// Returns the stable machine-readable reason for this error.
45    ///
46    /// This is the single source of truth for the wire code every surface
47    /// (HTTP server, CLI) reports for a bootstrap failure, mirroring
48    /// [`CoreError::code`](crate::Error::code).
49    pub fn code(&self) -> ErrorCode {
50        match self {
51            BootstrapNamespaceError::EmptyHolderId => ErrorCode::InvalidRequest,
52            BootstrapNamespaceError::NamespaceAlreadyExists { .. } => ErrorCode::NamespaceExists,
53            BootstrapNamespaceError::NamespaceDeleted { .. } => ErrorCode::NamespaceDeleted,
54            BootstrapNamespaceError::Head(_) => ErrorCode::ServerError,
55            BootstrapNamespaceError::Core(error) => error.code(),
56        }
57    }
58
59    /// Returns the structured context the code's consumers report beside it,
60    /// mirroring [`CoreError::details`](crate::Error::details): only the
61    /// wrapped core failure carries any.
62    pub fn details(&self) -> Option<loonfs_api::ErrorDetails> {
63        match self {
64            BootstrapNamespaceError::Core(error) => error.details(),
65            BootstrapNamespaceError::EmptyHolderId
66            | BootstrapNamespaceError::NamespaceAlreadyExists { .. }
67            | BootstrapNamespaceError::NamespaceDeleted { .. }
68            | BootstrapNamespaceError::Head(_) => None,
69        }
70    }
71}
72
73pub(crate) async fn bootstrap_namespace<S: ObjectStore + ?Sized>(
74    store: &S,
75    namespace_id: &NamespaceId,
76    context: &MutationContext,
77    allow_existing: bool,
78) -> Result<NamespaceSummary, BootstrapNamespaceError> {
79    if context.writer_id.trim().is_empty() {
80        return Err(BootstrapNamespaceError::EmptyHolderId);
81    }
82
83    // A fresh content-store id per namespace. Nothing claims it durably:
84    // uniqueness rests on the generated id's randomness, exactly as it does
85    // for every other generated id in the format.
86    let mut head = HeadState::initial(namespace_id.clone(), ContentStoreId::generate());
87    head.writer = Some(WriterBlock {
88        writer_id: context.writer_id.clone(),
89        acquired_at_ms: context.now_ms,
90    });
91
92    match install_namespace_head(store, namespace_id, &head).await? {
93        NamespaceHeadInstall::Landed => Ok(NamespaceSummary {
94            namespace_id: namespace_id.clone(),
95        }),
96        // Whoever wrote the head owns the id. A caller retrying after a
97        // lost acknowledgment gets the same answer as a caller who lost the
98        // race outright, and the namespace it names is complete and usable
99        // either way — the old flow's `namespace_partial`, which named a
100        // namespace nobody could use, is gone. `allow_existing` is how a
101        // caller says "create it if it is not there", including on a retry.
102        NamespaceHeadInstall::Exists if allow_existing => Ok(NamespaceSummary {
103            namespace_id: namespace_id.clone(),
104        }),
105        NamespaceHeadInstall::Exists => Err(BootstrapNamespaceError::NamespaceAlreadyExists {
106            namespace_id: namespace_id.clone(),
107        }),
108        NamespaceHeadInstall::Deleted => Err(BootstrapNamespaceError::NamespaceDeleted {
109            namespace_id: namespace_id.clone(),
110        }),
111    }
112}
113
114/// How one namespace-installing conditional write resolved.
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub(super) enum NamespaceHeadInstall {
117    /// This attempt's write created the namespace.
118    Landed,
119    /// A namespace already owns the id.
120    Exists,
121    /// The id is retired by a deletion tombstone.
122    Deleted,
123}
124
125/// Publishes a complete namespace head as the namespace's one and only
126/// installation write.
127///
128/// Conditional creation is the whole protocol: exactly one attempt can win,
129/// and the loser reads the head back to say what it lost to — an existing
130/// namespace or a deletion tombstone. There is no third answer, because
131/// there is no state between absent and complete.
132///
133/// The loser is not told whether the winner was its own earlier attempt.
134/// Nothing durable can say: the head's writer block is one writer session,
135/// and a server holds one session across every caller it serves, so
136/// "written by my session" does not mean "written by this attempt". A
137/// caller that wants a retry to succeed asks for that with
138/// `allow_existing`.
139pub(super) async fn install_namespace_head<S: ObjectStore + ?Sized>(
140    store: &S,
141    namespace_id: &NamespaceId,
142    head: &HeadState,
143) -> Result<NamespaceHeadInstall, CoreError> {
144    let object_key = wal_head(namespace_id.as_str());
145    let envelope = HeadStateEnvelope::from_state(ControlObjectKind::WalHead, head.clone())
146        .map_err(|err| CoreError::Internal(format!("failed to build head envelope: {err}")))?;
147    let bytes = encode_control_object(&envelope)
148        .map_err(|err| CoreError::Internal(format!("failed to encode head object: {err}")))?;
149    match store.put_if_absent(&object_key, Bytes::from(bytes)).await {
150        Ok(_) => Ok(NamespaceHeadInstall::Landed),
151        Err(ObjectStoreError::PreconditionFailed { .. }) => {
152            let existing = match read_head_object(store, namespace_id).await {
153                Ok(loaded) => loaded.envelope.state,
154                // An unreadable head still occupies the id: report the
155                // corruption rather than a lifecycle answer this attempt
156                // cannot support.
157                Err(error) => return Err(CoreError::load_head(error)),
158            };
159            if existing.state == NamespaceState::Deleted {
160                return Ok(NamespaceHeadInstall::Deleted);
161            }
162            Ok(NamespaceHeadInstall::Exists)
163        }
164        Err(error) => Err(CoreError::store(&object_key, &error)),
165    }
166}
167
168/// The built-in genesis metadata state: the root directory inode, and
169/// nothing else.
170///
171/// A created namespace materializes no manifest, so this is synthesized at
172/// read time as its basis until the first flush publishes one.
173pub(crate) fn bootstrap_metadata_state() -> MetadataState {
174    MetadataState::from_rows(
175        vec![InodeRecord {
176            inode_id: ROOT_INODE_ID,
177            inode_kind: InodeKind::Directory,
178            created_seq: ChangeSeq(0),
179        }],
180        Vec::new(),
181        Vec::new(),
182        Vec::new(),
183        Vec::new(),
184        Vec::new(),
185    )
186}