query log batching: one transaction per flush interval, not per query
Gates / frontend (push) Successful in 1m18s
Gates / test (push) Successful in 2m46s
Gates / test-aarch64 (push) Successful in 7m33s
Gates / package (push) Successful in 5m34s
Gates / container (push) Successful in 17s
CI / gates (push) Successful in 16m16s
Gates / frontend (push) Successful in 1m8s
Gates / container (push) Successful in 9s
Release / gates (push) Successful in 9m15s
Release / guard (push) Successful in 19s
Gates / test (push) Successful in 1m34s
Gates / test-aarch64 (push) Successful in 6m46s
Gates / package (push) Successful in 39s
Release / publish (push) Failing after 4m7s

This commit is contained in:
2026-08-20 20:57:11 +02:00
parent 037f209179
commit addf24f92c
18 changed files with 422 additions and 61 deletions
+16 -7
View File
@@ -228,7 +228,11 @@ test "S8 case 2: a single entry reaches the file once the flush interval passes"
defer log_db.deinit();
var queue_buf: [8]logger.Entry = undefined;
var query_log: logger.Logger = .init(.{}, &queue_buf);
// A one-second window, not the 60-second default: this case is about the
// entry reaching the file on the interval alone, with nothing to close the
// queue for it.
const flush_interval_s = 1;
var query_log: logger.Logger = .init(.{ .query_log_flush_interval_s = flush_interval_s }, &queue_buf);
var future = try io.concurrent(logger.Logger.runWriter, .{
&query_log,
@@ -239,9 +243,9 @@ test "S8 case 2: a single entry reaches the file once the flush interval passes"
query_log.log(io, entryAt(1, "only.example"));
// Ten flush intervals of headroom: a row that has not landed by then is a
// Three flush intervals of headroom: a row that has not landed by then is a
// failure of the interval race, not a slow machine.
const limit = 10 * logger.flush_interval_ms / 5;
const limit = 3 * flush_interval_s * 1000 / 5;
try awaitCount(&query_log.rows_written, 1, limit);
try testing.expectEqual(@as(i64, 1), try queries_repo.countRows(log_db.database()));
@@ -268,7 +272,9 @@ test "S8 case 3: a full queue drops the oldest entries and the newest survive" {
monitor.state_raw.store(@intFromEnum(disk_monitor.State.critical), .monotonic);
var queue_buf: [8]logger.Entry = undefined;
var query_log: logger.Logger = .init(.{}, &queue_buf);
// Zero interval: this case is about the queue cap and the gate, so the
// writer takes what is queued and goes straight to the gate.
var query_log: logger.Logger = .init(.{ .query_log_flush_interval_s = 0 }, &queue_buf);
// The whole burst is enqueued before the writer starts. A writer already
// draining the queue would take entries out of it mid-burst and make the
@@ -286,8 +292,9 @@ test "S8 case 3: a full queue drops the oldest entries and the newest survive" {
try awaitCount(&query_log.batches_gated, 1, 200);
try testing.expectEqual(@as(i64, 0), try queries_repo.countRows(log_db.database()));
// Un-gate before the shutdown: a writer held by the disk gate holds its
// batch, and `shutdown` alone would never release it.
// Un-gate before the shutdown: a writer still held by the gate counts its
// batch as dropped rather than writing it, which is a different case (the
// logger's own suite covers it).
monitor.state_raw.store(@intFromEnum(disk_monitor.State.ok), .monotonic);
try awaitCount(&query_log.rows_written, 8, 300);
query_log.shutdown(io);
@@ -403,7 +410,9 @@ test "S8 case 6: a critical disk gates the flushes and recovery releases them" {
try testing.expect(monitor.gauges().db_bytes > 0);
var queue_buf: [64]logger.Entry = undefined;
var query_log: logger.Logger = .init(.{}, &queue_buf);
// Zero interval: the case is the gate's hold and release, so the writer
// takes the five queued entries straight to it.
var query_log: logger.Logger = .init(.{ .query_log_flush_interval_s = 0 }, &queue_buf);
for (0..5) |i| query_log.log(io, entryAt(@intCast(i), "gated.example"));
var future = try io.concurrent(logger.Logger.runWriter, .{