storage: each runtime database connection keeps a page cache sized for its role, read back at open

This commit is contained in:
2026-10-21 21:40:05 +02:00
parent 8f2794533b
commit c2eac8d486
6 changed files with 161 additions and 8 deletions
+79
View File
@@ -878,6 +878,69 @@ pub fn applyPragmas(self: *Db, p: Pragmas) Error!void {
}
}
/// What a runtime connection is for, which decides how much page cache it
/// may keep.
///
/// SQLite's default is 2000 KiB per connection, and every page it caches is a
/// heap block the process keeps once the cache has filled. A page that falls
/// out of this cache is read again from the kernel's page cache, not from the
/// disk, so each role keeps only the pages its own access pattern reuses. The
/// sizes below are in 4 KiB pages, SQLite's default page size, against the
/// Pi's files: `config.db` 136 KiB, `querylog.db` 107 MiB.
pub const Role = enum {
/// 64 pages. The Pi's whole `config.db` is 34. Its one growing table, the
/// resolved diagnostics events, is capped at `events.Store.max_resolved_rows`
/// and is read only when the diagnostics page asks; the configuration and
/// the active events every request reads stay within the cache.
config,
/// 256 pages. One insert walks ten b-trees: the query log, its three
/// indexes, the domains table and its unique index, and the four bucket
/// projections. On a 107 MiB log their interior pages number about a
/// hundred; the rest holds the rightmost leaves the appends land on and
/// the leaves of the domains asked for most.
querylog_writer,
/// 64 pages. A pass runs once a day: one statement deletes the day that
/// aged out, the four projections drop their old buckets and recompute
/// the one the cutoff straddles, and every seventh pass a `VACUUM` streams
/// the whole file through the pager. The pages a pass would reuse, such as
/// the domain-index leaves its deleted rows share, come back from the
/// kernel's cache as copies, and a transaction with more dirty pages than
/// fit spills them to the WAL early. A larger cache would stay allocated
/// through the 24 hours between passes to save the pass a few copies.
querylog_retention,
/// 256 pages. The web API's history page walks the timestamp index and
/// its rows; the statistics read the bucket projections, which every
/// dashboard refresh asks for again.
querylog_reader,
pub fn cacheKib(role: Role) u32 {
return switch (role) {
.config => 256,
.querylog_writer => 1024,
.querylog_retention => 256,
.querylog_reader => 1024,
};
}
};
/// The step that makes a connection a runtime connection: sets its page cache
/// for `role` and reads it back. Called after the schema is in place, by the
/// code that opens a connection for the running server; migrations and CLI
/// commands keep SQLite's default.
pub fn initRuntimeConnection(self: *Db, role: Role) Error!void {
const kib = role.cacheKib();
var buf: [48]u8 = undefined;
// A negative `cache_size` is a size in KiB; a positive one counts pages,
// whose size depends on the database file.
const sql = std.fmt.bufPrintZ(&buf, "PRAGMA cache_size = -{d};", .{kib}) catch unreachable;
try self.exec(sql);
const set = try self.queryInt("PRAGMA cache_size");
if (set != -@as(i64, kib)) {
log.warn("PRAGMA cache_size for a {t} connection reads {d}, expected -{d}", .{ role, set, kib });
return error.SqliteError;
}
}
/// A write transaction.
///
/// Usage contract, followed everywhere in this milestone:
@@ -1168,6 +1231,22 @@ test "applyPragmas succeeds and foreign_keys reads back as 1" {
try testing.expectEqual(@as(i64, 1), try db.queryInt("PRAGMA foreign_keys"));
}
test "a runtime connection reports its role's cache size, a migration connection the default" {
var migrating = try openMemory();
defer migrating.close();
try applyPragmas(&migrating, .{});
// SQLite's compiled-in default: 2000 KiB.
try testing.expectEqual(@as(i64, -2000), try migrating.queryInt("PRAGMA cache_size"));
for (std.enums.values(Role)) |role| {
var runtime = try openMemory();
defer runtime.close();
try applyPragmas(&runtime, .{});
try initRuntimeConnection(&runtime, role);
try testing.expectEqual(-@as(i64, role.cacheKib()), try runtime.queryInt("PRAGMA cache_size"));
}
}
test "open on a directory path returns error.CantOpen and leaks no handle" {
var i: usize = 0;
while (i < 1000) : (i += 1) {