milestone 31: concurrent upstream exchanges, dot session reuse, queue metrics
Gates / frontend (push) Successful in 1m26s
Gates / test (push) Successful in 1m55s
Gates / test-aarch64 (push) Failing after 3h1m8s
Gates / package (push) Successful in 3m55s
Gates / container (push) Successful in 15s
CI / gates (push) Failing after 3h21m29s
Gates / frontend (push) Successful in 1m26s
Gates / test (push) Successful in 1m55s
Gates / test-aarch64 (push) Failing after 3h1m8s
Gates / package (push) Successful in 3m55s
Gates / container (push) Successful in 15s
CI / gates (push) Failing after 3h21m29s
This commit is contained in:
+126
-49
@@ -529,8 +529,8 @@ fn serve(r: cli.Runner, args: cli.RunArgs) !u8 {
|
||||
defer bundle.deinit(gpa);
|
||||
var bundle_lock: std.Io.RwLock = .init;
|
||||
|
||||
var upstreams = try Upstreams.build(gpa, cfg.upstreams, &dns_http, &bundle, &bundle_lock, &config_load);
|
||||
defer upstreams.deinit(gpa);
|
||||
var upstreams = try Upstreams.build(io, gpa, cfg.upstreams, &dns_http, &bundle, &bundle_lock, &config_load);
|
||||
defer upstreams.deinit(io, gpa);
|
||||
|
||||
var pool: pool_mod.Pool = .init(
|
||||
upstreams.active(),
|
||||
@@ -1146,16 +1146,34 @@ fn maintenanceOnce(
|
||||
|
||||
/// The pool's entries and everything they point into.
|
||||
///
|
||||
/// `Pool.Entry.client` is a type-erased pointer into `doh` or `dot`, and each
|
||||
/// client borrows a slice of `doh_buf`/`dot_buf`, so all five allocations live
|
||||
/// exactly as long as the pool does. One entry is used by one task at a time
|
||||
/// (`Entry.busy`), which is why the buffers are per client and not shared the
|
||||
/// way `cli.probeUpstreams` shares them.
|
||||
/// Every enabled upstream gets `pool_mod.slots_per_entry` leaf clients, one per
|
||||
/// slot of its entry, so that many exchanges can be in flight against it at
|
||||
/// once. `Slot.client` is a type-erased pointer into `doh` or `dot`, each of
|
||||
/// those clients borrows a slice of `doh_buf`/`dot_buf`, and each entry borrows
|
||||
/// a run of `slot_storage` and one counter of `recovery_counters` — so every
|
||||
/// allocation here lives exactly as long as the pool does, and none of them is
|
||||
/// ever resized. One slot is used by one task at a time, which is why the
|
||||
/// buffers are per client and not shared the way `cli.probeUpstreams` shares
|
||||
/// them.
|
||||
const Upstreams = struct {
|
||||
entries: []pool_mod.Entry,
|
||||
used: usize,
|
||||
/// Sliced per entry into `Entry.slots`, never pointing into the client
|
||||
/// arrays: `Pool.init` sorts entries and the slices have to survive it.
|
||||
slot_storage: []pool_mod.Slot,
|
||||
/// One per enabled upstream, and the reason it is a separate allocation:
|
||||
/// `Pool.init` sorts entries by value, so a counter living inside an entry
|
||||
/// would be pointed at by the wrong upstream's clients after the sort.
|
||||
recovery_counters: []std.atomic.Value(u64),
|
||||
doh: []doh_client.DohClient,
|
||||
dot: []dot_client.DotClient,
|
||||
/// How much of `doh`/`dot` was actually initialized. A malformed or skipped
|
||||
/// upstream leaves the tail of an over-allocated array undefined, and both
|
||||
/// `deinit` and `build`'s failure paths iterate only the initialized
|
||||
/// prefix — reading a `DotClient` that was never built, or closing a
|
||||
/// session that was never opened, is what these two counts prevent.
|
||||
doh_used: usize,
|
||||
dot_used: usize,
|
||||
doh_buf: []u8,
|
||||
dot_buf: []u8,
|
||||
|
||||
@@ -1163,6 +1181,7 @@ const Upstreams = struct {
|
||||
/// skipped, because one bad row in a table of four must not take DNS down.
|
||||
/// No usable row at all is a configuration fault.
|
||||
fn build(
|
||||
io: std.Io,
|
||||
gpa: Allocator,
|
||||
servers: []const model.UpstreamServer,
|
||||
http: *std.http.Client,
|
||||
@@ -1177,24 +1196,30 @@ const Upstreams = struct {
|
||||
if (enabled == 0) return error.NoUsableUpstreams;
|
||||
|
||||
const chunk = tls.Client.min_buffer_len;
|
||||
const slots = pool_mod.slots_per_entry;
|
||||
const leaf_clients = enabled * slots;
|
||||
|
||||
var self: Upstreams = .{
|
||||
.entries = try gpa.alloc(pool_mod.Entry, enabled),
|
||||
.used = 0,
|
||||
.slot_storage = &.{},
|
||||
.recovery_counters = &.{},
|
||||
.doh = &.{},
|
||||
.dot = &.{},
|
||||
.doh_used = 0,
|
||||
.dot_used = 0,
|
||||
.doh_buf = &.{},
|
||||
.dot_buf = &.{},
|
||||
};
|
||||
errdefer self.deinit(gpa);
|
||||
errdefer self.deinit(io, gpa);
|
||||
|
||||
self.doh = try gpa.alloc(doh_client.DohClient, enabled);
|
||||
self.dot = try gpa.alloc(dot_client.DotClient, enabled);
|
||||
self.doh_buf = try gpa.alloc(u8, enabled * (doh_request_buf_len + doh_transfer_buf_len));
|
||||
self.dot_buf = try gpa.alloc(u8, enabled * 4 * chunk);
|
||||
|
||||
var doh_count: usize = 0;
|
||||
var dot_count: usize = 0;
|
||||
self.slot_storage = try gpa.alloc(pool_mod.Slot, leaf_clients);
|
||||
self.recovery_counters = try gpa.alloc(std.atomic.Value(u64), enabled);
|
||||
for (self.recovery_counters) |*counter| counter.* = .init(0);
|
||||
self.doh = try gpa.alloc(doh_client.DohClient, leaf_clients);
|
||||
self.dot = try gpa.alloc(dot_client.DotClient, leaf_clients);
|
||||
self.doh_buf = try gpa.alloc(u8, leaf_clients * (doh_request_buf_len + doh_transfer_buf_len));
|
||||
self.dot_buf = try gpa.alloc(u8, leaf_clients * 4 * chunk);
|
||||
|
||||
for (servers) |server| {
|
||||
if (!server.enabled) continue;
|
||||
@@ -1208,46 +1233,27 @@ const Upstreams = struct {
|
||||
continue;
|
||||
};
|
||||
|
||||
const client: transport.Client = switch (endpoint.scheme) {
|
||||
.doh => doh: {
|
||||
const base = doh_count * (doh_request_buf_len + doh_transfer_buf_len);
|
||||
const slot = &self.doh[doh_count];
|
||||
slot.* = doh_client.DohClient.init(
|
||||
http,
|
||||
endpoint,
|
||||
self.doh_buf[base..][0..doh_request_buf_len],
|
||||
self.doh_buf[base + doh_request_buf_len ..][0..doh_transfer_buf_len],
|
||||
) catch {
|
||||
log.warn(
|
||||
"upstream {f} is not a usable DoH url; skipped",
|
||||
.{safe_url.redactQuoted(server.url)},
|
||||
);
|
||||
noteUpstream(config_load, server.url, "not a usable DoH url; skipped");
|
||||
continue;
|
||||
};
|
||||
doh_count += 1;
|
||||
break :doh slot.client();
|
||||
const entry_slots = self.slot_storage[self.used * slots ..][0..slots];
|
||||
switch (endpoint.scheme) {
|
||||
.doh => if (!self.wireDoh(http, endpoint, entry_slots)) {
|
||||
log.warn(
|
||||
"upstream {f} is not a usable DoH url; skipped",
|
||||
.{safe_url.redactQuoted(server.url)},
|
||||
);
|
||||
noteUpstream(config_load, server.url, "not a usable DoH url; skipped");
|
||||
continue;
|
||||
},
|
||||
.dot => dot: {
|
||||
const base = dot_count * 4 * chunk;
|
||||
const slot = &self.dot[dot_count];
|
||||
slot.* = dot_client.DotClient.init(endpoint, server.tls_name, gpa, bundle, bundle_lock, .{
|
||||
.tls_read = self.dot_buf[base..][0..chunk],
|
||||
.tls_write = self.dot_buf[base + chunk ..][0..chunk],
|
||||
.stream_read = self.dot_buf[base + 2 * chunk ..][0..chunk],
|
||||
.stream_write = self.dot_buf[base + 3 * chunk ..][0..chunk],
|
||||
});
|
||||
dot_count += 1;
|
||||
break :dot slot.client();
|
||||
},
|
||||
};
|
||||
.dot => self.wireDot(gpa, endpoint, server.tls_name, bundle, bundle_lock, entry_slots),
|
||||
}
|
||||
|
||||
self.entries[self.used] = .{
|
||||
.endpoint = endpoint,
|
||||
.client = client,
|
||||
.slots = entry_slots,
|
||||
.priority = server.priority,
|
||||
.enabled = true,
|
||||
.health = .init,
|
||||
.sem = .{ .permits = entry_slots.len },
|
||||
.reuse_recoveries = &self.recovery_counters[self.used],
|
||||
};
|
||||
self.used += 1;
|
||||
}
|
||||
@@ -1256,17 +1262,88 @@ const Upstreams = struct {
|
||||
return self;
|
||||
}
|
||||
|
||||
/// One `DohClient` per slot, all sharing the one `std.http.Client`: its
|
||||
/// connection pool already serves concurrent requests, and a `DohClient`'s
|
||||
/// only mutable state is the two buffers this gives each slot its own of.
|
||||
///
|
||||
/// False means the url is not a usable DoH url, which `DohClient.init`
|
||||
/// decides from the url alone — so it fails on the first slot or on none.
|
||||
/// `doh_used` still advances per client rather than per entry: it means
|
||||
/// "initialized", and a skipped entry's clients are simply never reached.
|
||||
fn wireDoh(
|
||||
self: *Upstreams,
|
||||
http: *std.http.Client,
|
||||
endpoint: transport.Endpoint,
|
||||
slots: []pool_mod.Slot,
|
||||
) bool {
|
||||
for (slots) |*slot| {
|
||||
const index = self.doh_used;
|
||||
const base = index * (doh_request_buf_len + doh_transfer_buf_len);
|
||||
self.doh[index] = doh_client.DohClient.init(
|
||||
http,
|
||||
endpoint,
|
||||
self.doh_buf[base..][0..doh_request_buf_len],
|
||||
self.doh_buf[base + doh_request_buf_len ..][0..doh_transfer_buf_len],
|
||||
) catch return false;
|
||||
self.doh_used = index + 1;
|
||||
slot.* = .{ .client = self.doh[index].client() };
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// One `DotClient` per slot, each with its own four TLS buffers and all
|
||||
/// sharing the trust store. Every client of one entry reports its stale-reuse
|
||||
/// recoveries through that entry's counter.
|
||||
fn wireDot(
|
||||
self: *Upstreams,
|
||||
gpa: Allocator,
|
||||
endpoint: transport.Endpoint,
|
||||
tls_name: []const u8,
|
||||
bundle: *Certificate.Bundle,
|
||||
bundle_lock: *std.Io.RwLock,
|
||||
slots: []pool_mod.Slot,
|
||||
) void {
|
||||
const chunk = tls.Client.min_buffer_len;
|
||||
const recoveries = &self.recovery_counters[self.used];
|
||||
for (slots) |*slot| {
|
||||
const index = self.dot_used;
|
||||
const base = index * 4 * chunk;
|
||||
self.dot[index] = dot_client.DotClient.init(
|
||||
endpoint,
|
||||
tls_name,
|
||||
gpa,
|
||||
bundle,
|
||||
bundle_lock,
|
||||
recoveries,
|
||||
.{
|
||||
.tls_read = self.dot_buf[base..][0..chunk],
|
||||
.tls_write = self.dot_buf[base + chunk ..][0..chunk],
|
||||
.stream_read = self.dot_buf[base + 2 * chunk ..][0..chunk],
|
||||
.stream_write = self.dot_buf[base + 3 * chunk ..][0..chunk],
|
||||
},
|
||||
);
|
||||
self.dot_used = index + 1;
|
||||
slot.* = .{ .client = self.dot[index].client() };
|
||||
}
|
||||
}
|
||||
|
||||
/// The prefix `Pool.init` is given. The rest of `entries` is allocated but
|
||||
/// never filled, which is what keeps `deinit` able to free the whole block.
|
||||
fn active(self: *Upstreams) []pool_mod.Entry {
|
||||
return self.entries[0..self.used];
|
||||
}
|
||||
|
||||
fn deinit(self: *Upstreams, gpa: Allocator) void {
|
||||
/// Connections first, memory second: a `DotClient` holds a socket its
|
||||
/// buffers belong to, so nothing it points at may be freed before it is
|
||||
/// closed.
|
||||
fn deinit(self: *Upstreams, io: std.Io, gpa: Allocator) void {
|
||||
for (self.dot[0..self.dot_used]) |*client| client.close(io);
|
||||
gpa.free(self.dot_buf);
|
||||
gpa.free(self.doh_buf);
|
||||
gpa.free(self.dot);
|
||||
gpa.free(self.doh);
|
||||
gpa.free(self.recovery_counters);
|
||||
gpa.free(self.slot_storage);
|
||||
gpa.free(self.entries);
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user