milestone 30: overview as a dashboard, explicit health contract, period aggregations
Gates / frontend (push) Successful in 1m32s
Gates / test (push) Successful in 1m54s
Gates / package (push) Successful in 5m28s
Gates / container (push) Successful in 14s
Gates / test-aarch64 (push) Failing after 3h10m0s
CI / gates (push) Failing after 3h11m55s

This commit is contained in:
2026-08-22 16:45:15 +02:00
parent 17422fac21
commit 648d9b4496
89 changed files with 7222 additions and 4239 deletions
+225
View File
@@ -16,6 +16,7 @@
//! opaque, matching `src/platform/tls_server.zig`'s Mbed TLS approach.
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const log = std.log.scoped(.db);
@@ -844,6 +845,137 @@ pub const Tx = struct {
}
};
/// A read transaction: one consistent view of the file across several
/// statements.
///
/// `BEGIN DEFERRED`, not `Tx`'s `BEGIN IMMEDIATE`. A reader that took the write
/// lock would stall the logger and retention for the length of an HTTP
/// response; a deferred transaction that only ever reads never upgrades, so it
/// cannot hit the mid-way `SQLITE_BUSY` the `Tx` doc warns about. In WAL mode it
/// pins the snapshot at the first read, which is the point: an aggregate and the
/// coverage watermark beside it describe the same database state even when
/// retention prunes between them.
///
/// Ending it is fallible, and the caller must treat it that way. A connection
/// left inside a transaction refuses the next `BEGIN`, so a swallowed failure
/// here does not cost one response — it costs every later one on the same
/// connection, and they would each be a correct-looking answer over a foreign
/// snapshot or an outright error the client was never told about.
///
/// ```zig
/// var tx = try ReadTx.begin(db);
/// errdefer tx.rollback();
/// ... // reads only
/// try tx.commit();
/// ```
pub const ReadTx = struct {
db: *Db,
active: bool,
pub fn begin(db: *Db) Error!ReadTx {
try db.exec("BEGIN DEFERRED;");
return .{ .db = db, .active = true };
}
/// Ends the transaction, and says so. A read transaction has nothing to
/// conflict over, so a failed COMMIT means the connection is in a state
/// this code did not put it in: the ROLLBACK below is the attempt to hand
/// the next caller a usable connection anyway, and the error is returned so
/// the response it was serving fails rather than reporting success over a
/// database whose state nobody can name.
pub fn commit(self: *ReadTx) Error!void {
assert(self.active);
self.active = false;
self.execCommit() catch |err| {
self.reportFault("COMMIT");
self.forceRollback();
return err;
};
}
fn execCommit(self: *ReadTx) Error!void {
if (commitFaultTripped()) return error.Internal;
return self.db.exec("COMMIT;");
}
/// Safe in `errdefer` and after `commit`. Never returns an error: it runs
/// on the path where something has already gone wrong, and that error is
/// the one worth reporting.
pub fn rollback(self: *ReadTx) void {
if (!self.active) return;
self.active = false;
self.forceRollback();
}
/// A ROLLBACK that fails leaves the connection inside a transaction with no
/// way left to get it out. Every later `begin` on it fails, which is the
/// visible symptom this reports the cause of.
fn forceRollback(self: *ReadTx) void {
self.db.exec("ROLLBACK;") catch {
self.reportFault("ROLLBACK");
};
}
/// The only record that this connection may be unusable, so in a real build
/// it is `err`. A test that deliberately causes the fault captures it
/// instead — see `read_tx_faults`.
fn reportFault(self: *ReadTx, comptime what: []const u8) void {
var buf: [256]u8 = undefined;
const message = self.db.lastError(&buf);
if (faultCaptured()) return;
log.err("read-transaction " ++ what ++ " failed: {s}", .{message});
}
};
/// Drives and observes the read-transaction teardown faults, which a unit test
/// cannot arrange against a healthy SQLite connection. Test builds only; it
/// reduces to nothing everywhere else — the rotation seam's shape
/// (logging.zig).
const read_tx_seam = if (builtin.is_test) struct {
var fail_next_commit: bool = false;
var capturing: bool = false;
var faults: usize = 0;
} else struct {};
fn commitFaultTripped() bool {
if (!builtin.is_test) return false;
if (!read_tx_seam.fail_next_commit) return false;
read_tx_seam.fail_next_commit = false;
return true;
}
/// True when a test has said it is expecting this fault and will assert on it.
/// Capture is opt-in for exactly one reason: the test runner fails a test that
/// logs at `err`, so a blanket silence would turn an unexpected teardown fault
/// in some unrelated test into a silent pass.
fn faultCaptured() bool {
if (!builtin.is_test) return false;
if (!read_tx_seam.capturing) return false;
read_tx_seam.faults += 1;
return true;
}
/// The seam's controls, for tests in this file and in the web layer.
pub const read_tx_faults = if (builtin.is_test) struct {
/// Fails the next `ReadTx.commit` before it issues COMMIT, so the
/// transaction is still open when the recovery path runs — which is the
/// shape of a real COMMIT failure.
pub fn failNextCommit() void {
read_tx_seam.fail_next_commit = true;
}
pub fn beginCapture() void {
read_tx_seam.capturing = true;
read_tx_seam.faults = 0;
}
/// Stops capturing and answers how many faults were reported meanwhile.
pub fn endCapture() usize {
read_tx_seam.capturing = false;
return read_tx_seam.faults;
}
} else struct {};
const testing = std.testing;
fn openMemory() Error!Db {
@@ -1029,6 +1161,99 @@ test "rollback after commit is a no-op" {
try testing.expectEqual(@as(i64, 1), try db.queryInt("SELECT count(*) FROM t"));
}
test "a read transaction sees one state and frees the connection when it commits" {
var db = try openMemory();
defer db.close();
try applyPragmas(&db, .{});
try db.exec("CREATE TABLE t (id INTEGER PRIMARY KEY);");
try db.exec("INSERT INTO t (id) VALUES (1);");
var tx = try ReadTx.begin(&db);
try testing.expectEqual(@as(i64, 1), try db.queryInt("SELECT count(*) FROM t"));
try tx.commit();
try testing.expect(!tx.active);
// Committed, so the connection takes a fresh transaction again.
var second = try ReadTx.begin(&db);
second.rollback();
try testing.expect(!second.active);
// Idempotent, so an `errdefer` that survives a successful rollback is a
// no-op rather than a stray ROLLBACK against the next transaction.
second.rollback();
var third = try ReadTx.begin(&db);
try third.commit();
}
test "a connection left inside a transaction refuses the next begin" {
var db = try openMemory();
defer db.close();
try applyPragmas(&db, .{});
// The poisoned connection, reached the only way it can be: a transaction
// that was opened and never ended. This is what a swallowed COMMIT failure
// would leave behind, and the point is that it is *loud* — the next reader
// gets an error it must report, never a silent read outside a snapshot.
try db.exec("BEGIN DEFERRED;");
try testing.expectError(error.Unexpected, ReadTx.begin(&db));
// And it is recoverable: ending the stray transaction restores the
// connection, which is what `commit`'s rollback attempt is reaching for.
try db.exec("ROLLBACK;");
var tx = try ReadTx.begin(&db);
try tx.commit();
}
test "a failed commit is reported, rolled back, and leaves the connection usable" {
var db = try openMemory();
defer db.close();
try applyPragmas(&db, .{});
try db.exec("CREATE TABLE t (id INTEGER PRIMARY KEY);");
try db.exec("INSERT INTO t (id) VALUES (1);");
read_tx_faults.beginCapture();
defer _ = read_tx_faults.endCapture();
var tx = try ReadTx.begin(&db);
try testing.expectEqual(@as(i64, 1), try db.queryInt("SELECT count(*) FROM t"));
// The failure lands where a real one does: the transaction is still open
// when the recovery path runs.
read_tx_faults.failNextCommit();
try testing.expectError(error.Internal, tx.commit());
try testing.expect(!tx.active);
// Exactly one fault: the COMMIT. The ROLLBACK behind it succeeded, which is
// the whole point of attempting it.
try testing.expectEqual(@as(usize, 1), read_tx_faults.endCapture());
// The connection is not poisoned — the next reader gets a transaction
// rather than inheriting the fault.
read_tx_faults.beginCapture();
var next = try ReadTx.begin(&db);
try testing.expectEqual(@as(i64, 1), try db.queryInt("SELECT count(*) FROM t"));
try next.commit();
try testing.expectEqual(@as(usize, 0), read_tx_faults.endCapture());
}
test "an armed commit fault fires once and no further" {
var db = try openMemory();
defer db.close();
try applyPragmas(&db, .{});
read_tx_faults.beginCapture();
defer _ = read_tx_faults.endCapture();
read_tx_faults.failNextCommit();
var first = try ReadTx.begin(&db);
try testing.expectError(error.Internal, first.commit());
// The seam disarms itself, so it cannot leak into a later test in the same
// binary and fail a commit nobody asked to fail.
var second = try ReadTx.begin(&db);
try second.commit();
try testing.expectEqual(@as(usize, 1), read_tx_faults.endCapture());
}
test "a row-producing statement reports its row through step" {
var db = try openMemory();
defer db.close();