upstream: one absolute per-query budget across queueing and failover
Gates / frontend (push) Successful in 1m46s
Gates / test (push) Successful in 2m32s
Gates / package (push) Successful in 4m20s
Gates / test-aarch64 (push) Successful in 8m15s
Gates / container (push) Successful in 15s
CI / gates (push) Successful in 30m33s
Gates / frontend (push) Successful in 1m46s
Gates / test (push) Successful in 2m32s
Gates / package (push) Successful in 4m20s
Gates / test-aarch64 (push) Successful in 8m15s
Gates / container (push) Successful in 15s
CI / gates (push) Successful in 30m33s
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:
+66
-2
@@ -52,6 +52,7 @@ const limits = @import("limits.zig");
|
||||
const logger = @import("../storage/logger.zig");
|
||||
const regex = @import("../filter/regex.zig");
|
||||
const safe_url = @import("../safe_url.zig");
|
||||
const pool = @import("../upstream/pool.zig");
|
||||
const transport = @import("../upstream/transport.zig");
|
||||
|
||||
const Config = model.Config;
|
||||
@@ -69,6 +70,7 @@ const Prefix = address.Prefix;
|
||||
/// like every other resource failure.
|
||||
pub const ValidateError = error{
|
||||
NoUpstreams,
|
||||
TooManyUpstreams,
|
||||
BadUpstreamUrl,
|
||||
UpstreamHostNotIpLiteral,
|
||||
DuplicateUpstreamUrl,
|
||||
@@ -357,8 +359,9 @@ fn checkScalars(cfg: Config, diags: *Diagnostics) error{OutOfMemory}!void {
|
||||
// The only cross-check that relates two knobs of one subsystem: the pool
|
||||
// races one attempt against `attempt` and the whole failover loop against
|
||||
// `total`, so an attempt budget above the total one can never be reached.
|
||||
// `read_timeout_ms` belongs to the forward-zone client and is deliberately
|
||||
// unrelated to both.
|
||||
// `read_timeout_ms` bounds the forward-zone client's whole exchange —
|
||||
// UDP attempt, TC=1 fallback and TCP retry under one budget — and is
|
||||
// deliberately unrelated to both.
|
||||
if (up.attempt_timeout_ms > up.total_timeout_ms) {
|
||||
try diags.add(
|
||||
error.BadTimeout,
|
||||
@@ -823,6 +826,21 @@ fn checkCollections(cfg: Config, diags: *Diagnostics, scratch: Allocator) error{
|
||||
.{},
|
||||
);
|
||||
}
|
||||
// Each enabled upstream becomes one pool entry, and the failover loop
|
||||
// tracks the entries it has spent in a fixed bitset of `Pool.max_entries`
|
||||
// bits. Without this check a config past that bound reaches an assert and
|
||||
// panics at startup, which is the wrong way to tell an operator that a
|
||||
// number is too large. Disabled upstreams are not counted: they never
|
||||
// become entries.
|
||||
if (enabled_upstreams > pool.Pool.max_entries) {
|
||||
try diags.add(
|
||||
error.TooManyUpstreams,
|
||||
"upstreams",
|
||||
.{},
|
||||
"{d} upstreams are enabled; nxdns is built for at most {d}",
|
||||
.{ enabled_upstreams, pool.Pool.max_entries },
|
||||
);
|
||||
}
|
||||
|
||||
var client_ips: IndexSet = .empty;
|
||||
for (cfg.clients, 0..) |client, i| {
|
||||
@@ -1323,6 +1341,52 @@ test "error.NoUpstreams when nothing is enabled" {
|
||||
try expectProblem(cfg, error.NoUpstreams, "upstreams");
|
||||
}
|
||||
|
||||
/// `count` distinct enabled upstreams. Generated rather than written out
|
||||
/// because the bound this exercises is 64, and a hand-written list that long
|
||||
/// would say less than the loop does.
|
||||
fn ManyUpstreams(comptime count: usize) type {
|
||||
return struct {
|
||||
const list: [count]model.UpstreamServer = blk: {
|
||||
var built: [count]model.UpstreamServer = undefined;
|
||||
for (&built, 0..) |*server, i| {
|
||||
server.* = .{ .url = std.fmt.comptimePrint("https://u{d}.example/dns-query", .{i}) };
|
||||
}
|
||||
break :blk built;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
fn manyUpstreams(comptime count: usize) []const model.UpstreamServer {
|
||||
return &ManyUpstreams(count).list;
|
||||
}
|
||||
|
||||
test "as many enabled upstreams as the pool holds validates cleanly" {
|
||||
var cfg = baseConfig();
|
||||
cfg.upstreams = manyUpstreams(pool.Pool.max_entries);
|
||||
try expectClean(cfg);
|
||||
}
|
||||
|
||||
test "error.TooManyUpstreams one enabled upstream past the pool's bound" {
|
||||
// The pool asserts this bound, so without the check here a valid-looking
|
||||
// config panics at startup instead of being reported.
|
||||
var cfg = baseConfig();
|
||||
cfg.upstreams = manyUpstreams(pool.Pool.max_entries + 1);
|
||||
try expectProblem(cfg, error.TooManyUpstreams, "upstreams");
|
||||
}
|
||||
|
||||
test "upstreams past the pool's bound are fine while they are disabled" {
|
||||
// Only enabled upstreams become pool entries, so a long list with a small
|
||||
// enabled subset is not near the bound at all.
|
||||
var cfg = baseConfig();
|
||||
cfg.upstreams = comptime blk: {
|
||||
var list = manyUpstreams(pool.Pool.max_entries + 1)[0 .. pool.Pool.max_entries + 1].*;
|
||||
for (list[1..]) |*server| server.enabled = false;
|
||||
const frozen = list;
|
||||
break :blk &frozen;
|
||||
};
|
||||
try expectClean(cfg);
|
||||
}
|
||||
|
||||
test "error.BadUpstreamUrl on an unsupported scheme" {
|
||||
var cfg = baseConfig();
|
||||
cfg.upstreams = &.{.{ .url = "ftp://dns.example/" }};
|
||||
|
||||
Reference in New Issue
Block a user