db-mode config changes apply live in-process

settings and upstream writes now follow a prepare, commit, publish, retire
contract: candidates are built and validated before the database transaction,
published as infallible pointer swaps, and old generations retire after their
readers drain. per-query policy values snapshot once per query; upstream pool,
cache, rate limiter, sessions, api limiter, log sink, blocklist scheduler and
the query-log queue each gained one named live operation. restart_required
shrinks from every scalar key to the bind keys and web.enabled; the admin ui
drops its restart notices for everything else. file mode is unchanged.
This commit is contained in:
2026-08-24 00:04:28 +02:00
parent 17e6e93ce1
commit d961b152a3
47 changed files with 7698 additions and 926 deletions
+37 -16
View File
@@ -34,6 +34,7 @@ const events_mod = @import("../storage/events.zig");
const http_util = @import("http_util.zig");
const logging = @import("../platform/logging.zig");
const pool_mod = @import("../upstream/pool.zig");
const upstream_owner = @import("../upstream/owner.zig");
const rate_limiter = @import("../server/rate_limiter.zig");
const retention_mod = @import("../storage/retention.zig");
const safe_url = @import("../safe_url.zig");
@@ -187,28 +188,36 @@ pub fn collect(state: *server.WebState, io: std.Io, arena: Allocator) Allocator.
if (state.handler) |handler| {
sample.dns = dnsCounters(&handler.stats);
if (handler.cache) |cache| {
// The pointer is loaded under the same mutex a query loads it under,
// never before it: a live `cache.size` or `dns.rate_limit` change
// frees the object it displaced as soon as the swap returns.
{
handler.cache_mutex.lockUncancelable(io);
defer handler.cache_mutex.unlock(io);
sample.cache = .{
if (handler.cache) |cache| sample.cache = .{
.stats = cache.stats,
.entries = cache.len(),
.memory_bytes = cache.memoryBytes(),
};
}
if (handler.limiter) |limiter| {
{
handler.limiter_mutex.lockUncancelable(io);
defer handler.limiter_mutex.unlock(io);
sample.limiter = .{ .stats = limiter.stats, .tracked_clients = limiter.table.count() };
if (handler.limiter) |limiter| {
sample.limiter = .{ .stats = limiter.stats, .tracked_clients = limiter.table.count() };
}
}
}
if (state.logger) |logger| sample.logger = .{
.queries_dropped = logger.queries_dropped.load(.monotonic),
.rows_written = logger.rows_written.load(.monotonic),
.batches_gated = logger.batches_gated.load(.monotonic),
};
if (state.logger) |controller| {
const reading = controller.sample(io);
sample.logger = .{
.queries_dropped = reading.queries_dropped,
.rows_written = reading.rows_written,
.batches_gated = reading.batches_gated,
};
}
if (state.tracker) |tracker| sample.tracker = .{
.stats = tracker.snapshotStats(io),
@@ -259,7 +268,11 @@ pub fn collect(state: *server.WebState, io: std.Io, arena: Allocator) Allocator.
sample.udp_listener = sumListeners(udp_server.Snapshot, udp_server.UdpServer, state.udp_listeners);
sample.tcp_listener = sumListeners(tcp_server.Snapshot, tcp_server.TcpServer, state.tcp_listeners);
if (state.pool) |pool| sample.upstreams = try upstreams(pool, io, arena);
if (state.upstreams) |owner| {
const generation = owner.acquire(io);
defer owner.release(io, generation);
if (generation.pool) |pool| sample.upstreams = try upstreams(pool, io, arena);
}
return sample;
}
@@ -700,18 +713,23 @@ fn writeLabelValue(w: *std.Io.Writer, value: []const u8) std.Io.Writer.Error!voi
const db = @import("../storage/db.zig");
const local_tables = @import("../server/local_tables.zig");
const logger_controller = @import("../storage/logger_controller.zig");
const logger_mod = @import("../storage/logger.zig");
const migrations = @import("../storage/migrations.zig");
const transport = @import("../upstream/transport.zig");
const testing = std.testing;
/// Backs `testHandler`'s owner. File-scope because the handler is returned by
/// value and its `*Owner` has to outlive the return; nothing here exchanges, so
/// the generation behind it is never acquired.
var unreachable_upstream: upstream_owner.Borrowed = .{};
/// A handler with no upstream reachable: every test here reads counters and
/// never runs a query.
fn testHandler() dns_handler.Handler {
return .{
.upstream = .{ .ptr = undefined, .exchangeFn = undefined },
.blocking = .{ .mode = .zero, .ttl = 5 },
.forward_read_timeout = .{ .raw = .fromMilliseconds(50), .clock = .awake },
.upstream = unreachable_upstream.client(.{ .ptr = undefined, .exchangeFn = undefined }),
.policy = .{ .blocking = .{ .mode = .zero, .ttl = 5 }, .forward_read_timeout = .{ .raw = .fromMilliseconds(50), .clock = .awake } },
};
}
@@ -1600,9 +1618,12 @@ test "collect reads the live counters of the components it is given" {
var queue_buf: [4]logger_mod.Entry = undefined;
var query_logger: logger_mod.Logger = .init(.{}, &queue_buf);
query_logger.rows_written.store(90, .monotonic);
var log_owner: logger_controller.Borrowed = .{};
var tracker: clients.Tracker = .init(30);
var retention: retention_mod.Retention = .init(.{});
var tracker_days: retention_mod.RetentionDays = .init(30);
var tracker: clients.Tracker = .init(&tracker_days);
var retention_days: retention_mod.RetentionDays = .init(30);
var retention: retention_mod.Retention = .init(&retention_days);
var tables: local_tables.LocalTables = .empty;
var names: client_names.Resolver = .init(&tables);
names.stats.no_zone = 4;
@@ -1611,7 +1632,7 @@ test "collect reads the live counters of the components it is given" {
var state: server.WebState = .{
.gpa = testing.allocator,
.handler = &handler,
.logger = &query_logger,
.logger = log_owner.over(&query_logger),
.tracker = &tracker,
.client_names = &names,
.retention = &retention,