upstream: a diagnostics episode follows health, and a peer fault carries its cause
Gates / frontend (push) Successful in 2m5s
Gates / test (push) Successful in 2m43s
Gates / test-aarch64 (push) Successful in 8m19s
Gates / package (push) Successful in 4m21s
Gates / container (push) Successful in 13s
CI / gates (push) Successful in 30m43s

This commit is contained in:
2026-09-12 20:24:49 +02:00
parent 6e9a36903e
commit 08cdf86ecd
20 changed files with 1797 additions and 426 deletions
+21 -19
View File
@@ -93,15 +93,21 @@ fn queryWithId(buf: *[query_bytes.len]u8, id: u16) []const u8 {
/// Echoes the question and appends one A record. This is the smallest thing a
/// real upstream could return that the handler forwards unchanged, so the
/// assertions below check bytes that travelled the whole path.
fn answerQuery(query: []const u8, response_buf: []u8) transport.ExchangeError![]u8 {
const request = packet.parse(query) catch return error.BadResponse;
const q = packet.firstQuestion(request) orelse return error.BadResponse;
fn answerQuery(query: []const u8, response_buf: []u8) transport.Outcome {
const request = packet.parse(query) catch return peerFault(error.BadResponse);
const q = packet.firstQuestion(request) orelse return peerFault(error.BadResponse);
var b = packet.ResponseBuilder.init(response_buf, request.header, q) catch
return error.ResponseTooLarge;
return peerFault(error.ResponseTooLarge);
b.addAnswer(q.name, .a, .in, answer_ttl, &answer_rdata) catch
return error.ResponseTooLarge;
return b.finish();
return peerFault(error.ResponseTooLarge);
return .{ .reply = b.finish() };
}
/// A fault whose cause is its own classification: these fixtures fail on
/// purpose and have no concrete cause behind the classification.
fn peerFault(kind: transport.PeerFault) transport.Outcome {
return .{ .fault = .{ .kind = kind, .cause = kind } };
}
/// The healthy upstream. `calls` is atomic because the listener tasks run on
@@ -114,16 +120,14 @@ const GoodUpstream = struct {
io: std.Io,
query: []const u8,
response_buf: []u8,
selected: *?[]const u8,
) transport.ExchangeError![]u8 {
) transport.LeafError!transport.Outcome {
_ = io;
selected.* = "fake://good-upstream";
const self: *GoodUpstream = @ptrCast(@alignCast(ptr));
_ = self.calls.fetchAdd(1, .monotonic);
return answerQuery(query, response_buf);
}
fn client(self: *GoodUpstream) transport.Client {
fn leaf(self: *GoodUpstream) transport.Leaf {
return .{ .ptr = self, .exchangeFn = exchangeFn };
}
};
@@ -140,17 +144,15 @@ const FaultyUpstream = struct {
io: std.Io,
query: []const u8,
response_buf: []u8,
selected: *?[]const u8,
) transport.ExchangeError![]u8 {
) transport.LeafError!transport.Outcome {
_ = io;
selected.* = "fake://faulty-upstream";
const self: *FaultyUpstream = @ptrCast(@alignCast(ptr));
const seen = self.calls.fetchAdd(1, .monotonic);
if (seen < self.fail_first) return self.fault;
if (seen < self.fail_first) return peerFault(self.fault);
return answerQuery(query, response_buf);
}
fn client(self: *FaultyUpstream) transport.Client {
fn leaf(self: *FaultyUpstream) transport.Leaf {
return .{ .ptr = self, .exchangeFn = exchangeFn };
}
};
@@ -166,10 +168,10 @@ const EntryStorage = struct {
fn entry(
self: *EntryStorage,
url: []const u8,
upstream_client: transport.Client,
upstream_leaf: transport.Leaf,
priority: i32,
) pool.Entry {
self.slots[0] = .{ .client = upstream_client };
self.slots[0] = .{ .client = upstream_leaf };
return .{
.endpoint = transport.Endpoint.parse(url) catch unreachable,
.slots = &self.slots,
@@ -270,8 +272,8 @@ test "the whole resolver answers over udp and tcp and fails over to a healthy up
var bad_storage: EntryStorage = .{};
var good_storage: EntryStorage = .{};
var entries = [_]pool.Entry{
bad_storage.entry("https://bad.example/dns-query", bad.client(), 10),
good_storage.entry("tls://good.example", good.client(), 20),
bad_storage.entry("https://bad.example/dns-query", bad.leaf(), 10),
good_storage.entry("tls://good.example", good.leaf(), 20),
};
var upstreams: pool.Pool = .init(&entries, test_cfg, pool_timeouts, 1);