haematite 0.7.0

Content-addressed, branchable, actor-native storage engine
Documentation
//! FENCE-CHAIN r8 D3 store-root construction modes and their directory
//! establishment (single-level create, unconditional fence for the fenced mode).

use std::fs;
use std::io::ErrorKind;
use std::path::Path;

use super::error::StoreError;

/// Which of FENCE-CHAIN r8 D3's three store-root construction modes a
/// [`super::DiskStore`] is being built in.
#[derive(Clone, Copy, Debug)]
pub enum ConstructMode {
    /// Create-or-open AND fence the store root's entry (public constructors, L2).
    CreateOrOpenFenced,
    /// Create-or-open without fencing (native boot; fence owned by the WAL).
    CreateOrOpenUnfenced,
    /// Open an existing store root, refusing to create, never fencing (observer).
    OpenExistingUnfenced,
}

/// Establish the store root per `mode` (D1 single-level create, D2 unconditional
/// fence for the fenced mode).
///
/// D1: creation is a SINGLE level below a pre-existing parent — the owning
/// subsystem (the router for `shard-{id}`, the database for `data_dir`)
/// established the parent. A missing parent is a typed refusal
/// ([`StoreError::DirectoryNotFound`]), never a deep `create_dir_all`.
///
/// D2: an existing directory of the right kind is accepted and fenced
/// UNCONDITIONALLY (the fenced mode fences whether or not this call created it);
/// only a missing immediate parent refuses. Created-this-call tracking scopes
/// nothing here — unlocked owners never remove on fence failure; residue is safe
/// because the next open re-runs the same unconditional fence.
pub fn ensure_directory(path: &Path, mode: ConstructMode) -> Result<(), StoreError> {
    let existed = match fs::metadata(path) {
        Ok(metadata) if metadata.is_dir() => true,
        Ok(_metadata) => {
            return Err(StoreError::NotADirectory {
                path: path.to_path_buf(),
            });
        }
        Err(error) if error.kind() == ErrorKind::NotFound => false,
        Err(error) => return Err(directory_error(error, path)),
    };

    match mode {
        ConstructMode::OpenExistingUnfenced => {
            if !existed {
                return Err(StoreError::DirectoryNotFound);
            }
            Ok(())
        }
        ConstructMode::CreateOrOpenUnfenced => {
            if !existed {
                // NOT journalled: this store root's fence is owned transitively by
                // the WAL's shard-directory fsync (R1e), which is outside the
                // fence-chain journal — so no journal-tracked fence would ever
                // clear a Create registered here, and the entry must not be
                // recorded as an unfenced obligation.
                create_dir_single(path, Journalled::No)?;
            }
            Ok(())
        }
        ConstructMode::CreateOrOpenFenced => {
            if !existed {
                create_dir_single(path, Journalled::Yes)?;
            }
            fence_store_root_entry(path)
        }
    }
}

/// Whether a single-level create records a journal obligation. Only the fenced
/// mode does — its Create is cleared by a journal-tracked store-root fence; the
/// unfenced (native) mode's fence is the WAL's, outside the journal.
#[derive(Clone, Copy)]
enum Journalled {
    Yes,
    No,
}

/// Create `path` a SINGLE level below its pre-existing parent (D1). A concurrent
/// creator winning the race (`AlreadyExists`) is accepted.
fn create_dir_single(path: &Path, journalled: Journalled) -> Result<(), StoreError> {
    #[cfg(test)]
    let reservation = match journalled {
        Journalled::Yes => Some(crate::fence::journal::reserve_create_dir(path)),
        Journalled::No => None,
    };
    #[cfg(not(test))]
    let _ = journalled;
    let result = fs::create_dir(path);
    match result {
        Ok(()) => {
            #[cfg(test)]
            if let Some(reservation) = reservation {
                reservation.commit();
            }
            Ok(())
        }
        Err(error) if error.kind() == ErrorKind::AlreadyExists => {
            #[cfg(test)]
            if let Some(reservation) = reservation {
                reservation.cancel();
            }
            Ok(())
        }
        Err(error) => {
            #[cfg(test)]
            if let Some(reservation) = reservation {
                reservation.cancel();
            }
            Err(directory_error(error, path))
        }
    }
}

/// Fence the store root's OWN directory entry by fsyncing its parent
/// (`shard-{id}`) — closes L2. Unconditional per D2.
fn fence_store_root_entry(path: &Path) -> Result<(), StoreError> {
    let Some(parent) = path.parent() else {
        return Ok(());
    };
    crate::fence::sync_dir_entry(parent).map_err(StoreError::Io)
}

/// Map a raw directory I/O error onto the typed [`StoreError`]. Shared with the
/// prefix-subdir establishment in `super::barrier`.
pub fn directory_error(error: std::io::Error, path: &Path) -> StoreError {
    match error.kind() {
        ErrorKind::NotFound => StoreError::DirectoryNotFound,
        ErrorKind::NotADirectory | ErrorKind::AlreadyExists => StoreError::NotADirectory {
            path: path.to_path_buf(),
        },
        _ => StoreError::Io(error),
    }
}