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:
+86
-2
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user