metrics: peak gauges for in-flight queries, stream connections and the query log queue

This commit is contained in:
2026-10-19 23:08:58 +02:00
parent 9d2511ee58
commit 36e11ca71d
12 changed files with 335 additions and 10 deletions
+4
View File
@@ -148,6 +148,10 @@ pub const DohServer = struct {
return self.core.boundAddress();
}
pub fn connectionsPeak(self: *const DohServer) u64 {
return self.core.connectionsPeak();
}
pub fn snapshotStats(self: *const DohServer) Snapshot {
const core = &self.core.stats;
return .{
+4
View File
@@ -125,6 +125,10 @@ pub const DotServer = struct {
self.* = undefined;
}
pub fn connectionsPeak(self: *const DotServer) u64 {
return self.core.connectionsPeak();
}
pub fn snapshotStats(self: *const DotServer) StatsSnapshot {
const core = &self.core.stats;
return .{
+38
View File
@@ -130,6 +130,11 @@ pub const Handler = struct {
pause: ?*pause.Pause = null,
tracker: ?*clients.Tracker = null,
stats: Stats = .{},
/// Queries inside `handle` right now, across every listener, and the most
/// there have been at once (specs/memory-budget.md item 0). Gauges, so they
/// stay out of `Stats`, which `/metrics` exports field by field as counters.
in_flight: std.atomic.Value(u64) = .init(0),
in_flight_peak: std.atomic.Value(u64) = .init(0),
pub const Stats = struct {
/// Queries answered by the upstream pool.
@@ -229,6 +234,21 @@ pub const Handler = struct {
query: []const u8,
response_buf: []u8,
scratch: *Scratch,
) Outcome {
const now_in_flight = self.in_flight.fetchAdd(1, .monotonic) + 1;
_ = self.in_flight_peak.fetchMax(now_in_flight, .monotonic);
defer _ = self.in_flight.fetchSub(1, .monotonic);
return self.answer(io, which, from, query, response_buf, scratch);
}
fn answer(
self: *Handler,
io: std.Io,
which: Transport,
from: address.NetAddress,
query: []const u8,
response_buf: []u8,
scratch: *Scratch,
) Outcome {
std.debug.assert(response_buf.len >= udp_limit_min);
std.debug.assert(response_buf.len <= transport.max_message_len);
@@ -1459,6 +1479,24 @@ test "a response arriving on the listener port is dropped" {
try testing.expectEqual(@as(usize, 0), fake.calls);
}
test "every query counts in flight for exactly its own handling" {
var t: TestIo = .init();
defer t.deinit();
var fake: FakeUpstream = .{ .reply = response_bytes };
var h_owner: upstream_owner.Borrowed = .{};
var h = bare(h_owner.client(fake.client()));
// Two queries other listeners are still handling.
h.in_flight.store(2, .monotonic);
var buf: [udp_limit_min]u8 = undefined;
try testing.expect(udp(&h, t.io(), query_bytes, &buf) == .reply);
try testing.expectEqual(Handler.Outcome.drop, udp(&h, t.io(), response_bytes, &buf));
try testing.expectEqual(@as(u64, 2), h.in_flight.load(.monotonic));
try testing.expectEqual(@as(u64, 3), h.in_flight_peak.load(.monotonic));
}
test "a query shorter than a header is dropped" {
var t: TestIo = .init();
defer t.deinit();
+17
View File
@@ -83,6 +83,10 @@ pub const CoreStats = struct {
accept_errors: std.atomic.Value(u64) = .init(0),
connection_errors: std.atomic.Value(u64) = .init(0),
idle_timeouts: std.atomic.Value(u64) = .init(0),
/// The most slots that have been taken at once (specs/memory-budget.md
/// item 0). A gauge, so no listener's `Snapshot` carries it: those export
/// every field as a counter. Written only under `Core.mutex`.
connections_peak: std.atomic.Value(u64) = .init(0),
};
// ---------------------------------------------------------------------------
@@ -255,6 +259,9 @@ pub fn Core(comptime Cfg: type) type {
/// that shuts the active connections down, so a stream that arrives
/// after that scan can never claim a slot the scan will not visit again.
shutdown_begun: bool,
/// Guarded by `mutex`. Slots that are not `.free`: a slot is taken from
/// `claim` until `finish` frees it, closing included.
taken: u64,
stats: CoreStats,
run_state: std.atomic.Value(State),
stopped: std.Io.Event,
@@ -299,6 +306,7 @@ pub fn Core(comptime Cfg: type) type {
.conns = conns,
.mutex = .init,
.shutdown_begun = false,
.taken = 0,
.stats = .{},
.run_state = .init(.idle),
.stopped = .unset,
@@ -336,6 +344,10 @@ pub fn Core(comptime Cfg: type) type {
return self.listener.socket.address;
}
pub fn connectionsPeak(self: *const Self) u64 {
return self.stats.connections_peak.load(.monotonic);
}
/// Accept loop. Returns when the task is canceled or `deinit` stops it.
pub fn serve(self: *Self, io: std.Io) void {
if (self.run_state.cmpxchgStrong(.idle, .serving, .acq_rel, .acquire) != null) return;
@@ -443,6 +455,10 @@ pub fn Core(comptime Cfg: type) type {
self.conns[index].stream = stream;
self.conns[index].peer = stream.socket.address;
self.conns[index].state = .active;
self.taken += 1;
if (self.taken > self.stats.connections_peak.load(.monotonic)) {
self.stats.connections_peak.store(self.taken, .monotonic);
}
},
.at_capacity, .shutting_down => {},
}
@@ -464,6 +480,7 @@ pub fn Core(comptime Cfg: type) type {
self.mutex.lockUncancelable(io);
conn.state = .free;
self.taken -= 1;
self.mutex.unlock(io);
}
+4
View File
@@ -99,6 +99,10 @@ pub const TcpServer = struct {
return self.core.boundAddress();
}
pub fn connectionsPeak(self: *const TcpServer) u64 {
return self.core.connectionsPeak();
}
/// The counters, read one at a time. A scrape that lands mid-accept can see
/// a connection counted before its outcome is; a lock would buy a
/// consistency no consumer needs.
@@ -329,6 +329,52 @@ test "a canceled serve does not wait for a live connection" {
try client_result;
}
test "the connections peak is the most slots held at once" {
if (!build_options.integration) return error.SkipZigTest;
const gpa = testing.allocator;
var threaded: std.Io.Threaded = .init(gpa, .{});
defer threaded.deinit();
const io = threaded.io();
var fake: FakeUpstream = .{ .reply = response_bytes };
var h_owner: upstream_owner.Borrowed = .{};
var h = bareHandler(h_owner.client(fake.client()));
const listen_address: net.IpAddress = try .parse("127.0.0.1", 0);
var server = try tcp_server.TcpServer.listen(gpa, io, listen_address, &h, .{
.max_connections = 3,
.idle_timeout = .{ .raw = .fromSeconds(600), .clock = .awake },
});
const server_address = server.boundAddress();
var serving: std.Io.Group = .init;
try serving.concurrent(io, tcp_server.TcpServer.serve, .{ &server, io });
var answered: [2]std.Io.Event = @splat(.unset);
var client_results: [2]anyerror!void = @splat({});
var clients_group: std.Io.Group = .init;
for (&answered, &client_results) |*event, *result| {
try clients_group.concurrent(io, holdConnection, .{ io, server_address, event, result });
}
for (&answered) |*event| event.waitUncancelable(io);
// Both replies prove both slots are claimed, and the listener has room for
// a third that never arrives.
const held_open = server.connectionsPeak();
server.deinit(io);
serving.await(io) catch |err| switch (err) {
error.Canceled => unreachable,
};
clients_group.await(io) catch |err| switch (err) {
error.Canceled => unreachable,
};
for (client_results) |result| try result;
try testing.expectEqual(@as(u64, 2), held_open);
}
/// The first slot the server would hand out, read after `serve` has returned so
/// nothing can be writing it.
fn firstFreeSlot(server: *const tcp_server.TcpServer) ?usize {