Skip to content
zit Artifact [512205eb62]
Login

Artifact [512205eb62]

Artifact 512205eb629d5af8d50ad336f1ef49f53b455abd52ee2f9b56d20893cafad39f:


//! The narrow file capability the storage engine reads and writes
//! through. Five operations, one error, nothing else — not a wrapper over
//! all of `std.Io`. Kept internal (not re-exported from root.zig) until a
//! real consumer (LogStore) needs it; its unit tests reach `zig build
//! test` through the dst module, which imports this file directly.
//!
//! There is no RFC section behind this file: it is native storage-engine
//! design, not a port. The oracle is this capability's own crash-recovery
//! invariants, exercised by the DST sweep in sim/storage_sweep.zig: every
//! acknowledged put stays readable and hash-verifies; a torn tail
//! truncates to exactly the last complete frame; the rebuilt index
//! matches the write-time model; and corruption always surfaces as an
//! error, never as silently wrong bytes.

const std = @import("std");

/// The device failed. Corruption is deliberately NOT a `StorageError`: a
/// corrupt read returns wrong bytes successfully (short of hardware I/O
/// failure), and the frame layer above is what turns "wrong bytes" into a
/// decision — the crash-recovery invariant that corruption is always an
/// error, never silent data. One variant is the whole set — every other
/// outcome (short read, no-op truncate, EOF) is defined as ordinary data,
/// not an error.
pub const StorageError = error{Io};

/// A capability, not a resource: `context` is caller-owned and outlives
/// every call made through the vtable.
pub const StorageFile = struct {
    context: *anyopaque,
    vtable: *const VTable,

    pub const VTable = struct {
        append: *const fn (ctx: *anyopaque, bytes: []const u8) StorageError!void,
        /// Never errors on short read — returns the count actually read,
        /// `0..buf.len`, with 0 meaning "at or past EOF": the error is
        /// defined out of existence for EOF rather than threaded through
        /// every caller.
        pread: *const fn (ctx: *anyopaque, offset: u64, buf: []u8) StorageError!usize,
        sync: *const fn (ctx: *anyopaque) StorageError!void,
        size: *const fn (ctx: *anyopaque) StorageError!u64,
        /// A no-op (same length) or a repeat truncate is legal, never an
        /// error.
        truncate: *const fn (ctx: *anyopaque, len: u64) StorageError!void,
    };

    pub fn append(self: StorageFile, bytes: []const u8) StorageError!void {
        return self.vtable.append(self.context, bytes);
    }

    pub fn pread(self: StorageFile, offset: u64, buf: []u8) StorageError!usize {
        return self.vtable.pread(self.context, offset, buf);
    }

    pub fn sync(self: StorageFile) StorageError!void {
        return self.vtable.sync(self.context);
    }

    pub fn size(self: StorageFile) StorageError!u64 {
        return self.vtable.size(self.context);
    }

    pub fn truncate(self: StorageFile, len: u64) StorageError!void {
        return self.vtable.truncate(self.context, len);
    }
};

