milestone 26: upstream health answers for the selected period
This commit is contained in:
+106
-9
@@ -48,6 +48,7 @@
|
||||
const std = @import("std");
|
||||
|
||||
const health = @import("health.zig");
|
||||
const history_mod = @import("history.zig");
|
||||
const safe_url = @import("../safe_url.zig");
|
||||
const transport = @import("transport.zig");
|
||||
|
||||
@@ -129,6 +130,12 @@ pub const Pool = struct {
|
||||
timeouts: Timeouts,
|
||||
mutex: std.Io.Mutex,
|
||||
rng: std.Random.DefaultPrng,
|
||||
/// Where recorded outcomes also go, as per-minute aggregates for the
|
||||
/// dashboard's ranged view (m26). Defaulted rather than an `init`
|
||||
/// parameter: the composition root wires it after the pool exists, and the
|
||||
/// pool is fully usable without it — `nxdns check` and every unit test here
|
||||
/// run with no history at all.
|
||||
history: ?*history_mod.Accumulator = null,
|
||||
|
||||
pub fn init(
|
||||
entries: []Entry,
|
||||
@@ -307,12 +314,19 @@ pub const Pool = struct {
|
||||
}
|
||||
|
||||
fn recordSuccess(self: *Pool, io: std.Io, entry: *Entry, at: std.Io.Timestamp) void {
|
||||
// Uncancelable: this section takes no Io and never blocks on a peer.
|
||||
// Losing the bookkeeping for a completed exchange to a cancellation
|
||||
// that arrives one instruction later would corrupt health for good.
|
||||
self.mutex.lockUncancelable(io);
|
||||
defer self.mutex.unlock(io);
|
||||
entry.health.recordSuccess(at);
|
||||
{
|
||||
// Uncancelable: this section takes no Io and never blocks on a
|
||||
// peer. Losing the bookkeeping for a completed exchange to a
|
||||
// cancellation that arrives one instruction later would corrupt
|
||||
// health for good.
|
||||
self.mutex.lockUncancelable(io);
|
||||
defer self.mutex.unlock(io);
|
||||
entry.health.recordSuccess(at);
|
||||
}
|
||||
// The block above closes before this line, and that ordering is the
|
||||
// constraint: the accumulator takes a mutex of its own, and no task may
|
||||
// hold one of the two while it takes the other.
|
||||
self.recordHistory(io, entry, .success);
|
||||
}
|
||||
|
||||
fn recordFailure(
|
||||
@@ -322,12 +336,35 @@ pub const Pool = struct {
|
||||
at: std.Io.Timestamp,
|
||||
err: transport.ExchangeError,
|
||||
) void {
|
||||
self.mutex.lockUncancelable(io);
|
||||
defer self.mutex.unlock(io);
|
||||
entry.health.recordFailure(at, @errorName(err), self.cfg, self.rng.random().int(u32));
|
||||
{
|
||||
self.mutex.lockUncancelable(io);
|
||||
defer self.mutex.unlock(io);
|
||||
entry.health.recordFailure(at, @errorName(err), self.cfg, self.rng.random().int(u32));
|
||||
}
|
||||
// After the pool mutex is released, for the reason `recordSuccess`
|
||||
// states.
|
||||
self.recordHistory(io, entry, .{ .failure = @errorName(err) });
|
||||
}
|
||||
|
||||
const Outcome = union(enum) { success, failure: []const u8 };
|
||||
|
||||
/// The wall clock, not the `.awake` timestamp the health state runs on:
|
||||
/// history is aggregated into wall-clock minutes so a dashboard period
|
||||
/// means the same thing here as everywhere else on the page.
|
||||
fn recordHistory(self: *Pool, io: std.Io, entry: *Entry, outcome: Outcome) void {
|
||||
const history = self.history orelse return;
|
||||
const wall_s = std.Io.Clock.real.now(io).toSeconds();
|
||||
switch (outcome) {
|
||||
.success => history.recordSuccess(io, entry.endpoint.url, wall_s),
|
||||
.failure => |name| history.recordFailure(io, entry.endpoint.url, wall_s, name),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const db = @import("../storage/db.zig");
|
||||
const querylog_schema = @import("../storage/querylog_schema.zig");
|
||||
const upstream_history_repo = @import("../storage/repositories/upstream_history_repo.zig");
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
/// A query for example.com A: id 0x1234, RD set, one question.
|
||||
@@ -710,6 +747,66 @@ test "every entry disabled yields ConnectFailed without waiting out the total bu
|
||||
try testing.expectEqual(@as(usize, 0), two.calls);
|
||||
}
|
||||
|
||||
test "a wired accumulator receives both outcomes the pool records" {
|
||||
var threaded: std.Io.Threaded = .init(testing.allocator, .{});
|
||||
defer threaded.deinit();
|
||||
const io = threaded.io();
|
||||
|
||||
var bad: Fake = .{ .behavior = .{ .fail = error.Timeout } };
|
||||
var good: Fake = .{ .behavior = .{ .reply = response_bytes } };
|
||||
var entries = [_]Entry{
|
||||
testEntry("https://bad.example/dns-query", &bad, 10),
|
||||
testEntry("https://good.example/dns-query", &good, 20),
|
||||
};
|
||||
var pool: Pool = .init(&entries, test_cfg, test_timeouts, 1);
|
||||
|
||||
const acc = try testing.allocator.create(history_mod.Accumulator);
|
||||
defer testing.allocator.destroy(acc);
|
||||
acc.* = .init;
|
||||
pool.history = acc;
|
||||
|
||||
var buf: [512]u8 = undefined;
|
||||
// One exchange: the first entry fails over into the second, so this drives
|
||||
// one failure and one success.
|
||||
_ = try pool.exchange(io, query_bytes, &buf);
|
||||
|
||||
// Two cells, one per url, in whatever minute the wall clock is in.
|
||||
try testing.expectEqual(@as(u32, 2), acc.snapshotStats(io).pending);
|
||||
|
||||
// Read back through the flush path rather than through the accumulator's
|
||||
// private cells: the whole point of the hook is that these outcomes reach
|
||||
// storage under the right url.
|
||||
var database = try db.Db.open(":memory:", .{ .mode = .memory });
|
||||
defer database.close();
|
||||
try db.applyPragmas(&database, .{});
|
||||
try database.exec(querylog_schema.ddl);
|
||||
acc.flushOnce(io, &database, upstream_history_repo.flush);
|
||||
|
||||
// A window wide enough that a minute boundary crossed mid-test changes
|
||||
// nothing about what it contains.
|
||||
const now = std.Io.Clock.real.now(io).toSeconds();
|
||||
const failing = try upstream_history_repo.windowStats(
|
||||
&database,
|
||||
"https://bad.example/dns-query",
|
||||
now - 3600,
|
||||
now + 3600,
|
||||
);
|
||||
try testing.expectEqual(@as(u64, 1), failing.failures);
|
||||
try testing.expectEqual(@as(u64, 0), failing.successes);
|
||||
try testing.expect(failing.last_failure_ts != null);
|
||||
try testing.expectEqualStrings("Timeout", failing.lastFailureError());
|
||||
|
||||
const succeeding = try upstream_history_repo.windowStats(
|
||||
&database,
|
||||
"https://good.example/dns-query",
|
||||
now - 3600,
|
||||
now + 3600,
|
||||
);
|
||||
try testing.expectEqual(@as(u64, 1), succeeding.successes);
|
||||
try testing.expectEqual(@as(u64, 0), succeeding.failures);
|
||||
try testing.expectEqual(@as(?i64, null), succeeding.last_failure_ts);
|
||||
}
|
||||
|
||||
test "snapshot reports the counters in pool order" {
|
||||
var threaded: std.Io.Threaded = .init(testing.allocator, .{});
|
||||
defer threaded.deinit();
|
||||
|
||||
Reference in New Issue
Block a user