switch to single file cache

This commit is contained in:
Muki Kiboigo
2026-03-30 09:56:23 -07:00
parent 4f3d5c181e
commit 201fba6362
3 changed files with 94 additions and 100 deletions

View File

@@ -325,7 +325,7 @@ fn serveFromCache(req: Request, cached: *const CachedResponse) !void {
if (!proceed) { if (!proceed) {
switch (cached.data) { switch (cached.data) {
.buffer => |_| {}, .buffer => |_| {},
.file => |file| file.close(), .file => |f| f.file.close(),
} }
req.error_callback(req.ctx, error.Abort); req.error_callback(req.ctx, error.Abort);
return; return;
@@ -337,18 +337,24 @@ fn serveFromCache(req: Request, cached: *const CachedResponse) !void {
try req.data_callback(response, data); try req.data_callback(response, data);
} }
}, },
.file => |file| { .file => |f| {
const file = f.file;
defer file.close(); defer file.close();
var buf: [1024]u8 = undefined; var buf: [1024]u8 = undefined;
var file_reader = file.reader(&buf); var file_reader = file.reader(&buf);
try file_reader.seekTo(f.offset);
const reader = &file_reader.interface; const reader = &file_reader.interface;
var read_buf: [1024]u8 = undefined;
while (true) { var read_buf: [1024]u8 = undefined;
const curr = try reader.readSliceShort(&read_buf); var remaining = f.len;
if (curr == 0) break;
try req.data_callback(response, read_buf[0..curr]); while (remaining > 0) {
const read_len = @min(read_buf.len, remaining);
const n = try reader.readSliceShort(read_buf[0..read_len]);
if (n == 0) break;
remaining -= n;
try req.data_callback(response, read_buf[0..n]);
} }
}, },
} }
@@ -1133,7 +1139,7 @@ pub const Response = struct {
.transfer => |t| t.getContentLength(), .transfer => |t| t.getContentLength(),
.cached => |c| switch (c.data) { .cached => |c| switch (c.data) {
.buffer => |buf| @intCast(buf.len), .buffer => |buf| @intCast(buf.len),
.file => |f| @intCast(f.getEndPos() catch 0), .file => |f| @intCast(f.len),
}, },
}; };
} }

View File

