pointlock-store 0.1.8

Pointlock's event-sourced RunLog, SQLite/WAL checkpoints, evidence store, and read-side projections.
Documentation
//! The advisory per-run writer lease (07 §3.3 rule 5).
//!
//! The ledger is single-writer (I1), but a `running` status alone cannot
//! say whether a writer is alive or died mid-segment without writing
//! `runSuspended`. Liveness is therefore a separate primitive: a
//! non-blocking exclusive `flock` on `<root>/locks/<run>.lock`, taken by
//! every writing segment ([`pointlock_runner`](https://docs.rs/pointlock-runner)'s
//! `run`/`resume`) before its first append and held until the segment
//! returns. The kernel releases `flock` when the holder dies, so:
//!
//! - lease held → a live writer; every other writer must refuse
//!   ([`StoreError::WriterBusy`]);
//! - lease free + status `running` → the previous segment crashed; the
//!   run is resumable without any operator vouch.
//!
//! **Advisory and filesystem-bound.** `flock` is advisory (a writer that
//! skips it is not stopped) and is unreliable on network filesystems
//! (NFS, SMB): there it may error, or — worse — grant two holders. Keep
//! stores on local disks; `pointlock resume --force-stale-writer` is the
//! escape hatch for filesystems where the lock lies.
//!
//! The guard's `Drop` calls `unlock` explicitly before the descriptor
//! closes: a close-only release can be pinned by a child process that
//! inherited a duplicate of the descriptor (`inspect --serve` spawns
//! children), which would make a dead segment look alive.

use std::fs::{File, OpenOptions};
use std::path::{Path, PathBuf};

use fs2::FileExt;

use crate::error::StoreError;

/// An acquired per-run writer lease; dropping it releases the lock.
#[derive(Debug)]
pub struct WriterLease {
    file: File,
    run_id: String,
}

/// `<root>/locks/<run>.lock`. Run ids are free-form strings, so any byte
/// outside `[A-Za-z0-9._-]` is mapped to `_`: two exotic ids could share a
/// file, which only errs on the refusing side (a spurious `WriterBusy`,
/// escapable with `--force-stale-writer`), never on the permissive one.
pub fn lock_path(root: &Path, run_id: &str) -> PathBuf {
    let safe: String = run_id
        .chars()
        .map(|c| {
            if c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-') {
                c
            } else {
                '_'
            }
        })
        .collect();
    root.join("locks").join(format!("{safe}.lock"))
}

fn open_lock_file(root: &Path, run_id: &str) -> Result<File, StoreError> {
    let path = lock_path(root, run_id);
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    Ok(OpenOptions::new()
        .read(true)
        .write(true)
        .create(true)
        .truncate(false)
        .open(path)?)
}

impl WriterLease {
    /// Takes the run's lease without blocking. A lease held elsewhere —
    /// another process, or another handle in this one — is
    /// [`StoreError::WriterBusy`]; any other lock failure surfaces as
    /// [`StoreError::Io`].
    pub fn acquire(root: impl AsRef<Path>, run_id: &str) -> Result<Self, StoreError> {
        let file = open_lock_file(root.as_ref(), run_id)?;
        match FileExt::try_lock_exclusive(&file) {
            Ok(()) => Ok(WriterLease {
                file,
                run_id: run_id.to_owned(),
            }),
            Err(err) if err.kind() == fs2::lock_contended_error().kind() => {
                Err(StoreError::WriterBusy {
                    run_id: run_id.to_owned(),
                })
            }
            Err(err) => Err(err.into()),
        }
    }

    /// Probes liveness: `true` iff some holder currently has the lease.
    /// The probe try-locks and immediately unlocks + closes; it never
    /// holds across anything else and creates nothing: a lease can only
    /// be held on an existing lock file, so a missing file is `false`
    /// without touching the filesystem. A lock file that exists but
    /// cannot be opened answers `true` (fail closed: a probe that cannot
    /// see must not vouch).
    pub fn is_held(root: impl AsRef<Path>, run_id: &str) -> bool {
        let file = match OpenOptions::new()
            .read(true)
            .write(true)
            .open(lock_path(root.as_ref(), run_id))
        {
            Ok(file) => file,
            Err(err) if err.kind() == std::io::ErrorKind::NotFound => return false,
            Err(_) => return true,
        };
        match FileExt::try_lock_exclusive(&file) {
            Ok(()) => {
                let _ = FileExt::unlock(&file);
                false
            }
            Err(_) => true,
        }
    }

    /// The run this lease guards.
    pub fn run_id(&self) -> &str {
        &self.run_id
    }
}

impl Drop for WriterLease {
    fn drop(&mut self) {
        // Explicit release (see the module docs): close alone may not
        // drop the lock when a child holds a duplicate descriptor. The
        // fully qualified call pins fs2's method (std grew an inherent
        // `File::unlock` in 1.89, above this workspace's MSRV).
        let _ = FileExt::unlock(&self.file);
    }
}