milestone 31: concurrent upstream exchanges, dot session reuse, queue metrics
This commit is contained in:
+62
-4
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user