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
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:
+78
-1
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user