milestone 31: concurrent upstream exchanges, dot session reuse, queue metrics

This commit is contained in:
2026-08-22 19:54:02 +02:00
parent 623667e475
commit c99a37d170
14 changed files with 2099 additions and 188 deletions
+62 -4
View File
@@ -974,10 +974,16 @@ fn probeUpstreams(r: Runner, cfg: model.Config) !usize {
continue;
};
// Both clients are pinned for the pool's lifetime: `Entry.client` is an
// erased pointer into one of them.
// Both clients are pinned for the pool's lifetime: the entry's one slot
// holds an erased pointer into one of them.
var doh: doh_client.DohClient = undefined;
var dot: dot_client.DotClient = undefined;
// A DoT probe leaves a connection open on a client whose buffers the
// next iteration reuses, so it is closed at the end of every iteration —
// on the FAIL paths and on any error out of the loop too. The flag is
// what keeps the close off `dot` while it is still undefined.
var dot_wired = false;
defer if (dot_wired) dot.close(r.io);
const client: transport.Client = switch (endpoint.scheme) {
.doh => doh: {
doh = doh_client.DohClient.init(&http, endpoint, &request_buf, &transfer_buf) catch {
@@ -988,22 +994,31 @@ fn probeUpstreams(r: Runner, cfg: model.Config) !usize {
break :doh doh.client();
},
.dot => dot: {
dot = dot_client.DotClient.init(endpoint, server.tls_name, r.gpa, &bundle, &bundle_lock, .{
dot = dot_client.DotClient.init(endpoint, server.tls_name, r.gpa, &bundle, &bundle_lock, null, .{
.tls_read = tls_buffers[0..chunk],
.tls_write = tls_buffers[chunk .. 2 * chunk],
.stream_read = tls_buffers[2 * chunk .. 3 * chunk],
.stream_write = tls_buffers[3 * chunk ..],
});
dot_wired = true;
break :dot dot.client();
},
};
// One slot: the probe is sequential by design, and one exchange per
// upstream is the whole of it.
var slots = [_]pool.Slot{.{ .client = client }};
// The pool's counter has to point somewhere, and a probe has no
// process-wide counter storage to point it at.
var recoveries: std.atomic.Value(u64) = .init(0);
var entries = [_]pool.Entry{.{
.endpoint = endpoint,
.client = client,
.slots = &slots,
.priority = server.priority,
.enabled = true,
.health = .init,
.sem = .{ .permits = slots.len },
.reuse_recoveries = &recoveries,
}};
var single: pool.Pool = .init(&entries, .{}, timeouts, seed);
@@ -1746,6 +1761,49 @@ test "the upstream probe redacts a url it cannot parse, without leaving the mach
try testing.expect(!std.mem.containsAtLeast(u8, text, 1, "efgh34"));
}
test "a failed DoT probe reaches close through the per-iteration defer without a session" {
// Exactly that and no more. This is the only test that reaches the DoT
// branch at all: it arms `dot_wired`, fails the dial, and runs the deferred
// `DotClient.close` on a client that never opened a session — twice, so the
// second iteration's dial happens after the first close. What that proves is
// that the path compiles and survives: `dot_wired` set only after `init`,
// and `close` tolerant of a sessionless client. It does *not* prove the
// defer is load-bearing — with no session open, deleting the `defer` would
// leave this test green.
//
// The property that matters — a probe's connection not surviving into the
// next iteration's shared TLS buffers — needs a session to exist, so it is
// pinned one level down on the client itself, by the close-after-failure
// assertions in `dot_client_integration_test.zig`. A successful DoT probe
// cannot be pinned in-repo at all: it needs a real handshake, and the only
// DoT peer this repo has is the self-signed `-Dintegration` loopback
// fixture, which `probeUpstreams` cannot verify because it builds its bundle
// from the system trust store.
//
// `tls://` with a host that is not an IP literal fails inside `dial` before
// any socket is opened or any CA store is read: upstream name resolution is
// out of scope, so `resolveAddress` refuses it. That keeps the test off the
// network and off the host's configuration.
var captured: Captured = .init(testing.allocator);
defer captured.deinit();
const r = captured.runner();
const cfg: model.Config = .{
.groups = &.{.{ .name = "default" }},
.upstreams = &.{
.{ .url = "tls://dns.example:853", .tls_name = "dns.example" },
.{ .url = "tls://other.example:853", .tls_name = "other.example" },
},
};
try testing.expectEqual(@as(usize, 2), try probeUpstreams(r, cfg));
try testing.expectEqualStrings(
"FAIL upstreams[0] 'tls://dns.example:853': ConnectFailed\n" ++
"FAIL upstreams[1] 'tls://other.example:853': ConnectFailed\n",
captured.out.written(),
);
}
test "a successful import prints the warnings the file earned" {
// D5, second half. `check` printed this WARN and `import` recorded it and
// threw it away, because diagnostics were written on the failure path only.