milestone 30: overview as a dashboard, explicit health contract, period aggregations
This commit is contained in:
+113
-10
@@ -20,9 +20,8 @@
|
||||
//! having no store.
|
||||
//!
|
||||
//! **Time is a parameter, not a seam.** Every method that stamps a row takes
|
||||
//! `now_s` (`purge`/`purgeAll` only delete, so they take none),
|
||||
//! matching `upstream/history.zig` and `storage/logger.zig`. Production callers
|
||||
//! read `Clock.real`; tests pass literals. There is no clock in here and no
|
||||
//! `now_s` (`purge`/`purgeAll` only delete, so they take none), matching
|
||||
//! `storage/logger.zig`. Production callers read `Clock.real`; tests pass literals. There is no clock in here and no
|
||||
//! function pointer standing in for one.
|
||||
//!
|
||||
//! **`resolve` is on the DNS hot path.** `pool.recordSuccess` calls it after
|
||||
@@ -39,9 +38,15 @@ const events_repo = @import("repositories/events_repo.zig");
|
||||
|
||||
const log = std.log.scoped(.events);
|
||||
|
||||
/// Every failure episode nxdns can record. **The enum is the truth**: its
|
||||
/// cardinality is what the exhaustive tests, the API and the frontend copy map
|
||||
/// all count, and the dotted string is only its wire form.
|
||||
/// Every failure episode nxdns can **emit**, and nothing else. **The enum is
|
||||
/// the truth** about what a running process writes: the exhaustive tests count
|
||||
/// it, and the dotted string is only its wire form.
|
||||
///
|
||||
/// It is not the whole documented union. The API and the frontend copy map hold
|
||||
/// these plus `legacy_wire_codes`, because a stored row outlives its producer —
|
||||
/// see there. A code whose producer is gone moves to that list rather than
|
||||
/// staying here: a member here is a code `Store.report` accepts, and a report
|
||||
/// under a dead code opens an episode nothing can ever resolve.
|
||||
pub const Code = enum {
|
||||
disk_space,
|
||||
disk_probe,
|
||||
@@ -52,7 +57,6 @@ pub const Code = enum {
|
||||
query_log_write,
|
||||
query_log_maintenance,
|
||||
query_log_recreated,
|
||||
upstream_history_write,
|
||||
upstream_exchange,
|
||||
client_names_storage,
|
||||
clients_storage,
|
||||
@@ -75,7 +79,6 @@ pub fn wire(code: Code) []const u8 {
|
||||
.query_log_write => "query_log.write",
|
||||
.query_log_maintenance => "query_log.maintenance",
|
||||
.query_log_recreated => "query_log.recreated",
|
||||
.upstream_history_write => "upstream_history.write",
|
||||
.upstream_exchange => "upstream.exchange",
|
||||
.client_names_storage => "client_names.storage",
|
||||
.clients_storage => "clients.storage",
|
||||
@@ -92,7 +95,6 @@ pub fn component(code: Code) []const u8 {
|
||||
.blocklist_refresh, .blocklist_snapshot, .blocklist_storage => "blocklist",
|
||||
.certificate_reload => "certificate",
|
||||
.query_log_write, .query_log_maintenance, .query_log_recreated => "query_log",
|
||||
.upstream_history_write => "upstream_history",
|
||||
.upstream_exchange => "upstream",
|
||||
.client_names_storage => "client_names",
|
||||
.clients_storage => "clients",
|
||||
@@ -101,6 +103,21 @@ pub fn component(code: Code) []const u8 {
|
||||
};
|
||||
}
|
||||
|
||||
/// Wire codes that stored rows still carry and no code emits.
|
||||
///
|
||||
/// They live as text, not as `Code` members, because the read path is the only
|
||||
/// path that meets them: the list endpoint passes a stored code straight
|
||||
/// through, and `events_repo.componentOf` derives the component from the text.
|
||||
/// The one thing the store must still do with them is close whatever a past
|
||||
/// release left open — see `Store.init`.
|
||||
///
|
||||
/// The documented event-code union is these plus every `Code`, which is what
|
||||
/// keeps a real response describing an old row inside the contract.
|
||||
pub const legacy_wire_codes = [_][]const u8{
|
||||
// Milestone 30 deleted the upstream-minute history subsystem.
|
||||
"upstream_history.write",
|
||||
};
|
||||
|
||||
/// Fixed per emit call, not per code: the same disk monitor reports a
|
||||
/// transition to `warn` as a warning and one to `critical` as an error.
|
||||
///
|
||||
@@ -198,6 +215,17 @@ pub const Store = struct {
|
||||
pub fn init(io: std.Io, database: *db.Db, now_s: i64) db.Error!Store {
|
||||
var store: Store = .{ .database = database };
|
||||
|
||||
// Before the mirror load and the prune, both of which would otherwise
|
||||
// read a state this is about to change: an episode left open here would
|
||||
// sit in the mirror forever and hold `untracked_active_count` above
|
||||
// zero, and one resolved after the prune would keep its row for another
|
||||
// ninety days. `now_s` is the resolution time on purpose — the store's
|
||||
// own open time, not the episode's stale `last_seen`, which a database
|
||||
// older than the retention window would prune in this very call.
|
||||
for (legacy_wire_codes) |code| {
|
||||
_ = try events_repo.resolveActiveByCode(database, now_s, code);
|
||||
}
|
||||
|
||||
var chunk: [16]events_repo.ActiveRow = undefined;
|
||||
var after: i64 = 0;
|
||||
while (store.active.len < mirror_capacity) {
|
||||
@@ -688,7 +716,7 @@ const testing = std.testing;
|
||||
|
||||
test "wire and component are exhaustive, unique and agree with each other" {
|
||||
const all = std.enums.values(Code);
|
||||
try testing.expectEqual(@as(usize, 15), all.len);
|
||||
try testing.expectEqual(@as(usize, 14), all.len);
|
||||
|
||||
for (all, 0..) |code, i| {
|
||||
const text = wire(code);
|
||||
@@ -722,6 +750,7 @@ const Fixture = struct {
|
||||
io: std.Io = undefined,
|
||||
database: db.Db = undefined,
|
||||
store: Store = undefined,
|
||||
text_buf: [256]u8 = undefined,
|
||||
|
||||
fn init(self: *Fixture, now_s: i64) !void {
|
||||
self.threaded = .init(testing.allocator, .{});
|
||||
@@ -744,8 +773,82 @@ const Fixture = struct {
|
||||
fn count(self: *Fixture, sql: []const u8) !i64 {
|
||||
return self.database.queryInt(sql);
|
||||
}
|
||||
|
||||
fn text(self: *Fixture, sql: []const u8) ![]const u8 {
|
||||
var stmt = try self.database.prepare(sql);
|
||||
defer stmt.deinit();
|
||||
if (!try stmt.step()) return error.NoRow;
|
||||
const value = stmt.columnText(0);
|
||||
@memcpy(self.text_buf[0..value.len], value);
|
||||
return self.text_buf[0..value.len];
|
||||
}
|
||||
};
|
||||
|
||||
test "an m29 flush-failure episode is resolved once at init and stays listable" {
|
||||
var fx: Fixture = .{};
|
||||
try fx.init(1000);
|
||||
defer fx.deinit();
|
||||
|
||||
// The state an m29 database is left in: a live episode under a code that no
|
||||
// producer emits any more, so nothing will ever resolve it. Seeded through
|
||||
// the repository and not through `report`, because the live emitter cannot
|
||||
// name this code — that it cannot is half of what m30 changed.
|
||||
_ = try events_repo.insertActive(
|
||||
&fx.database,
|
||||
1000,
|
||||
legacy_wire_codes[0],
|
||||
"history",
|
||||
"history",
|
||||
"warning",
|
||||
"Busy",
|
||||
);
|
||||
try testing.expectEqual(
|
||||
@as(i64, 1),
|
||||
try fx.count("SELECT count(*) FROM operational_events WHERE resolved_at IS NULL"),
|
||||
);
|
||||
|
||||
// Reopening is what m30 upgrades on. Ninety days later, so a resolution
|
||||
// stamped with the episode's own `last_seen` would be pruned by this very
|
||||
// call rather than left for an operator to read.
|
||||
const reopened_at = 1000 + Store.resolved_retention_s + 86_400;
|
||||
const upgraded = try Store.init(fx.io, &fx.database, reopened_at);
|
||||
|
||||
try testing.expectEqual(@as(i64, 1), try fx.count("SELECT count(*) FROM operational_events"));
|
||||
try testing.expectEqual(
|
||||
@as(i64, reopened_at),
|
||||
try fx.count("SELECT resolved_at FROM operational_events"),
|
||||
);
|
||||
try testing.expectEqualStrings(
|
||||
"upstream_history.write",
|
||||
try fx.text("SELECT code FROM operational_events"),
|
||||
);
|
||||
// Nothing is open, so the mirror is empty and the SQL slow path is off.
|
||||
try testing.expectEqual(@as(u32, 0), upgraded.active.len);
|
||||
try testing.expectEqual(@as(u32, 0), upgraded.untracked_active_count);
|
||||
|
||||
// A second start finds nothing left to close, and the sweep is idempotent.
|
||||
const again = try Store.init(fx.io, &fx.database, reopened_at + 60);
|
||||
try testing.expectEqual(@as(u32, 0), again.active.len);
|
||||
try testing.expectEqual(
|
||||
@as(i64, reopened_at),
|
||||
try fx.count("SELECT resolved_at FROM operational_events"),
|
||||
);
|
||||
}
|
||||
|
||||
test "a legacy wire code is not something the live emitter can name" {
|
||||
for (legacy_wire_codes) |legacy| {
|
||||
try testing.expectEqual(@as(?Code, null), parseWire(legacy));
|
||||
for (std.enums.values(Code)) |code| {
|
||||
try testing.expect(!std.mem.eql(u8, legacy, wire(code)));
|
||||
}
|
||||
// The read path still has to answer for it, and does so from the text.
|
||||
const dot = std.mem.indexOfScalar(u8, legacy, '.') orelse
|
||||
return error.TestUnexpectedResult;
|
||||
try testing.expectEqualStrings(legacy[0..dot], events_repo.componentOf(legacy));
|
||||
try testing.expect(legacy.len <= events_repo.code_capacity);
|
||||
}
|
||||
}
|
||||
|
||||
test "a failure opens one episode and repeats of it count rather than multiply" {
|
||||
var fx: Fixture = .{};
|
||||
try fx.init(1000);
|
||||
|
||||
Reference in New Issue
Block a user