milestone 26: upstream health answers for the selected period
This commit is contained in:
+100
-1
@@ -15,6 +15,7 @@ const db = @import("db.zig");
|
||||
const disk_monitor = @import("disk_monitor.zig");
|
||||
const model = @import("../config/model.zig");
|
||||
const queries_repo = @import("repositories/queries_repo.zig");
|
||||
const upstream_history_repo = @import("repositories/upstream_history_repo.zig");
|
||||
|
||||
const log = std.log.scoped(.retention);
|
||||
|
||||
@@ -31,6 +32,10 @@ pub const pass_interval_s = 86_400;
|
||||
pub const Stats = struct {
|
||||
passes: u64 = 0,
|
||||
rows_pruned: u64 = 0,
|
||||
/// Upstream-history minute rows, counted apart from `rows_pruned`: that
|
||||
/// counter is the query log's, and an operator watching it must not see it
|
||||
/// move because a different table was tidied.
|
||||
upstream_rows_pruned: u64 = 0,
|
||||
checkpoints: u64 = 0,
|
||||
vacuums: u64 = 0,
|
||||
/// Vacuums the disk monitor refused. The pass still pruned and
|
||||
@@ -44,6 +49,7 @@ pub const Stats = struct {
|
||||
const Counters = struct {
|
||||
passes: std.atomic.Value(u64) = .init(0),
|
||||
rows_pruned: std.atomic.Value(u64) = .init(0),
|
||||
upstream_rows_pruned: std.atomic.Value(u64) = .init(0),
|
||||
checkpoints: std.atomic.Value(u64) = .init(0),
|
||||
vacuums: std.atomic.Value(u64) = .init(0),
|
||||
vacuums_gated: std.atomic.Value(u64) = .init(0),
|
||||
@@ -67,6 +73,7 @@ pub const Retention = struct {
|
||||
return .{
|
||||
.passes = self.counters.passes.load(.monotonic),
|
||||
.rows_pruned = self.counters.rows_pruned.load(.monotonic),
|
||||
.upstream_rows_pruned = self.counters.upstream_rows_pruned.load(.monotonic),
|
||||
.checkpoints = self.counters.checkpoints.load(.monotonic),
|
||||
.vacuums = self.counters.vacuums.load(.monotonic),
|
||||
.vacuums_gated = self.counters.vacuums_gated.load(.monotonic),
|
||||
@@ -97,7 +104,8 @@ pub const Retention = struct {
|
||||
monitor: ?*disk_monitor.Monitor,
|
||||
) void {
|
||||
add(&self.counters.passes, 1);
|
||||
const cutoff = std.Io.Clock.real.now(io).toSeconds() - model.retentionSeconds(self.cfg);
|
||||
const now = std.Io.Clock.real.now(io).toSeconds();
|
||||
const cutoff = now - model.retentionSeconds(self.cfg);
|
||||
|
||||
if (queries_repo.pruneOlderThan(database, cutoff)) |deleted| {
|
||||
add(&self.counters.rows_pruned, @intCast(deleted));
|
||||
@@ -105,6 +113,20 @@ pub const Retention = struct {
|
||||
log.warn("retention prune before {d} failed: {s}", .{ cutoff, @errorName(err) });
|
||||
}
|
||||
|
||||
// Before the vacuum-cadence logic below, which returns early on six
|
||||
// passes out of seven and again whenever the monitor refuses the
|
||||
// vacuum. A step placed after it would almost never run.
|
||||
//
|
||||
// `logging.retention_days` is not the window here: these are per-minute
|
||||
// aggregates whose whole purpose is to outlive the per-query rows, so
|
||||
// the window is the repository's own constant.
|
||||
const history_cutoff = now - upstream_history_repo.retention_window_s;
|
||||
if (upstream_history_repo.pruneOlderThan(database, history_cutoff)) |deleted| {
|
||||
add(&self.counters.upstream_rows_pruned, @intCast(deleted));
|
||||
} else |err| {
|
||||
log.warn("upstream history prune before {d} failed: {s}", .{ history_cutoff, @errorName(err) });
|
||||
}
|
||||
|
||||
if (queries_repo.checkpointTruncate(database)) {
|
||||
add(&self.counters.checkpoints, 1);
|
||||
} else |err| {
|
||||
@@ -372,6 +394,83 @@ test "a failing prune counts the pass and leaves the rows alone" {
|
||||
try testing.expectEqual(@as(u64, 1), retention.snapshotStats().checkpoints);
|
||||
}
|
||||
|
||||
test "the upstream-history window is its own, and a one-day query log does not shrink it" {
|
||||
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();
|
||||
const day = 86_400;
|
||||
try writeRows(&database, &.{ now - 2 * day, now - 60 });
|
||||
try upstream_history_repo.flush(&database, &.{
|
||||
.{ .url = "https://gone.example", .minute_ts = now - 32 * day, .successes = 1, .failures = 0, .last_failure_ts = null, .last_error = "" },
|
||||
.{ .url = "https://kept.example", .minute_ts = now - 29 * day, .successes = 1, .failures = 0, .last_failure_ts = null, .last_error = "" },
|
||||
});
|
||||
|
||||
// A query log kept for one day, and 30 days of upstream minutes beside it.
|
||||
var retention: Retention = .init(.{ .retention_days = 1 });
|
||||
retention.runOnce(io, &database, null);
|
||||
|
||||
const stats = retention.snapshotStats();
|
||||
// One query-log row is older than one day; one minute row is older than the
|
||||
// fixed 31-day upstream window. Each counter moved by its own amount.
|
||||
try testing.expectEqual(@as(u64, 1), stats.rows_pruned);
|
||||
try testing.expectEqual(@as(u64, 1), stats.upstream_rows_pruned);
|
||||
try testing.expectEqual(@as(i64, 1), try upstream_history_repo.countMinutes(&database));
|
||||
// The day-29 row is exactly what a 30-day dashboard window asks for.
|
||||
const kept = try upstream_history_repo.windowStats(
|
||||
&database,
|
||||
"https://kept.example",
|
||||
now - 30 * day,
|
||||
now,
|
||||
);
|
||||
try testing.expectEqual(@as(u64, 1), kept.attempts);
|
||||
// And the emptied target went with its rows.
|
||||
try testing.expectEqual(@as(i64, 1), try upstream_history_repo.countTargets(&database));
|
||||
}
|
||||
|
||||
test "the history prune runs on the passes where the vacuum logic returns early" {
|
||||
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();
|
||||
const old_minute = now - 40 * 86_400;
|
||||
|
||||
// The first pass: `passes_since_vacuum` is 1, so the vacuum block returns
|
||||
// before it does anything. A prune placed after that block would never run
|
||||
// on six passes out of seven.
|
||||
try upstream_history_repo.flush(&database, &.{
|
||||
.{ .url = "https://a.example", .minute_ts = old_minute, .successes = 1, .failures = 0, .last_failure_ts = null, .last_error = "" },
|
||||
});
|
||||
var early: Retention = .init(.{});
|
||||
early.runOnce(io, &database, null);
|
||||
try testing.expectEqual(@as(u64, 1), early.snapshotStats().upstream_rows_pruned);
|
||||
try testing.expectEqual(@as(i64, 0), try upstream_history_repo.countMinutes(&database));
|
||||
|
||||
// The gated pass: the disk monitor refuses the vacuum and that branch
|
||||
// returns too, and the prune still has to have happened before it.
|
||||
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(.{});
|
||||
for (0..vacuum_every_passes - 1) |_| gated.runOnce(io, &database, &monitor);
|
||||
try upstream_history_repo.flush(&database, &.{
|
||||
.{ .url = "https://b.example", .minute_ts = old_minute, .successes = 1, .failures = 0, .last_failure_ts = null, .last_error = "" },
|
||||
});
|
||||
gated.runOnce(io, &database, &monitor);
|
||||
|
||||
try testing.expectEqual(@as(u64, 1), gated.snapshotStats().vacuums_gated);
|
||||
try testing.expectEqual(@as(u64, 1), gated.snapshotStats().upstream_rows_pruned);
|
||||
try testing.expectEqual(@as(i64, 0), try upstream_history_repo.countMinutes(&database));
|
||||
}
|
||||
|
||||
test "the next pass retries what the failed one could not do" {
|
||||
var threaded: std.Io.Threaded = .init(testing.allocator, .{});
|
||||
defer threaded.deinit();
|
||||
|
||||
Reference in New Issue
Block a user