add max entry limiting to SqliteCache

This commit is contained in:
Muki Kiboigo committed 2026-08-06 13:29:22 -07:00
1 parent e26a200146
commit 4f5bcec9a6
2 files changed
+70 -4

No files matched your search

+65 -3
View File
@@ -41,6 +41,7 @@ pub const SqliteCache = @This();
allocator: std.mem.Allocator,
pool: Pool,
entry_limit: u32,
const cache_migrations: []const Migration = &.{
.{ .sql =
@@ -67,6 +68,7 @@ const cache_migrations: []const Migration = &.{
\\ ) strict
},
.{ .sql = "create index header_url on header(url)" },
.{ .sql = "create index cache_stored_at on cache(stored_at)" },
};
pub const SqliteCachePath = union(enum) {
@@ -85,7 +87,7 @@ pub const SqliteCachePath = union(enum) {
}
};
pub fn init(allocator: std.mem.Allocator, path: SqliteCachePath) !SqliteCache {
pub fn init(allocator: std.mem.Allocator, path: SqliteCachePath, entry_limit: u32) !SqliteCache {
var pool = switch (path) {
.memory => try Pool.init(allocator, ":memory:"),
.path => |cache_dir| blk: {
@@ -125,13 +127,25 @@ pub fn init(allocator: std.mem.Allocator, path: SqliteCachePath) !SqliteCache {
}
log.info(.cache, "sqlite cache initialized", .{ .path = path, .version = version });
return .{ .allocator = allocator, .pool = pool };
return .{ .allocator = allocator, .pool = pool, .entry_limit = entry_limit };
}
pub fn deinit(self: *SqliteCache) void {
self.pool.deinit(self.allocator);
}
fn evictOverflow(self: *SqliteCache, conn: Conn) !void {
const limit = self.entry_limit;
if (limit == 0) return;
try conn.exec(
\\ delete from cache
\\ where url not in (
\\ select url from cache order by stored_at desc limit $1
\\ )
, .{@as(i64, @intCast(limit))});
}
pub fn get(self: *SqliteCache, arena: std.mem.Allocator, req: CacheGetRequest) !CacheGetResult {
const conn = try self.pool.acquire();
defer self.pool.release(conn);
@@ -270,6 +284,7 @@ pub fn put(self: *SqliteCache, req: CachePutRequest, body: []const u8) !void {
);
}
try self.evictOverflow(conn);
try conn.commit();
log.debug(.cache, "put", .{ .url = req.url, .body_len = body.len });
@@ -376,7 +391,7 @@ pub fn renew(self: *SqliteCache, _: std.mem.Allocator, req: RenewResponse) !void
const testing = std.testing;
fn setupCache(allocator: std.mem.Allocator) !Cache {
return Cache{ .kind = .{ .sqlite = try .init(allocator, .memory) } };
return Cache{ .kind = .{ .sqlite = try .init(allocator, .memory, 0) } };
}
test "SqliteCache: Migrations" {
@@ -900,3 +915,50 @@ test "SqliteCache: renew preserves body" {
try testing.expect(result == .hit);
try testing.expectEqualStrings("original body", result.hit.data.buffer);
}
test "SqliteCache: evicts oldest entries over the limit" {
var cache = Cache{ .kind = .{ .sqlite = try .init(testing.allocator, .memory, 3) } };
defer cache.deinit();
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const now: u64 = @intCast(std.Io.Timestamp.now(testing.io, .boot).toSeconds());
const urls = [_][:0]const u8{
"https://example.com/a",
"https://example.com/b",
"https://example.com/c",
"https://example.com/d",
};
for (urls, 0..) |url, i| {
try cache.put(.{
.url = url,
.content_type = "text/html",
.status = 200,
.stored_at = now + i,
.age_at_store = 0,
.cache_control = .{ .max_age = 600 },
.headers = &.{},
.vary_headers = &.{},
}, url);
}
const evicted = try cache.get(arena.allocator(), .{
.url = "https://example.com/a",
.timestamp = now,
.request_headers = &.{},
});
try testing.expect(evicted == .miss);
for (urls[1..]) |url| {
const hit = try cache.get(arena.allocator(), .{
.url = url,
.timestamp = now + urls.len,
.request_headers = &.{},
});
try testing.expect(hit == .hit);
try testing.expectEqualStrings(url, hit.hit.data.buffer);
}
}