//! 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, ¬ice_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, ¬ices.interface) catch |e|
lockError(e);
}
return storelock.acquire(io, root_dir, storelock.writer_lock_name, storelock.default_wait_ms, ¬ices.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);
}