db-mode config changes apply live in-process
Gates / frontend (push) Successful in 1m43s
Gates / test (push) Successful in 2m14s
Gates / test-aarch64 (push) Successful in 8m3s
Gates / package (push) Successful in 5m42s
Gates / container (push) Successful in 54s
CI / gates (push) Successful in 50m24s

settings and upstream writes now follow a prepare, commit, publish, retire
contract: candidates are built and validated before the database transaction,
published as infallible pointer swaps, and old generations retire after their
readers drain. per-query policy values snapshot once per query; upstream pool,
cache, rate limiter, sessions, api limiter, log sink, blocklist scheduler and
the query-log queue each gained one named live operation. restart_required
shrinks from every scalar key to the bind keys and web.enabled; the admin ui
drops its restart notices for everything else. file mode is unchanged.
This commit is contained in:
2026-08-24 00:04:28 +02:00
parent f7f4c8be09
commit ce143d1d87
47 changed files with 7698 additions and 926 deletions
+77 -17
View File
@@ -14,7 +14,6 @@ const std = @import("std");
const db = @import("db.zig");
const disk_monitor = @import("disk_monitor.zig");
const events = @import("events.zig");
const model = @import("../config/model.zig");
const queries_repo = @import("repositories/queries_repo.zig");
const log = std.log.scoped(.retention);
@@ -50,15 +49,42 @@ const Counters = struct {
vacuums_gated: std.atomic.Value(u64) = .init(0),
};
/// `logging.retention_days`, shared by its two consumers — this pass and
/// `server/clients.zig`'s stale-client prune. One cell rather than a copy in
/// each: the two must never prune to different cutoffs, and a settings apply
/// stores once. Owned by app-level state and outlives both readers.
///
/// `.monotonic` is enough: the value stands alone and orders nothing else, and
/// each consumer reads it once per pass.
pub const RetentionDays = struct {
value: std.atomic.Value(u32),
pub fn init(days: u16) RetentionDays {
return .{ .value = .init(days) };
}
pub fn get(self: *const RetentionDays) u32 {
return self.value.load(.monotonic);
}
pub fn setRetentionDays(self: *RetentionDays, days: u16) void {
self.value.store(days, .monotonic);
}
pub fn seconds(self: *const RetentionDays) i64 {
return @as(i64, self.get()) * std.time.s_per_day;
}
};
pub const Retention = struct {
cfg: model.Logging,
days: *const RetentionDays,
counters: Counters,
/// Passes since the last vacuum that succeeded. Plain rather than atomic:
/// only the retention task reads or writes it, and no consumer reports it.
passes_since_vacuum: u32,
pub fn init(cfg: model.Logging) Retention {
return .{ .cfg = cfg, .counters = .{}, .passes_since_vacuum = 0 };
pub fn init(days: *const RetentionDays) Retention {
return .{ .days = days, .counters = .{}, .passes_since_vacuum = 0 };
}
/// The counters, read one at a time. A scrape that lands mid-pass can see a
@@ -100,7 +126,7 @@ pub const Retention = struct {
) void {
add(&self.counters.passes, 1);
const now = std.Io.Clock.real.now(io).toSeconds();
const cutoff = now - model.retentionSeconds(self.cfg);
const cutoff = now - self.days.seconds();
// Diagnostics retention rides this pass rather than a schedule of its
// own: one daily housekeeping task, and a box restarted every night
@@ -268,7 +294,8 @@ test "a pass prunes the rows past the retention window and keeps the rest" {
const day = 86_400;
try writeRows(&database, &.{ now - 40 * day, now - 31 * day, now - 29 * day, now - 60 });
var retention: Retention = .init(.{ .retention_days = 30 });
var retention_days: RetentionDays = .init(30);
var retention: Retention = .init(&retention_days);
retention.runOnce(io, &database, null, null);
try testing.expectEqual(@as(i64, 2), try queries_repo.countRows(&database));
@@ -292,17 +319,41 @@ test "the cutoff follows retention_days" {
// window of the other.
try writeRows(&database, &.{now - 3 * day});
var keeps: Retention = .init(.{ .retention_days = 7 });
var keeps_days: RetentionDays = .init(7);
var keeps: Retention = .init(&keeps_days);
keeps.runOnce(io, &database, null, null);
try testing.expectEqual(@as(i64, 1), try queries_repo.countRows(&database));
try testing.expectEqual(@as(u64, 0), keeps.snapshotStats().rows_pruned);
var prunes: Retention = .init(.{ .retention_days = 1 });
var prunes_days: RetentionDays = .init(1);
var prunes: Retention = .init(&prunes_days);
prunes.runOnce(io, &database, null, null);
try testing.expectEqual(@as(i64, 0), try queries_repo.countRows(&database));
try testing.expectEqual(@as(u64, 1), prunes.snapshotStats().rows_pruned);
}
test "setRetentionDays changes the cutoff the next pass prunes by" {
var threaded: std.Io.Threaded = .init(testing.allocator, .{});
defer threaded.deinit();
const io = threaded.io();
var database = try openLog();
defer database.close();
const now = std.Io.Clock.real.now(io).toSeconds();
try writeRows(&database, &.{now - 3 * 86_400});
var days: RetentionDays = .init(7);
var pass: Retention = .init(&days);
pass.runOnce(io, &database, null, null);
try testing.expectEqual(@as(i64, 1), try queries_repo.countRows(&database));
days.setRetentionDays(1);
pass.runOnce(io, &database, null, null);
try testing.expectEqual(@as(i64, 0), try queries_repo.countRows(&database));
try testing.expectEqual(@as(u64, 1), pass.snapshotStats().rows_pruned);
}
test "the seventh pass vacuums and the six before it do not" {
var threaded: std.Io.Threaded = .init(testing.allocator, .{});
defer threaded.deinit();
@@ -311,7 +362,8 @@ test "the seventh pass vacuums and the six before it do not" {
var database = try openLog();
defer database.close();
var retention: Retention = .init(.{});
var retention_days: RetentionDays = .init(30);
var retention: Retention = .init(&retention_days);
for (0..6) |_| {
retention.runOnce(io, &database, null, null);
try testing.expectEqual(@as(u64, 0), retention.snapshotStats().vacuums);
@@ -340,7 +392,8 @@ test "a gated pass skips the vacuum, counts it, and vacuums on the next pass" {
var monitor: disk_monitor.Monitor = .init(.{}, std.Io.Dir.cwd(), ".", null);
monitor.state_raw.store(@intFromEnum(disk_monitor.State.critical), .monotonic);
var gated: Retention = .init(.{});
var gated_days: RetentionDays = .init(30);
var gated: Retention = .init(&gated_days);
for (0..vacuum_every_passes) |_| gated.runOnce(io, &database, &monitor, null);
// Prune and checkpoint ran on every pass; only the vacuum was refused.
@@ -371,7 +424,8 @@ test "a warn state still allows the vacuum" {
var monitor: disk_monitor.Monitor = .init(.{}, std.Io.Dir.cwd(), ".", null);
monitor.state_raw.store(@intFromEnum(disk_monitor.State.warn), .monotonic);
var retention: Retention = .init(.{});
var retention_days: RetentionDays = .init(30);
var retention: Retention = .init(&retention_days);
for (0..vacuum_every_passes) |_| retention.runOnce(io, &database, &monitor, null);
try testing.expectEqual(@as(u64, 1), retention.snapshotStats().vacuums);
@@ -386,7 +440,8 @@ test "a pass over an empty database still counts" {
var database = try openLog();
defer database.close();
var retention: Retention = .init(.{});
var retention_days: RetentionDays = .init(30);
var retention: Retention = .init(&retention_days);
retention.runOnce(io, &database, null, null);
try testing.expectEqual(@as(u64, 1), retention.snapshotStats().passes);
@@ -410,7 +465,8 @@ test "a failing prune counts the pass and leaves the rows alone" {
\\BEGIN SELECT RAISE(ABORT, 'refused'); END;
);
var retention: Retention = .init(.{ .retention_days = 30 });
var retention_days: RetentionDays = .init(30);
var retention: Retention = .init(&retention_days);
retention.runOnce(io, &database, null, null);
try testing.expectEqual(@as(i64, 1), try queries_repo.countRows(&database));
@@ -435,7 +491,8 @@ test "the next pass retries what the failed one could not do" {
\\BEGIN SELECT RAISE(ABORT, 'refused'); END;
);
var retention: Retention = .init(.{ .retention_days = 30 });
var retention_days: RetentionDays = .init(30);
var retention: Retention = .init(&retention_days);
retention.runOnce(io, &database, null, null);
try testing.expectEqual(@as(i64, 2), try queries_repo.countRows(&database));
@@ -466,7 +523,8 @@ test "a failing prune opens a maintenance episode the next clean pass closes" {
\\BEGIN SELECT RAISE(ABORT, 'refused'); END;
);
var retention: Retention = .init(.{});
var retention_days: RetentionDays = .init(30);
var retention: Retention = .init(&retention_days);
retention.runOnce(io, &database, null, &fx.store);
// Only the prune failed; the checkpoint succeeded, and a success writes no
@@ -501,7 +559,8 @@ test "a gated vacuum is a maintenance failure the next ungated pass closes" {
var monitor: disk_monitor.Monitor = .init(.{}, std.Io.Dir.cwd(), ".", null);
monitor.state_raw.store(@intFromEnum(disk_monitor.State.critical), .monotonic);
var retention: Retention = .init(.{});
var retention_days: RetentionDays = .init(30);
var retention: Retention = .init(&retention_days);
for (0..vacuum_every_passes) |_| retention.runOnce(io, &database, &monitor, &fx.store);
try testing.expectEqualStrings("vacuum", try fx.text(
@@ -536,7 +595,8 @@ test "a pass prunes the diagnostics store once" {
fx.store.reportResolved(io, stale, .query_log_recreated, "one-shot", "one-shot", .warning, "aside kept");
try testing.expectEqual(@as(i64, 1), try fx.count("SELECT count(*) FROM operational_events"));
var retention: Retention = .init(.{});
var retention_days: RetentionDays = .init(30);
var retention: Retention = .init(&retention_days);
retention.runOnce(io, &database, null, &fx.store);
try testing.expectEqual(@as(i64, 0), try fx.count("SELECT count(*) FROM operational_events"));