Skip to content
zit Artifact [362a0ff642]
Login

Artifact [362a0ff642]

Artifact 362a0ff64225fb98a34c17d546b485dcd1182d6aa1dfcf61a6e235c977fa4ce1:


//! Opening a store for append under its one writer lock: the seam every
//! writer passes through, kept free of the CLI so the direct-commit core (and
//! what links it) can take the lock without the verb layer.

const std = @import("std");
const storelock = @import("../storelock.zig");
const checkout = @import("../checkout.zig");
const import_transaction = @import("../import_transaction.zig");

/// The store's file name beside its checkout record.
const repo_marker: []const u8 = ".zit";

/// Open the store for appending, under the store's one write lock.
///
/// Every writer to `.zit` passes through here so that two appenders
/// serialize instead of tearing the log. The lock is `.zit.lock` beside the
/// store, taken by exclusive create (`storelock`), and it is taken BEFORE the
/// store is opened, so the caller's `stat` reads an end no other writer can
/// move. A second writer waits for it, says after a second what it is waiting
/// for, and gives up after `storelock.default_wait_ms` naming the holder.
/// Readers take no lock at all (`verify` documents that promise), so a writer
/// never blocks them and they never block a writer.
///
/// Two rules this imposes on callers. `close` the handle as soon as the
/// append has committed: holding it across later, store-free work (a push,
/// an advisory) extends the exclusion for no reason. And no flow may open a
/// second handle while it still holds one; that is refused at once as
/// `error.StoreLocked` rather than waited on.
///
/// A generation REWRITE (`pack`, `shun --force`) takes the same lock around
/// its final check and rename (`lockStoreWriter`), so no append can land in
/// the generation it is about to replace.
pub fn openStoreAppend(io: std.Io, root_dir: std.Io.Dir) (std.Io.File.OpenError || StoreLocationError || StoreLockError)!StoreAppend {
    var lock = try lockStoreWriter(io, root_dir);
    errdefer lock.release(io);
    return .{ .file = try openStoreFile(io, root_dir), .lock = lock };
}

/// The store file under `root_dir`, resolved through a checkout's store
/// pointer, opened for read and write: the one write-mode open of the store,
/// reached only through `openStoreAppend`, after the lock.
fn openStoreFile(io: std.Io, root_dir: std.Io.Dir) (std.Io.File.OpenError || StoreLocationError)!std.Io.File {
    if (try storeDirOf(io, root_dir)) |store| {
        var store_dir = store;
        defer store_dir.close(io);
        return store_dir.openFile(io, repo_marker, .{ .mode = .read_write });
    }
    return root_dir.openFile(io, repo_marker, .{ .mode = .read_write });
}

/// The store opened for appending, and the writer lock that makes that safe.
pub const StoreAppend = struct {
    file: std.Io.File,
    lock: storelock.Lock,

    /// Closes the store, then releases the lock.
    pub fn close(self: *StoreAppend, io: std.Io) void {
        self.file.close(io);
        self.lock.release(io);
    }
};

/// Why the writer lock was not taken. `StoreBusy`: another process held it
/// past the wait, and stderr already says who and what to do, so a caller
/// adds nothing. `StoreLocked`: anything else, which the caller reports.
pub const StoreLockError = error{ StoreBusy, StoreLocked, StoreLockAmbiguous, ImportRecoveryRequired };

/// Take the store's writer lock, resolved through the checkout to the store's
/// own directory, so every worktree of one store shares one lock. Waiting and
/// giving-up notices go to stderr: several writers have no other stream.
pub fn lockStoreWriter(io: std.Io, root_dir: std.Io.Dir) (StoreLocationError || StoreLockError)!storelock.Lock {
    var lock = try lockStoreRecovery(io, root_dir);
    errdefer lock.release(io);
    const resolved = try storeDirOf(io, root_dir);
    defer if (resolved) |dir| dir.close(io);
    import_transaction.requireIdle(io, resolved orelse root_dir) catch |err| {
        if (err == error.ImportRecoveryRequired) return error.ImportRecoveryRequired;
        return error.StoreLocked;
    };
    return lock;
}

/// Only explicit source-free import recovery may bypass the pending fence.
pub fn lockStoreRecovery(io: std.Io, root_dir: std.Io.Dir) (StoreLocationError || StoreLockError)!storelock.Lock {
    var notice_buf: [512]u8 = undefined;
    // Streaming, as main's own stderr is: a positional writer starts at
    // offset 0 and overwrites what the verb has already written to a
    // redirected stderr.
    var notices: std.Io.File.Writer = .initStreaming(.stderr(), io, &notice_buf);
    defer notices.interface.flush() catch {};
    if (try storeDirOf(io, root_dir)) |store| {
        var store_dir = store;
        defer store_dir.close(io);
        return storelock.acquire(io, store_dir, storelock.writer_lock_name, storelock.default_wait_ms, &notices.interface) catch |e|
            lockError(e);
    }
    return storelock.acquire(io, root_dir, storelock.writer_lock_name, storelock.default_wait_ms, &notices.interface) catch |e|
        lockError(e);
}

fn lockError(e: storelock.AcquireError) StoreLockError {
    return switch (e) {
        error.Busy => error.StoreBusy,
        error.Ambiguous => error.StoreLockAmbiguous,
        error.HeldByThisProcess, error.Io => error.StoreLocked,
    };
}

test "lockStoreWriter: only a holder that outlasted the wait is StoreBusy" {
    // StoreBusy tells callers the notice is already on stderr, so it must
    // mean exactly the case that wrote one.
    try std.testing.expectEqual(error.StoreBusy, lockError(error.Busy));
    try std.testing.expectEqual(error.StoreLocked, lockError(error.HeldByThisProcess));
    try std.testing.expectEqual(error.StoreLocked, lockError(error.Io));
}

pub const StoreLocationError = error{ Storage, Corrupt };

/// Where this checkout's store lives; see `checkout.storeDirOf`.
pub fn storeDirOf(io: std.Io, root_dir: std.Io.Dir) StoreLocationError!?std.Io.Dir {
    return checkout.storeDirOf(io, root_dir);
}