milestone 26: upstream health answers for the selected period

This commit is contained in:
2026-08-17 18:21:56 +02:00
parent 3ed9a57822
commit e0a7cd8a6b
28 changed files with 2865 additions and 182 deletions
+52 -12
View File
@@ -48,6 +48,7 @@ const faults = @import("config/faults.zig");
const fetcher = @import("filter/fetcher.zig");
const forward_zones = @import("local/forward_zones.zig");
const handler = @import("server/handler.zig");
const history_mod = @import("upstream/history.zig");
const http_util = @import("web/http_util.zig");
const loader = @import("config/loader.zig");
const local_records = @import("local/records.zig");
@@ -68,6 +69,7 @@ const shutdown = @import("server/shutdown.zig");
const sse = @import("web/sse.zig");
const static = @import("web/static.zig");
const tcp_server = @import("server/tcp_server.zig");
const upstream_history_repo = @import("storage/repositories/upstream_history_repo.zig");
const transport = @import("upstream/transport.zig");
const udp_server = @import("server/udp_server.zig");
const validate = @import("config/validate.zig");
@@ -448,6 +450,13 @@ fn serve(r: cli.Runner, args: cli.RunArgs) !u8 {
@truncate(@as(u96, @bitCast(std.Io.Clock.real.now(io).nanoseconds))),
);
// On the heap, not in this frame: the accumulator carries its pending cells
// and the flush task's buffer inline, which is about a megabyte.
const history = try gpa.create(history_mod.Accumulator);
defer gpa.destroy(history);
history.* = .init;
pool.history = history;
// -----------------------------------------------------------------------
// per-query state
// -----------------------------------------------------------------------
@@ -524,6 +533,8 @@ fn serve(r: cli.Runner, args: cli.RunArgs) !u8 {
defer querylog_writer_db.close();
var querylog_retention_db = try data.reopenQuerylogDb(io);
defer querylog_retention_db.close();
var querylog_history_db = try data.reopenQuerylogDb(io);
defer querylog_history_db.close();
var tracker_db = try data.openConfigDb(io);
defer tracker_db.close();
@@ -646,6 +657,7 @@ fn serve(r: cli.Runner, args: cli.RunArgs) !u8 {
.local_tables = &tables,
.logger = &query_logger,
.retention = &retention,
.history = history,
.sessions = if (sessions) |*s| s else null,
.limiter = if (web_limiter) |*l| l else null,
.hub = hub,
@@ -735,12 +747,42 @@ fn serve(r: cli.Runner, args: cli.RunArgs) !u8 {
shutdown.install(io);
// Declared after everything it borrows, so its `cancel` — which both
// requests cancellation and joins — is the first thing that runs on the way
// out (ruling 22). Nothing below this line may be released while a task
// could still touch it.
// Declared after everything it borrows, so the teardown below — whose
// `cancel` both requests cancellation and joins — is the first thing that
// runs on the way out (ruling 22). Nothing below this line may be released
// while a task could still touch it.
var group: std.Io.Group = .init;
defer group.cancel(io);
// Ruling 4's shutdown order, on the one path every exit from here takes:
// the logger sees a closed queue and drains what it holds rather than
// losing it to cancellation (ruling 22), then every task stops, and only
// then does the final flush run — with no recording task left that could
// add a cell after it.
//
// A `defer` and not straight-line code after `shutdown.wait`, because a
// `concurrent` spawn below can fail with the DNS listeners already
// serving; an orderly error teardown owes the operator the same drain a
// signal gets. The `querylog_history_db` this flush writes through is
// declared above, so its `close` runs after it.
defer {
query_logger.shutdown(io);
group.cancel(io);
history.flushOnce(io, &querylog_history_db, upstream_history_repo.flush);
}
// The gate every non-essential write consults. Reading it before the
// monitor's own task has sampled is safe: a fresh `Monitor` publishes `.ok`
// (disk_monitor.zig:63), so nothing is refused for want of a sample.
const gate: ?*disk_monitor.Monitor = &monitor;
// The writer starts before the listeners, and that order is the deferred
// drain's precondition: a listener that is already accepting queries
// enqueues log entries, and `Logger.shutdown` only closes the queue —
// someone has to be on the other end to write what it hands over. Spawned
// after the listeners, a `concurrent` failure in between would leave those
// entries with no consumer and `group.cancel` nothing to drain, which is
// exactly the loss the teardown above exists to prevent.
try group.concurrent(io, logger_mod.Logger.runWriter, .{ &query_logger, io, &querylog_writer_db, gate });
if (udp6) |*s| try group.concurrent(io, udp_server.UdpServer.serve, .{ s, io });
if (udp4) |*s| try group.concurrent(io, udp_server.UdpServer.serve, .{ s, io });
@@ -751,9 +793,10 @@ fn serve(r: cli.Runner, args: cli.RunArgs) !u8 {
if (doh_certs) |*store| try group.concurrent(io, cert_store.CertStore.watch, .{ store, io });
if (dot_certs) |*store| try group.concurrent(io, cert_store.CertStore.watch, .{ store, io });
const gate: ?*disk_monitor.Monitor = &monitor;
try group.concurrent(io, logger_mod.Logger.runWriter, .{ &query_logger, io, &querylog_writer_db, gate });
try group.concurrent(io, retention_mod.Retention.run, .{ &retention, io, &querylog_retention_db, gate });
// Ungated: a flush writes at most one row per upstream per minute, the same
// category as the query logger's own writes, which are ungated too.
try group.concurrent(io, history_mod.Accumulator.run, .{ history, io, &querylog_history_db });
try group.concurrent(io, disk_monitor.Monitor.run, .{ &monitor, io });
try group.concurrent(io, manager_mod.Manager.runScheduler, .{ &manager, io });
try group.concurrent(io, clients.Tracker.run, .{ &tracker, io, &tracker_db, gate, &client_names_resolver });
@@ -774,14 +817,11 @@ fn serve(r: cli.Runner, args: cli.RunArgs) !u8 {
});
// A canceled wait is a shutdown request too: whoever canceled this task
// wants the process to stop, and the teardown below is how it stops.
// wants the process to stop, and returning into the teardown deferred above
// is how it stops.
shutdown.wait(io) catch {};
log.info("shutting down", .{});
// Before the group is canceled, so the writer sees a closed queue and
// drains what it holds rather than losing it to cancellation (ruling 22).
query_logger.shutdown(io);
return cli.exit_ok;
}