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
+86 -2
View File
@@ -245,11 +245,14 @@ pub const LiveHash = struct {
};
/// One live session. `last_used` drives the LRU eviction and moves on every
/// successful validation; `expires_at` is fixed at login, so a session ends at
/// its TTL however busy it was.
/// successful validation; `expires_at` is `issued_at + ttl`, so a session ends
/// at its TTL however busy it was. `issued_at` is kept so `setTtl` can
/// recompute `expires_at` for live sessions from their origin rather than from
/// the moment of the change.
const Slot = struct {
used: bool,
digest: [Sha256.digest_length]u8,
issued_at: i64,
expires_at: i64,
last_used: i64,
};
@@ -282,6 +285,7 @@ pub const Sessions = struct {
.slots = @splat(.{
.used = false,
.digest = @splat(0),
.issued_at = 0,
.expires_at = 0,
.last_used = 0,
}),
@@ -311,6 +315,7 @@ pub const Sessions = struct {
slot.* = .{
.used = true,
.digest = digest,
.issued_at = now_s,
.expires_at = now_s + self.ttl_seconds,
.last_used = now_s,
};
@@ -381,6 +386,36 @@ pub const Sessions = struct {
return live;
}
/// Installs a new TTL and re-dates every live session from its
/// `issued_at`, so the change is retroactive rather than sliding.
///
/// The semantics are SERVER-SIDE only. Shortening takes effect for every
/// session at once, including ones that are already over the new age — the
/// next sweep drops them. Lengthening extends how long the table honours a
/// session, but the browser still holds the cookie's ORIGINAL `Max-Age`:
/// the cookie is never refreshed, so a session does not become sliding and
/// a lengthened session ends when the browser drops the cookie.
pub fn setTtl(self: *Sessions, io: std.Io, ttl_hours: u16) void {
std.debug.assert(ttl_hours > 0);
const ttl_seconds = @as(i64, ttl_hours) * 3600;
self.mutex.lockUncancelable(io);
defer self.mutex.unlock(io);
self.ttl_seconds = ttl_seconds;
for (&self.slots) |*slot| {
if (!slot.used) continue;
slot.expires_at = slot.issued_at + ttl_seconds;
}
}
/// The live TTL in seconds, for the login cookie's `Max-Age`.
pub fn ttlSeconds(self: *Sessions, io: std.Io) i64 {
self.mutex.lockUncancelable(io);
defer self.mutex.unlock(io);
return self.ttl_seconds;
}
fn findLocked(self: *Sessions, digest: [Sha256.digest_length]u8) ?*Slot {
var found: ?*Slot = null;
for (&self.slots) |*slot| {
@@ -557,6 +592,55 @@ test "a session expires at its ttl and frees its slot" {
try testing.expectEqual(@as(u32, 0), sessions.count(io, 7200));
}
test "shortening the ttl expires an over-age live session at once" {
var threaded: std.Io.Threaded = .init(testing.allocator, .{});
defer threaded.deinit();
const io = threaded.io();
// Two hours, so a session minted at 0 is live at 3_600.
var sessions: Sessions = .init(2);
const cookie = sessions.createWithToken(io, tokenOf(9), 0);
try testing.expect(sessions.validateAt(io, &cookie, 3_600));
// One hour re-dates it from `issued_at`, which puts its expiry at 3_600.
sessions.setTtl(io, 1);
try testing.expect(!sessions.validateAt(io, &cookie, 3_600));
try testing.expectEqual(@as(u32, 0), sessions.count(io, 3_600));
}
test "lengthening the ttl extends a live session server-side" {
var threaded: std.Io.Threaded = .init(testing.allocator, .{});
defer threaded.deinit();
const io = threaded.io();
var sessions: Sessions = .init(1);
const cookie = sessions.createWithToken(io, tokenOf(9), 0);
try testing.expect(!sessions.validateAt(io, &cookie, 3_600));
// Re-dating from `issued_at` rather than from now: three hours after
// minting, not three hours from here.
var extended: Sessions = .init(1);
const live = extended.createWithToken(io, tokenOf(9), 0);
extended.setTtl(io, 3);
try testing.expect(extended.validateAt(io, &live, 3_600));
try testing.expect(extended.validateAt(io, &live, 10_799));
try testing.expect(!extended.validateAt(io, &live, 10_800));
}
test "a session minted after setTtl uses the new ttl" {
var threaded: std.Io.Threaded = .init(testing.allocator, .{});
defer threaded.deinit();
const io = threaded.io();
var sessions: Sessions = .init(24);
sessions.setTtl(io, 2);
try testing.expectEqual(@as(i64, 7_200), sessions.ttlSeconds(io));
const cookie = sessions.createWithToken(io, tokenOf(4), 100);
try testing.expect(sessions.validateAt(io, &cookie, 7_299));
try testing.expect(!sessions.validateAt(io, &cookie, 7_300));
}
test "the thirty-third session evicts the least recently used one" {
var threaded: std.Io.Threaded = .init(testing.allocator, .{});
defer threaded.deinit();