/// Production binding: a real file reached only through the injected
/// `std.Io` (design-seams rule — no ambient filesystem access). `end`
/// tracks the append watermark so `append` never needs a `size` round trip.
pub const FileStorage = struct {
    io: std.Io,
    file: std.Io.File,
    end: u64,

    /// Precondition: `initial_size` equals `file`'s current length. Not
    /// verified here — doing so would mean a stat syscall inside a plain
    /// constructor, and TigerStyle assertions must not carry I/O side
    /// effects. A caller that gets this wrong sees the mismatch surface
    /// downstream, as a wrong `size()`/`pread` result.
    pub fn init(io: std.Io, file: std.Io.File, initial_size: u64) FileStorage {
        return .{ .io = io, .file = file, .end = initial_size };
    }

    pub fn storageFile(self: *FileStorage) StorageFile {
        return .{ .context = self, .vtable = &vtable };
    }

    const vtable: StorageFile.VTable = .{
        .append = appendImpl,
        .pread = preadImpl,
        .sync = syncImpl,
        .size = sizeImpl,
        .truncate = truncateImpl,
    };

    fn appendImpl(ctx: *anyopaque, bytes: []const u8) StorageError!void {
        const self: *FileStorage = @ptrCast(@alignCast(ctx));
        self.file.writePositionalAll(self.io, bytes, self.end) catch return error.Io;
        self.end += bytes.len;
    }

    fn preadImpl(ctx: *anyopaque, offset: u64, buf: []u8) StorageError!usize {
        const self: *FileStorage = @ptrCast(@alignCast(ctx));
        return self.file.readPositionalAll(self.io, buf, offset) catch error.Io;
    }

    fn syncImpl(ctx: *anyopaque) StorageError!void {
        const self: *FileStorage = @ptrCast(@alignCast(ctx));
        self.file.sync(self.io) catch return error.Io;
    }

    fn sizeImpl(ctx: *anyopaque) StorageError!u64 {
        const self: *FileStorage = @ptrCast(@alignCast(ctx));
        return self.end;
    }

    fn truncateImpl(ctx: *anyopaque, len: u64) StorageError!void {
        const self: *FileStorage = @ptrCast(@alignCast(ctx));
        self.file.setLength(self.io, len) catch return error.Io;
        // setLength makes the file exactly `len` bytes — it both shrinks and
        // extends — so the append watermark is `len`, not `@min(end, len)`,
        // which would leave `end` stale (and the next append overwriting the
        // extended tail) whenever `len` grows the file.
        self.end = len;
    }
};

test "storage §B: append then pread round-trips through the prod binding" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const io = std.testing.io;
    var raw = try tmp.dir.createFile(io, "storage-test.bin", .{ .read = true });
    defer raw.close(io);

    var fs = FileStorage.init(io, raw, 0);
    const file = fs.storageFile();
    try file.append("hello");

    var buf: [5]u8 = undefined;
    const n = try file.pread(0, &buf);
    try std.testing.expectEqual(@as(usize, 5), n);
    try std.testing.expectEqualStrings("hello", &buf);
}

test "storage §B: size tracks appended length and truncate cuts the tail exactly" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const io = std.testing.io;
    var raw = try tmp.dir.createFile(io, "storage-test.bin", .{ .read = true });
    defer raw.close(io);

    var fs = FileStorage.init(io, raw, 0);
    const file = fs.storageFile();
    try file.append("0123456789");
    try std.testing.expectEqual(@as(u64, 10), try file.size());

    try file.truncate(4);
    try std.testing.expectEqual(@as(u64, 4), try file.size());

    var buf: [4]u8 = undefined;
    const n = try file.pread(0, &buf);
    try std.testing.expectEqual(@as(usize, 4), n);
    try std.testing.expectEqualStrings("0123", &buf);
}

test "storage §B: sync on a healthy file returns without error" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const io = std.testing.io;
    var raw = try tmp.dir.createFile(io, "storage-test.bin", .{ .read = true });
    defer raw.close(io);

    var fs = FileStorage.init(io, raw, 0);
    const file = fs.storageFile();
    try file.append("payload");
    try file.sync();
}

test "storage §B: pread at/after EOF returns a short count, not an error" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const io = std.testing.io;
    var raw = try tmp.dir.createFile(io, "storage-test.bin", .{ .read = true });
    defer raw.close(io);

    var fs = FileStorage.init(io, raw, 0);
    const file = fs.storageFile();
    try file.append("ab");

    var buf: [4]u8 = undefined;
    const n = try file.pread(2, &buf); // exactly at EOF
    try std.testing.expectEqual(@as(usize, 0), n);
    const n2 = try file.pread(10, &buf); // well past EOF
    try std.testing.expectEqual(@as(usize, 0), n2);
}