overview: one endpoint, live projections and a response cache (m36)
Gates / frontend (push) Successful in 2m6s
Gates / test (push) Successful in 2m57s
Gates / test-aarch64 (push) Successful in 8m31s
Gates / package (push) Successful in 4m19s
Gates / container (push) Failing after 2s
CI / gates (push) Failing after 26m21s

This commit is contained in:
2026-08-27 17:48:20 +02:00
parent a261dc2aa3
commit 6a0630c288
40 changed files with 3298 additions and 2242 deletions
+78 -1
View File
@@ -175,6 +175,67 @@ pub const UpstreamBuild = struct {
bundle_lock: *std.Io.RwLock,
};
/// The Overview response cache: one already-serialized body per period.
///
/// A slot is valid for exactly one `(window.until, data_version)` pair, so it
/// expires both ways a stale Overview can arise — the window rolls onto the
/// next bucket, or another connection (the logger, retention) commits and moves
/// `PRAGMA data_version`. There is no time-to-live and no background refresh:
/// nothing here can serve bytes that describe a database state the reader could
/// not have seen.
///
/// Every field is read and written under `WebState.querylog_lock`, which is
/// also what makes the cache single-flight: a second request for the same key
/// waits for the first rebuild and then hits. The type carries no lock of its
/// own precisely so that nobody can touch it without the one that matters.
pub const OverviewCache = struct {
/// One per `overview.Period`, indexed by `@intFromEnum`. The handler asserts
/// the two counts agree.
pub const slot_count = 4;
const Slot = struct {
/// Empty until the first successful build; never a valid empty body,
/// since every response carries at least the period and the window.
body: []u8 = &.{},
until: i64 = 0,
data_version: i64 = 0,
};
slots: [slot_count]Slot = @splat(.{}),
/// The stored bytes for this key, or null. The caller copies them into its
/// request arena before releasing the lock: a later rebuild frees this
/// allocation.
pub fn get(self: *const OverviewCache, period_index: usize, until: i64, data_version: i64) ?[]const u8 {
const slot = &self.slots[period_index];
if (slot.body.len == 0) return null;
if (slot.until != until or slot.data_version != data_version) return null;
return slot.body;
}
/// Takes ownership of `body`, which must be a `gpa` allocation, and frees
/// whatever the slot held.
pub fn put(
self: *OverviewCache,
gpa: Allocator,
period_index: usize,
until: i64,
data_version: i64,
body: []u8,
) void {
const slot = &self.slots[period_index];
gpa.free(slot.body);
slot.* = .{ .body = body, .until = until, .data_version = data_version };
}
pub fn deinit(self: *OverviewCache, gpa: Allocator) void {
for (&self.slots) |*slot| {
gpa.free(slot.body);
slot.* = .{};
}
}
};
pub const WebState = struct {
gpa: Allocator,
web: model.Web = .{},
@@ -272,6 +333,9 @@ pub const WebState = struct {
/// the shared connection would fail and a third task's reads would land
/// inside someone else's snapshot.
querylog_lock: std.Io.Mutex = .init,
/// The Overview response cache, guarded by `querylog_lock` above. Whoever
/// owns the `WebState` calls `overview_cache.deinit`.
overview_cache: OverviewCache = .{},
/// The diagnostics event store, which owns a third connection of its own
/// and serializes every access — read and write — through its mutex. Null
/// when `Store.init` failed, which `/api/health` reports as `unavailable`
@@ -350,11 +414,24 @@ pub const QuerylogRead = struct {
pub fn open(state: *WebState, io: std.Io, database: *db.Db) db.Error!QuerylogRead {
state.querylog_lock.lockUncancelable(io);
errdefer state.querylog_lock.unlock(io);
var scope = try openLocked(state, io, database);
scope.held = true;
return scope;
}
/// The transaction alone, for a caller that already holds `querylog_lock`
/// and keeps holding it past `commit` — the overview handler, which decides
/// its response cache under the same one hold. Calling `open` there would
/// deadlock on a mutex the task already owns.
///
/// The returned scope releases nothing: `commit` and `abort` end the
/// transaction and leave the lock to whoever took it.
pub fn openLocked(state: *WebState, io: std.Io, database: *db.Db) db.Error!QuerylogRead {
return .{
.state = state,
.io = io,
.tx = try db.ReadTx.begin(database),
.held = true,
.held = false,
};
}