milestone 27: diagnostics — operational failures land in one curated log, resolved history purgeable
Gates / frontend (push) Successful in 1m33s
Gates / test (push) Successful in 1m48s
Gates / test-aarch64 (push) Successful in 7m10s
Gates / package (push) Successful in 5m31s
Gates / container (push) Successful in 15s
CI / gates (push) Successful in 14m51s

This commit is contained in:
2026-08-20 20:05:59 +02:00
parent 3dd8214ef2
commit 037f209179
50 changed files with 8608 additions and 102 deletions
+68 -1
View File
@@ -88,6 +88,18 @@ pub const OpenResult = struct {
database: db.Db,
/// Non-null feeds a counter and the `/api/health` rollup.
recreated: ?RecreateReason,
/// The path the previous file was kept as, by value. It existed only in a
/// stack buffer inside `open` before the diagnostics event needed it, and a
/// slice of that buffer would dangle the moment `open` returned.
///
/// Empty when nothing was renamed aside, which `.missing` and a clean open
/// both are.
aside_buf: [path_buf_len]u8 = undefined,
aside_len: u16 = 0,
pub fn aside(self: *const OpenResult) []const u8 {
return self.aside_buf[0..self.aside_len];
}
};
pub const Error = db.Error || error{AsideNameCollision} ||
@@ -156,7 +168,12 @@ pub fn open(io: std.Io, dir: std.Io.Dir, path: [:0]const u8) Error!OpenResult {
aside.?,
});
}
return .{ .database = fresh, .recreated = cause };
var result: OpenResult = .{ .database = fresh, .recreated = cause };
if (aside) |name| {
result.aside_len = @intCast(name.len);
@memcpy(result.aside_buf[0..name.len], name);
}
return result;
}
/// The whitelist. `null` means "propagate, do not touch the file".
@@ -321,3 +338,53 @@ test "recreatable selects exactly two of db.Error's members" {
}
try testing.expectEqual(@as(usize, 2), whitelisted);
}
test "a recreate returns the aside name by value and a fresh create returns none" {
var threaded: std.Io.Threaded = .init(testing.allocator, .{});
defer threaded.deinit();
const io = threaded.io();
var tmp = testing.tmpDir(.{ .iterate = true });
defer tmp.cleanup();
var path_buf: [path_buf_len]u8 = undefined;
const path = try std.fmt.bufPrintZ(&path_buf, ".zig-cache/tmp/{s}/querylog.db", .{tmp.sub_path});
// First open: the file is missing, so nothing is renamed aside. The
// one-shot event is deliberately not emitted for this case.
var created = try open(io, std.Io.Dir.cwd(), path);
created.database.close();
try testing.expectEqual(RecreateReason.missing, created.recreated.?);
try testing.expectEqualStrings("", created.aside());
try tmp.dir.writeFile(io, .{ .sub_path = "querylog.db", .data = "not a database at all" });
var recreated = try open(io, std.Io.Dir.cwd(), path);
recreated.database.close();
try testing.expectEqual(RecreateReason.not_a_database, recreated.recreated.?);
try testing.expect(recreated.aside().len != 0);
// The name is a real file, which is the whole reason it travels out.
const kept = std.fs.path.basename(recreated.aside());
try tmp.dir.access(io, kept, .{});
}
test "a clean reopen reports no recreate and no aside" {
var threaded: std.Io.Threaded = .init(testing.allocator, .{});
defer threaded.deinit();
const io = threaded.io();
var tmp = testing.tmpDir(.{ .iterate = true });
defer tmp.cleanup();
var path_buf: [path_buf_len]u8 = undefined;
const path = try std.fmt.bufPrintZ(&path_buf, ".zig-cache/tmp/{s}/querylog.db", .{tmp.sub_path});
var first = try open(io, std.Io.Dir.cwd(), path);
first.database.close();
var second = try open(io, std.Io.Dir.cwd(), path);
second.database.close();
try testing.expectEqual(@as(?RecreateReason, null), second.recreated);
try testing.expectEqualStrings("", second.aside());
}