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
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:
+77
-17
@@ -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"));
|
||||
|
||||
Reference in New Issue
Block a user