querylog: the aside file's name says why, so a schema change is not called corrupt
This commit is contained in:
@@ -138,7 +138,7 @@ pub fn open(io: std.Io, dir: std.Io.Dir, path: [:0]const u8) Error!OpenResult {
|
||||
const aside: ?[]const u8 = if (cause == .missing)
|
||||
null
|
||||
else
|
||||
try renameAside(io, dir, path, &aside_buf);
|
||||
try renameAside(io, dir, path, cause, &aside_buf);
|
||||
|
||||
// Not optional: a stale WAL left beside the renamed database would be
|
||||
// replayed into the freshly created file and corrupt it immediately. Any
|
||||
@@ -178,20 +178,37 @@ fn quickCheck(database: *db.Db) db.Error!bool {
|
||||
return std.ascii.eqlIgnoreCase(stmt.columnText(0), "ok");
|
||||
}
|
||||
|
||||
/// What the aside file's name calls the reason it was set aside.
|
||||
///
|
||||
/// The name is the only account of the reason an operator gets: the log line
|
||||
/// naming it scrolls away, the file stays for months. `fingerprint_mismatch` is
|
||||
/// a database with nothing wrong with it — this build's DDL moved — so calling
|
||||
/// its file "corrupt" invites the operator to delete evidence of a healthy file.
|
||||
fn asideTag(reason: RecreateReason) []const u8 {
|
||||
return switch (reason) {
|
||||
.missing => unreachable, // there is no file to rename
|
||||
.corrupt => "corrupt",
|
||||
.not_a_database => "not-a-database",
|
||||
.quick_check_failed => "quick-check-failed",
|
||||
.fingerprint_mismatch => "schema-changed",
|
||||
};
|
||||
}
|
||||
|
||||
/// Renames the unusable file out of the way and returns the name it now has.
|
||||
///
|
||||
/// `renamePreserve` is `RENAME_NOREPLACE`: it returns `error.PathAlreadyExists`
|
||||
/// instead of overwriting. A previously saved corrupt file must never be
|
||||
/// destroyed by the next recreate, and two recreates in the same second are not
|
||||
/// hypothetical on a boot loop — hence the uniquifying retries.
|
||||
fn renameAside(io: std.Io, dir: std.Io.Dir, path: []const u8, buf: []u8) Error![]const u8 {
|
||||
/// instead of overwriting. A previously saved file must never be destroyed by
|
||||
/// the next recreate, and two recreates in the same second are not hypothetical
|
||||
/// on a boot loop — hence the uniquifying retries.
|
||||
fn renameAside(io: std.Io, dir: std.Io.Dir, path: []const u8, reason: RecreateReason, buf: []u8) Error![]const u8 {
|
||||
const tag = asideTag(reason);
|
||||
const seconds = std.Io.Clock.real.now(io).toSeconds();
|
||||
var attempt: u32 = 0;
|
||||
while (attempt < 100) : (attempt += 1) {
|
||||
const aside = if (attempt == 0)
|
||||
std.fmt.bufPrint(buf, "{s}.corrupt-{d}", .{ path, seconds }) catch return error.NameTooLong
|
||||
std.fmt.bufPrint(buf, "{s}.{s}-{d}", .{ path, tag, seconds }) catch return error.NameTooLong
|
||||
else
|
||||
std.fmt.bufPrint(buf, "{s}.corrupt-{d}-{d}", .{ path, seconds, attempt }) catch return error.NameTooLong;
|
||||
std.fmt.bufPrint(buf, "{s}.{s}-{d}-{d}", .{ path, tag, seconds, attempt }) catch return error.NameTooLong;
|
||||
|
||||
dir.renamePreserve(path, dir, aside, io) catch |e| switch (e) {
|
||||
error.PathAlreadyExists => continue,
|
||||
@@ -287,6 +304,13 @@ test "recreatable is a whitelist and never selects a resource error" {
|
||||
}
|
||||
}
|
||||
|
||||
test "the aside name says why, and a healthy file is never called corrupt" {
|
||||
try testing.expectEqualStrings("corrupt", asideTag(.corrupt));
|
||||
try testing.expectEqualStrings("not-a-database", asideTag(.not_a_database));
|
||||
try testing.expectEqualStrings("quick-check-failed", asideTag(.quick_check_failed));
|
||||
try testing.expectEqualStrings("schema-changed", asideTag(.fingerprint_mismatch));
|
||||
}
|
||||
|
||||
test "recreatable selects exactly two of db.Error's members" {
|
||||
// Exhaustive over the whole set, so a variant added to `db.Error` later
|
||||
// defaults to propagate. The list above only proves the named errors are
|
||||
|
||||
@@ -190,7 +190,7 @@ fn countLines(text: []const u8) usize {
|
||||
// querylog aside files
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const aside_prefix = "querylog.db.corrupt-";
|
||||
const aside_prefix = "querylog.db.";
|
||||
|
||||
const Names = struct {
|
||||
items: std.ArrayList([]u8),
|
||||
@@ -412,6 +412,10 @@ test "S7 case 3: a wrong user_version recreates and keeps the old file aside" {
|
||||
defer asides.deinit();
|
||||
try testing.expectEqual(@as(usize, 1), asides.items.items.len);
|
||||
|
||||
// The file was healthy: this build's schema moved, the database did not rot.
|
||||
// An operator who reads "corrupt" here deletes a file that was never broken.
|
||||
try testing.expect(std.mem.startsWith(u8, asides.items.items[0], "querylog.db.schema-changed-"));
|
||||
|
||||
const kept = try f.read(asides.items.items[0]);
|
||||
defer testing.allocator.free(kept);
|
||||
try testing.expectEqualSlices(u8, original, kept);
|
||||
@@ -441,6 +445,12 @@ test "S7 case 4: a garbage file recreates and the garbage is preserved" {
|
||||
defer asides.deinit();
|
||||
try testing.expectEqual(@as(usize, 1), asides.items.items.len);
|
||||
|
||||
const expected: []const u8 = if (reason == .corrupt)
|
||||
"querylog.db.corrupt-"
|
||||
else
|
||||
"querylog.db.not-a-database-";
|
||||
try testing.expect(std.mem.startsWith(u8, asides.items.items[0], expected));
|
||||
|
||||
const kept = try f.read(asides.items.items[0]);
|
||||
defer testing.allocator.free(kept);
|
||||
try testing.expectEqualSlices(u8, &garbage, kept);
|
||||
|
||||
Reference in New Issue
Block a user