milestone 8: web server, rest api, sse, auth, metrics and static assets
This commit is contained in:
+57
-27
@@ -26,6 +26,7 @@ pub const vacuum_every_passes = 7;
|
||||
/// expresses, so a finer schedule would prune nothing new.
|
||||
pub const pass_interval_s = 86_400;
|
||||
|
||||
/// A consistent copy of the counters, for `/metrics` and the health rollup.
|
||||
pub const Stats = struct {
|
||||
passes: u64 = 0,
|
||||
rows_pruned: u64 = 0,
|
||||
@@ -33,12 +34,34 @@ pub const Stats = struct {
|
||||
vacuums: u64 = 0,
|
||||
};
|
||||
|
||||
/// The live counters. Atomic because the retention task writes them and the web
|
||||
/// task reads them, on different threads, with no lock between the two — the
|
||||
/// same shape the query logger uses for its own counters.
|
||||
const Counters = struct {
|
||||
passes: std.atomic.Value(u64) = .init(0),
|
||||
rows_pruned: std.atomic.Value(u64) = .init(0),
|
||||
checkpoints: std.atomic.Value(u64) = .init(0),
|
||||
vacuums: std.atomic.Value(u64) = .init(0),
|
||||
};
|
||||
|
||||
pub const Retention = struct {
|
||||
cfg: model.Logging,
|
||||
stats: Stats,
|
||||
counters: Counters,
|
||||
|
||||
pub fn init(cfg: model.Logging) Retention {
|
||||
return .{ .cfg = cfg, .stats = .{} };
|
||||
return .{ .cfg = cfg, .counters = .{} };
|
||||
}
|
||||
|
||||
/// The four counters, read one at a time. A scrape that lands mid-pass can
|
||||
/// see a pass counted before the rows it pruned are; the alternative is a
|
||||
/// lock on the pass itself, which buys a consistency no consumer needs.
|
||||
pub fn snapshotStats(self: *const Retention) Stats {
|
||||
return .{
|
||||
.passes = self.counters.passes.load(.monotonic),
|
||||
.rows_pruned = self.counters.rows_pruned.load(.monotonic),
|
||||
.checkpoints = self.counters.checkpoints.load(.monotonic),
|
||||
.vacuums = self.counters.vacuums.load(.monotonic),
|
||||
};
|
||||
}
|
||||
|
||||
/// One pass: prune, checkpoint, and on every seventh pass vacuum.
|
||||
@@ -53,29 +76,36 @@ pub const Retention = struct {
|
||||
///
|
||||
/// `database` must be a connection no other task uses; see `run`.
|
||||
pub fn runOnce(self: *Retention, io: std.Io, database: *db.Db) void {
|
||||
self.stats.passes += 1;
|
||||
const pass = add(&self.counters.passes, 1) + 1;
|
||||
const cutoff = std.Io.Clock.real.now(io).toSeconds() - model.retentionSeconds(self.cfg);
|
||||
|
||||
if (queries_repo.pruneOlderThan(database, cutoff)) |deleted| {
|
||||
self.stats.rows_pruned += @intCast(deleted);
|
||||
_ = add(&self.counters.rows_pruned, @intCast(deleted));
|
||||
} else |err| {
|
||||
log.warn("retention prune before {d} failed: {s}", .{ cutoff, @errorName(err) });
|
||||
}
|
||||
|
||||
if (queries_repo.checkpointTruncate(database)) {
|
||||
self.stats.checkpoints += 1;
|
||||
_ = add(&self.counters.checkpoints, 1);
|
||||
} else |err| {
|
||||
log.warn("retention checkpoint failed: {s}", .{@errorName(err)});
|
||||
}
|
||||
|
||||
if (self.stats.passes % vacuum_every_passes != 0) return;
|
||||
if (pass % vacuum_every_passes != 0) return;
|
||||
if (queries_repo.vacuum(database)) {
|
||||
self.stats.vacuums += 1;
|
||||
_ = add(&self.counters.vacuums, 1);
|
||||
} else |err| {
|
||||
log.warn("retention vacuum failed: {s}", .{@errorName(err)});
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the value before the addition, which is what the pass counter
|
||||
/// needs: only this task increments it, so `previous + 1` is this pass's
|
||||
/// number.
|
||||
fn add(counter: *std.atomic.Value(u64), delta: u64) u64 {
|
||||
return counter.fetchAdd(delta, .monotonic);
|
||||
}
|
||||
|
||||
/// Daily loop, first pass immediately. Phase 7 starts it.
|
||||
///
|
||||
/// `boot` rather than `awake`: a box that suspends overnight must still see
|
||||
@@ -159,10 +189,10 @@ test "a pass prunes the rows past the retention window and keeps the rest" {
|
||||
retention.runOnce(io, &database);
|
||||
|
||||
try testing.expectEqual(@as(i64, 2), try queries_repo.countRows(&database));
|
||||
try testing.expectEqual(@as(u64, 1), retention.stats.passes);
|
||||
try testing.expectEqual(@as(u64, 2), retention.stats.rows_pruned);
|
||||
try testing.expectEqual(@as(u64, 1), retention.stats.checkpoints);
|
||||
try testing.expectEqual(@as(u64, 0), retention.stats.vacuums);
|
||||
try testing.expectEqual(@as(u64, 1), retention.snapshotStats().passes);
|
||||
try testing.expectEqual(@as(u64, 2), retention.snapshotStats().rows_pruned);
|
||||
try testing.expectEqual(@as(u64, 1), retention.snapshotStats().checkpoints);
|
||||
try testing.expectEqual(@as(u64, 0), retention.snapshotStats().vacuums);
|
||||
}
|
||||
|
||||
test "the cutoff follows retention_days" {
|
||||
@@ -182,12 +212,12 @@ test "the cutoff follows retention_days" {
|
||||
var keeps: Retention = .init(.{ .retention_days = 7 });
|
||||
keeps.runOnce(io, &database);
|
||||
try testing.expectEqual(@as(i64, 1), try queries_repo.countRows(&database));
|
||||
try testing.expectEqual(@as(u64, 0), keeps.stats.rows_pruned);
|
||||
try testing.expectEqual(@as(u64, 0), keeps.snapshotStats().rows_pruned);
|
||||
|
||||
var prunes: Retention = .init(.{ .retention_days = 1 });
|
||||
prunes.runOnce(io, &database);
|
||||
try testing.expectEqual(@as(i64, 0), try queries_repo.countRows(&database));
|
||||
try testing.expectEqual(@as(u64, 1), prunes.stats.rows_pruned);
|
||||
try testing.expectEqual(@as(u64, 1), prunes.snapshotStats().rows_pruned);
|
||||
}
|
||||
|
||||
test "the seventh pass vacuums and the six before it do not" {
|
||||
@@ -201,17 +231,17 @@ test "the seventh pass vacuums and the six before it do not" {
|
||||
var retention: Retention = .init(.{});
|
||||
for (0..6) |_| {
|
||||
retention.runOnce(io, &database);
|
||||
try testing.expectEqual(@as(u64, 0), retention.stats.vacuums);
|
||||
try testing.expectEqual(@as(u64, 0), retention.snapshotStats().vacuums);
|
||||
}
|
||||
retention.runOnce(io, &database);
|
||||
|
||||
try testing.expectEqual(@as(u64, 7), retention.stats.passes);
|
||||
try testing.expectEqual(@as(u64, 1), retention.stats.vacuums);
|
||||
try testing.expectEqual(@as(u64, 7), retention.stats.checkpoints);
|
||||
try testing.expectEqual(@as(u64, 7), retention.snapshotStats().passes);
|
||||
try testing.expectEqual(@as(u64, 1), retention.snapshotStats().vacuums);
|
||||
try testing.expectEqual(@as(u64, 7), retention.snapshotStats().checkpoints);
|
||||
|
||||
for (0..7) |_| retention.runOnce(io, &database);
|
||||
try testing.expectEqual(@as(u64, 14), retention.stats.passes);
|
||||
try testing.expectEqual(@as(u64, 2), retention.stats.vacuums);
|
||||
try testing.expectEqual(@as(u64, 14), retention.snapshotStats().passes);
|
||||
try testing.expectEqual(@as(u64, 2), retention.snapshotStats().vacuums);
|
||||
}
|
||||
|
||||
test "a pass over an empty database still counts" {
|
||||
@@ -225,9 +255,9 @@ test "a pass over an empty database still counts" {
|
||||
var retention: Retention = .init(.{});
|
||||
retention.runOnce(io, &database);
|
||||
|
||||
try testing.expectEqual(@as(u64, 1), retention.stats.passes);
|
||||
try testing.expectEqual(@as(u64, 0), retention.stats.rows_pruned);
|
||||
try testing.expectEqual(@as(u64, 1), retention.stats.checkpoints);
|
||||
try testing.expectEqual(@as(u64, 1), retention.snapshotStats().passes);
|
||||
try testing.expectEqual(@as(u64, 0), retention.snapshotStats().rows_pruned);
|
||||
try testing.expectEqual(@as(u64, 1), retention.snapshotStats().checkpoints);
|
||||
try testing.expectEqual(@as(i64, 0), try queries_repo.countRows(&database));
|
||||
}
|
||||
|
||||
@@ -250,10 +280,10 @@ test "a failing prune counts the pass and leaves the rows alone" {
|
||||
retention.runOnce(io, &database);
|
||||
|
||||
try testing.expectEqual(@as(i64, 1), try queries_repo.countRows(&database));
|
||||
try testing.expectEqual(@as(u64, 1), retention.stats.passes);
|
||||
try testing.expectEqual(@as(u64, 0), retention.stats.rows_pruned);
|
||||
try testing.expectEqual(@as(u64, 1), retention.snapshotStats().passes);
|
||||
try testing.expectEqual(@as(u64, 0), retention.snapshotStats().rows_pruned);
|
||||
// The checkpoint runs whether or not the prune did.
|
||||
try testing.expectEqual(@as(u64, 1), retention.stats.checkpoints);
|
||||
try testing.expectEqual(@as(u64, 1), retention.snapshotStats().checkpoints);
|
||||
}
|
||||
|
||||
test "the next pass retries what the failed one could not do" {
|
||||
@@ -279,6 +309,6 @@ test "the next pass retries what the failed one could not do" {
|
||||
retention.runOnce(io, &database);
|
||||
|
||||
try testing.expectEqual(@as(i64, 0), try queries_repo.countRows(&database));
|
||||
try testing.expectEqual(@as(u64, 2), retention.stats.passes);
|
||||
try testing.expectEqual(@as(u64, 2), retention.stats.rows_pruned);
|
||||
try testing.expectEqual(@as(u64, 2), retention.snapshotStats().passes);
|
||||
try testing.expectEqual(@as(u64, 2), retention.snapshotStats().rows_pruned);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user