@@ -137,7 +137,11 @@ pub const CacheRequest = struct {
pub const CachedData = union(enum) { pub const CachedData = union(enum) {
buffer: []const u8, buffer: []const u8,
file: std.fs.File, file: struct {
file: std.fs.File,
offset: usize,
len: usize,
},
}; };
pub const CachedResponse = struct { pub const CachedResponse = struct {

View File

@@ -34,7 +34,7 @@ pub const FsCache = @This();
dir: std.fs.Dir, dir: std.fs.Dir,
locks: [LOCK_STRIPES]std.Thread.Mutex = .{std.Thread.Mutex{}} ** LOCK_STRIPES, locks: [LOCK_STRIPES]std.Thread.Mutex = .{std.Thread.Mutex{}} ** LOCK_STRIPES,
const CacheMetadataFile = struct { const CacheMetadataJson = struct {
version: usize, version: usize,
metadata: CachedMetadata, metadata: CachedMetadata,
}; };
@@ -44,8 +44,9 @@ fn getLockPtr(self: *FsCache, key: *const [HASHED_KEY_LEN]u8) *std.Thread.Mutex
return &self.locks[lock_idx]; return &self.locks[lock_idx];
} }
const BODY_LEN_HEADER_LEN = 8;
const HASHED_KEY_LEN = 64; const HASHED_KEY_LEN = 64;
const HASHED_PATH_LEN = HASHED_KEY_LEN + 5; const HASHED_PATH_LEN = HASHED_KEY_LEN + 6;
const HASHED_TMP_PATH_LEN = HASHED_PATH_LEN + 4; const HASHED_TMP_PATH_LEN = HASHED_PATH_LEN + 4;
fn hashKey(key: []const u8) [HASHED_KEY_LEN]u8 { fn hashKey(key: []const u8) [HASHED_KEY_LEN]u8 {
@@ -56,27 +57,15 @@ fn hashKey(key: []const u8) [HASHED_KEY_LEN]u8 {
return hex; return hex;
} }
fn metaPath(hashed_key: *const [HASHED_KEY_LEN]u8) [HASHED_PATH_LEN]u8 { fn cachePath(hashed_key: *const [HASHED_KEY_LEN]u8) [HASHED_PATH_LEN]u8 {
var path: [HASHED_PATH_LEN]u8 = undefined; var path: [HASHED_PATH_LEN]u8 = undefined;
_ = std.fmt.bufPrint(&path, "{s}.meta", .{hashed_key}) catch unreachable; _ = std.fmt.bufPrint(&path, "{s}.cache", .{hashed_key}) catch unreachable;
return path; return path;
} }
fn bodyPath(hashed_key: *const [HASHED_KEY_LEN]u8) [HASHED_PATH_LEN]u8 { fn cacheTmpPath(hashed_key: *const [HASHED_KEY_LEN]u8) [HASHED_TMP_PATH_LEN]u8 {
var path: [HASHED_PATH_LEN]u8 = undefined;
_ = std.fmt.bufPrint(&path, "{s}.body", .{hashed_key}) catch unreachable;
return path;
}
fn metaTmpPath(hashed_key: *const [HASHED_KEY_LEN]u8) [HASHED_TMP_PATH_LEN]u8 {
var path: [HASHED_TMP_PATH_LEN]u8 = undefined; var path: [HASHED_TMP_PATH_LEN]u8 = undefined;
_ = std.fmt.bufPrint(&path, "{s}.meta.tmp", .{hashed_key}) catch unreachable; _ = std.fmt.bufPrint(&path, "{s}.cache.tmp", .{hashed_key}) catch unreachable;
return path;
}
fn bodyTmpPath(hashed_key: *const [HASHED_KEY_LEN]u8) [HASHED_TMP_PATH_LEN]u8 {
var path: [HASHED_TMP_PATH_LEN]u8 = undefined;
_ = std.fmt.bufPrint(&path, "{s}.body.tmp", .{hashed_key}) catch unreachable;
return path; return path;
} }
@@ -98,106 +87,96 @@ pub fn deinit(self: *FsCache) void {
pub fn get(self: *FsCache, arena: std.mem.Allocator, req: CacheRequest) ?Cache.CachedResponse { pub fn get(self: *FsCache, arena: std.mem.Allocator, req: CacheRequest) ?Cache.CachedResponse {
const hashed_key = hashKey(req.url); const hashed_key = hashKey(req.url);
const meta_p = metaPath(&hashed_key); const cache_p = cachePath(&hashed_key);
const body_p = bodyPath(&hashed_key);
const lock = self.getLockPtr(&hashed_key); const lock = self.getLockPtr(&hashed_key);
lock.lock(); lock.lock();
defer lock.unlock(); defer lock.unlock();
const meta_file = self.dir.openFile(&meta_p, .{ .mode = .read_only }) catch return null; const file = self.dir.openFile(&cache_p, .{ .mode = .read_only }) catch return null;
defer meta_file.close(); errdefer file.close();
const contents = meta_file.readToEndAlloc(arena, 1 * 1024 * 1024) catch return null; var file_buf: [1024]u8 = undefined;
defer arena.free(contents); var len_buf: [BODY_LEN_HEADER_LEN]u8 = undefined;
const cache_file: CacheMetadataFile = std.json.parseFromSliceLeaky( var file_reader = file.reader(&file_buf);
CacheMetadataFile, const file_reader_iface = &file_reader.interface;
file_reader_iface.readSliceAll(&len_buf) catch return null;
const body_len = std.mem.readInt(u64, &len_buf, .little);
// Now we read metadata.
file_reader.seekTo(body_len + BODY_LEN_HEADER_LEN) catch return null;
var json_reader = std.json.Reader.init(arena, file_reader_iface);
const cache_file: CacheMetadataJson = std.json.parseFromTokenSourceLeaky(
CacheMetadataJson,
arena, arena,
contents, &json_reader,
.{ .allocate = .alloc_always }, .{
.allocate = .alloc_always,
},
) catch { ) catch {
self.dir.deleteFile(&meta_p) catch {}; self.dir.deleteFile(&cache_p) catch {};
self.dir.deleteFile(&body_p) catch {};
return null; return null;
}; };
const metadata = cache_file.metadata;
if (cache_file.version != CACHE_VERSION) { if (cache_file.version != CACHE_VERSION) {
self.dir.deleteFile(&meta_p) catch {}; self.dir.deleteFile(&cache_p) catch {};
self.dir.deleteFile(&body_p) catch {};
return null; return null;
} }
const metadata = cache_file.metadata;
const now = req.timestamp; const now = req.timestamp;
const age = (now - metadata.stored_at) + @as(i64, @intCast(metadata.age_at_store)); const age = (now - metadata.stored_at) + @as(i64, @intCast(metadata.age_at_store));
if (age < 0 or @as(u64, @intCast(age)) >= metadata.cache_control.max_age) { if (age < 0 or @as(u64, @intCast(age)) >= metadata.cache_control.max_age) {
self.dir.deleteFile(&meta_p) catch {}; self.dir.deleteFile(&cache_p) catch {};
self.dir.deleteFile(&body_p) catch {};
return null; return null;
} }
const body_file = self.dir.openFile(
&body_p,
.{ .mode = .read_only },
) catch return null;
return .{ return .{
.metadata = metadata, .metadata = metadata,
.data = .{ .file = body_file }, .data = .{
.file = .{
.file = file,
.offset = BODY_LEN_HEADER_LEN,
.len = body_len,
},
},
}; };
} }
pub fn put(self: *FsCache, meta: CachedMetadata, body: []const u8) !void { pub fn put(self: *FsCache, meta: CachedMetadata, body: []const u8) !void {
const hashed_key = hashKey(meta.url); const hashed_key = hashKey(meta.url);
const meta_p = metaPath(&hashed_key); const cache_p = cachePath(&hashed_key);
const meta_tmp_p = metaTmpPath(&hashed_key); const cache_tmp_p = cacheTmpPath(&hashed_key);
const body_p = bodyPath(&hashed_key);
const body_tmp_p = bodyTmpPath(&hashed_key);
var writer_buf: [512]u8 = undefined;
const lock = self.getLockPtr(&hashed_key); const lock = self.getLockPtr(&hashed_key);
lock.lock(); lock.lock();
defer lock.unlock(); defer lock.unlock();
{ const file = try self.dir.createFile(&cache_tmp_p, .{});
const meta_file = try self.dir.createFile(&meta_tmp_p, .{}); defer file.close();
errdefer {
meta_file.close();
self.dir.deleteFile(&meta_tmp_p) catch {};
}
var meta_file_writer = meta_file.writer(&writer_buf); var writer_buf: [1024]u8 = undefined;
const meta_file_writer_iface = &meta_file_writer.interface;
try std.json.Stringify.value(
CacheMetadataFile{ .version = CACHE_VERSION, .metadata = meta },
.{ .whitespace = .minified },
meta_file_writer_iface,
);
try meta_file_writer_iface.flush();
meta_file.close();
}
errdefer self.dir.deleteFile(&meta_tmp_p) catch {};
try self.dir.rename(&meta_tmp_p, &meta_p);
{ var file_writer = file.writer(&writer_buf);
const body_file = try self.dir.createFile(&body_tmp_p, .{}); var file_writer_iface = &file_writer.interface;
errdefer {
body_file.close();
self.dir.deleteFile(&body_tmp_p) catch {};
}
var body_file_writer = body_file.writer(&writer_buf); var len_buf: [8]u8 = undefined;
const body_file_writer_iface = &body_file_writer.interface; std.mem.writeInt(u64, &len_buf, body.len, .little);
try body_file_writer_iface.writeAll(body); try file_writer_iface.writeAll(&len_buf);
try body_file_writer_iface.flush(); try file_writer_iface.writeAll(body);
body_file.close();
}
errdefer self.dir.deleteFile(&body_tmp_p) catch {};
errdefer self.dir.deleteFile(&meta_p) catch {}; try std.json.Stringify.value(
try self.dir.rename(&body_tmp_p, &body_p); CacheMetadataJson{ .version = CACHE_VERSION, .metadata = meta },
.{ .whitespace = .minified },
file_writer_iface,
);
try file_writer_iface.flush();
try self.dir.rename(&cache_tmp_p, &cache_p);
} }
const testing = std.testing; const testing = std.testing;
@@ -209,9 +188,9 @@ test "FsCache: basic put and get" {
const path = try tmp.dir.realpathAlloc(testing.allocator, "."); const path = try tmp.dir.realpathAlloc(testing.allocator, ".");
defer testing.allocator.free(path); defer testing.allocator.free(path);
var fs_cache = try FsCache.init(path); const fs_cache = try FsCache.init(path);
defer fs_cache.deinit();
var cache = Cache{ .kind = .{ .fs = fs_cache } }; var cache = Cache{ .kind = .{ .fs = fs_cache } };
defer cache.deinit();
var arena = std.heap.ArenaAllocator.init(testing.allocator); var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit(); defer arena.deinit();
@@ -233,15 +212,20 @@ test "FsCache: basic put and get" {
const body = "hello world"; const body = "hello world";
try cache.put(meta, body); try cache.put(meta, body);
const result = cache.get(arena.allocator(), .{ .url = "https://example.com", .timestamp = now }) orelse return error.CacheMiss; const result = cache.get(
defer result.data.file.close(); arena.allocator(),
.{ .url = "https://example.com", .timestamp = now },
) orelse return error.CacheMiss;
const f = result.data.file;
const file = f.file;
defer file.close();
var buf: [64]u8 = undefined; var buf: [64]u8 = undefined;
var file_reader = result.data.file.reader(&buf); var file_reader = file.reader(&buf);
try file_reader.seekTo(f.offset);
const read_buf = try file_reader.interface.allocRemaining(testing.allocator, .unlimited); const read_buf = try file_reader.interface.readAlloc(testing.allocator, f.len);
defer testing.allocator.free(read_buf); defer testing.allocator.free(read_buf);
try testing.expectEqualStrings(body, read_buf); try testing.expectEqualStrings(body, read_buf);
} }
@@ -252,9 +236,9 @@ test "FsCache: get expiration" {
const path = try tmp.dir.realpathAlloc(testing.allocator, "."); const path = try tmp.dir.realpathAlloc(testing.allocator, ".");
defer testing.allocator.free(path); defer testing.allocator.free(path);
var fs_cache = try FsCache.init(path); const fs_cache = try FsCache.init(path);
defer fs_cache.deinit();
var cache = Cache{ .kind = .{ .fs = fs_cache } }; var cache = Cache{ .kind = .{ .fs = fs_cache } };
defer cache.deinit();
var arena = std.heap.ArenaAllocator.init(testing.allocator); var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit(); defer arena.deinit();
@@ -282,7 +266,7 @@ test "FsCache: get expiration" {
arena.allocator(), arena.allocator(),
.{ .url = "https://example.com", .timestamp = now + 50 }, .{ .url = "https://example.com", .timestamp = now + 50 },
) orelse return error.CacheMiss; ) orelse return error.CacheMiss;
result.data.file.close(); result.data.file.file.close();
try testing.expectEqual(null, cache.get( try testing.expectEqual(null, cache.get(
arena.allocator(), arena.allocator(),