milestone 26: upstream health answers for the selected period
This commit is contained in:
@@ -52,6 +52,15 @@ pub const Input = struct {
|
||||
upstreams_total: u32 = 0,
|
||||
queries_dropped: u64 = 0,
|
||||
writer_failed: bool = false,
|
||||
/// Current state, not a count: the upstream-history flush is failing right
|
||||
/// now. Cleared by the next flush that succeeds (m26 ruling 7).
|
||||
///
|
||||
/// `rows_dropped` deliberately does not appear here. It is cumulative, and
|
||||
/// a rollup that is computed statelessly cannot ask whether a counter grew
|
||||
/// — so feeding it in would latch `/api/health` to degraded forever after
|
||||
/// one overflow. Drops surface through the metric and through the API's
|
||||
/// per-window `complete` instead.
|
||||
history_flush_failing: bool = false,
|
||||
refreshes_gated: u64 = 0,
|
||||
snapshot_generation: ?u64 = null,
|
||||
};
|
||||
@@ -59,11 +68,15 @@ pub const Input = struct {
|
||||
pub const status_ok = "ok";
|
||||
pub const status_degraded = "degraded";
|
||||
|
||||
/// Ruling 22's three conditions. Each one is something an operator must act on:
|
||||
/// a disk that is filling stops the query log, a pool with nothing available
|
||||
/// stops resolution, and a failed writer means rows are being lost right now.
|
||||
/// Conditions an operator must act on, and every one of them is a fact about
|
||||
/// now rather than a count of the past: a disk that is filling stops the query
|
||||
/// log, a pool with nothing available stops resolution, a failed writer means
|
||||
/// rows are being lost right now, and a failing history flush means the
|
||||
/// dashboard's upstream numbers are not being recorded. Each clears itself when
|
||||
/// the underlying condition does.
|
||||
pub fn degraded(input: Input) bool {
|
||||
return input.disk_state != .ok or input.upstreams_available == 0 or input.writer_failed;
|
||||
return input.disk_state != .ok or input.upstreams_available == 0 or
|
||||
input.writer_failed or input.history_flush_failing;
|
||||
}
|
||||
|
||||
pub fn rollup(input: Input) Body {
|
||||
@@ -115,6 +128,10 @@ pub fn collect(state: *server.WebState, io: std.Io) Input {
|
||||
input.writer_failed = logger.writer_failed.load(.monotonic);
|
||||
}
|
||||
|
||||
if (state.history) |history| {
|
||||
input.history_flush_failing = history.snapshotStats(io).last_flush_failed;
|
||||
}
|
||||
|
||||
if (state.manager) |manager| {
|
||||
input.refreshes_gated = manager.refreshesGated();
|
||||
if (manager.acquire(io)) |acquired| {
|
||||
@@ -130,7 +147,10 @@ pub fn collect(state: *server.WebState, io: std.Io) Input {
|
||||
// tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const db = @import("../../storage/db.zig");
|
||||
const history_mod = @import("../../upstream/history.zig");
|
||||
const logger_mod = @import("../../storage/logger.zig");
|
||||
const upstream_history_repo = @import("../../storage/repositories/upstream_history_repo.zig");
|
||||
const testing = std.testing;
|
||||
|
||||
/// A box with nothing wrong with it: one upstream up, disk ok, writer alive.
|
||||
@@ -148,6 +168,10 @@ test "the degraded matrix covers disk state, availability and the writer" {
|
||||
.{ .input = withDisk(healthy, .critical), .degraded = true },
|
||||
.{ .input = withAvailable(healthy, 0), .degraded = true },
|
||||
.{ .input = withWriterFailed(healthy), .degraded = true },
|
||||
// A failing upstream-history flush is losing the dashboard's numbers
|
||||
// right now, and it recovers on its own the moment a flush succeeds.
|
||||
.{ .input = withHistoryFailing(healthy, true), .degraded = true },
|
||||
.{ .input = withHistoryFailing(healthy, false), .degraded = false },
|
||||
// Two faults at once still report one status.
|
||||
.{ .input = withWriterFailed(withDisk(healthy, .critical)), .degraded = true },
|
||||
// Some upstreams down is not degraded while one still answers.
|
||||
@@ -182,6 +206,48 @@ fn withWriterFailed(input: Input) Input {
|
||||
return out;
|
||||
}
|
||||
|
||||
fn withHistoryFailing(input: Input, failing: bool) Input {
|
||||
var out = input;
|
||||
out.history_flush_failing = failing;
|
||||
return out;
|
||||
}
|
||||
|
||||
test "a history overflow that already happened does not degrade the rollup" {
|
||||
// `rows_dropped` is cumulative and the rollup is stateless, so the only
|
||||
// thing it could do with a drop count is latch on it. The accumulator's
|
||||
// drops reach an operator through `/metrics` and through the per-window
|
||||
// `complete` flag, and never through this.
|
||||
const dropped: Input = .{
|
||||
.upstreams_available = 1,
|
||||
.upstreams_total = 1,
|
||||
.history_flush_failing = false,
|
||||
};
|
||||
try testing.expect(!degraded(dropped));
|
||||
try testing.expectEqualStrings(status_ok, rollup(dropped).status);
|
||||
}
|
||||
|
||||
test "collect reads the accumulator's current flush state" {
|
||||
var threaded: std.Io.Threaded = .init(testing.allocator, .{});
|
||||
defer threaded.deinit();
|
||||
const io = threaded.io();
|
||||
|
||||
const acc = try testing.allocator.create(history_mod.Accumulator);
|
||||
defer testing.allocator.destroy(acc);
|
||||
acc.* = .init;
|
||||
|
||||
var state: server.WebState = .{ .gpa = testing.allocator, .history = acc };
|
||||
try testing.expect(!collect(&state, io).history_flush_failing);
|
||||
|
||||
var database = try db.Db.open(":memory:", .{ .mode = .memory });
|
||||
defer database.close();
|
||||
acc.recordSuccess(io, "https://a.example", 60);
|
||||
// No schema in this database, so the real write fails and the flag is set
|
||||
// by the production path rather than by a test poking a field.
|
||||
acc.flushOnce(io, &database, upstream_history_repo.flush);
|
||||
try testing.expect(collect(&state, io).history_flush_failing);
|
||||
try testing.expectEqualStrings("degraded", rollup(collect(&state, io)).status);
|
||||
}
|
||||
|
||||
test "the body reports every input verbatim" {
|
||||
const body = rollup(.{
|
||||
.disk_state = .warn,
|
||||
|
||||
Reference in New Issue
Block a user