upstream: one absolute per-query budget across queueing and failover
waiting for a slot now spends the query budget; truncated attempts that expire fault the budget, not the upstream, and are never attributed. admission sweeps in priority order before blocking. forward zones spend read_timeout_ms once across udp, truncation and tcp. adds nxdns_upstream_budget_exhausted_total and a 64-upstream validation limit.
This commit is contained in:
+39
-2
@@ -166,6 +166,12 @@ pub const Sample = struct {
|
||||
udp_listener: ?udp_server.Snapshot = null,
|
||||
tcp_listener: ?tcp_server.Snapshot = null,
|
||||
upstreams: []const UpstreamSample = &.{},
|
||||
/// Exchanges the pool gave up on because the request's own budget ran out.
|
||||
/// Pool-wide rather than per-upstream on purpose: budget exhaustion is
|
||||
/// evidence about the pool, never about an endpoint, so it carries no url
|
||||
/// label and cannot live in `UpstreamSample`. Absent while no pool is
|
||||
/// wired, like every other collaborator.
|
||||
upstream_budget_exhausted_total: ?u64 = null,
|
||||
};
|
||||
|
||||
pub fn handle(
|
||||
@@ -271,7 +277,10 @@ pub fn collect(state: *server.WebState, io: std.Io, arena: Allocator) Allocator.
|
||||
if (state.upstreams) |owner| {
|
||||
const generation = owner.acquire(io);
|
||||
defer owner.release(io, generation);
|
||||
if (generation.pool) |pool| sample.upstreams = try upstreams(pool, io, arena);
|
||||
if (generation.pool) |pool| {
|
||||
sample.upstreams = try upstreams(pool, io, arena);
|
||||
sample.upstream_budget_exhausted_total = pool.budgetExhaustedTotal();
|
||||
}
|
||||
}
|
||||
|
||||
return sample;
|
||||
@@ -465,6 +474,15 @@ pub fn render(w: *std.Io.Writer, sample: Sample) std.Io.Writer.Error!void {
|
||||
if (sample.doh_certs != null or sample.dot_certs != null) try renderCerts(w, sample);
|
||||
|
||||
if (sample.upstreams.len != 0) try renderUpstreams(w, sample.upstreams);
|
||||
if (sample.upstream_budget_exhausted_total) |total| {
|
||||
try labeledHead(
|
||||
w,
|
||||
"nxdns_upstream_budget_exhausted_total",
|
||||
"Exchanges that ran out of their own total budget before any upstream answered.",
|
||||
"counter",
|
||||
);
|
||||
try w.print("nxdns_upstream_budget_exhausted_total {d}\n", .{total});
|
||||
}
|
||||
}
|
||||
|
||||
fn renderCerts(w: *std.Io.Writer, sample: Sample) std.Io.Writer.Error!void {
|
||||
@@ -1540,7 +1558,7 @@ test "the queue families carry what a real pool recorded, through the real snaps
|
||||
.priority = 10,
|
||||
.enabled = true,
|
||||
.health = .init,
|
||||
.sem = .{ .permits = slots.len },
|
||||
.admission = .{ .permits = slots.len },
|
||||
.reuse_recoveries = &recoveries,
|
||||
}};
|
||||
var pool: pool_mod.Pool = .init(&entries, .{}, .{
|
||||
@@ -1662,3 +1680,22 @@ fn fieldIndex(comptime name: []const u8) usize {
|
||||
}
|
||||
@compileError("no such counter: " ++ name);
|
||||
}
|
||||
|
||||
test "the budget-exhausted counter renders pool-wide, without a url label" {
|
||||
const text = try renderToString(testing.allocator, .{ .upstream_budget_exhausted_total = 7 });
|
||||
defer testing.allocator.free(text);
|
||||
|
||||
try testing.expect(std.mem.containsAtLeast(
|
||||
u8,
|
||||
text,
|
||||
1,
|
||||
"# TYPE nxdns_upstream_budget_exhausted_total counter\n",
|
||||
));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, text, 1, "nxdns_upstream_budget_exhausted_total 7\n"));
|
||||
|
||||
// No pool, no series: an operator tells "no upstreams wired" from "zero
|
||||
// exhausted budgets" the same way every other collaborator is told.
|
||||
const absent = try renderToString(testing.allocator, .{});
|
||||
defer testing.allocator.free(absent);
|
||||
try testing.expect(!std.mem.containsAtLeast(u8, absent, 1, "nxdns_upstream_budget_exhausted_total"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user