refactor: more refactoring to zx.Cache

This commit is contained in:
Nurul Huda (Apon) 2026-06-03 18:30:12 +06:00
parent 6923b09633
commit 05444f3583
No known key found for this signature in database
GPG Key ID: 5D3F1DE2855A2F79
9 changed files with 103 additions and 129 deletions

View File

@ -35,7 +35,7 @@ pub var host: []const u8 = "localhost:3000";
pub var current_path: []const u8 = "/";
fn settingsKV() zx.kv.KVScope {
return zx.kv.scope(settings_namespace);
return zx.kv.scoped(settings_namespace);
}
pub fn loadSettings() bool {

View File

@ -192,7 +192,7 @@ function parseEdgeMeta(stderrText: string): { status: number; headers: Headers;
* Run a WASM module for a single request using JSPI.
*
* Pass `kv` as a map of binding names KV namespaces. The Zig side selects
* a binding via `zx.kv.scope("name")`; the top-level `zx.kv.*` functions use
* a binding via `zx.kv.scoped("name")`; the top-level `zx.kv.*` functions use
* `"default"`.
*
* @example

View File

@ -101,7 +101,7 @@ fn filterUsers(allocator: std.mem.Allocator, users: *std.ArrayList(User), search
}
fn kv() zx.Kv {
return zx.kv.scope(.@"examples/form");
return zx.kv.scoped(.@"examples/form");
}
fn syncFromKv(ctx: zx.PageContext, users: *std.ArrayList(User)) void {

View File

@ -65,7 +65,7 @@ fn resolveOptions(init: zx.Init, config: Config) !Config {
};
zx.kv = kv_fs.kv();
zx.cache = try zx.Cache.init(init.io, cache_fs.kv(), .{
zx.cache = try zx.Cache.init(init.io, allocator, cache_fs.kv(), .{
.max_size = resolved.cache.max_size,
});

View File

@ -18,6 +18,9 @@ const header_len = 9;
backend: Kv = Kv.failing,
namespace: []const u8 = "default",
allocator: std.mem.Allocator,
io: std.Io,
memory: ?*cachez.Cache(CacheEntry) = null,
pub const PutOptions = Kv.PutOptions;
@ -36,31 +39,12 @@ const CacheEntry = struct {
}
};
const State = struct {
io: std.Io,
allocator: Allocator,
memory: cachez.Cache(CacheEntry),
};
const StoredEntry = struct {
expires_at: ?u64,
payload: []const u8,
};
const StateMutex = struct {
inner: std.Io.Mutex = .init,
pub fn lock(self: *@This(), io: std.Io) void {
self.inner.lockUncancelable(io);
}
pub fn unlock(self: *@This(), io: std.Io) void {
self.inner.unlock(io);
}
};
var state: ?State = null;
var state_mutex: StateMutex = .{};
pub fn init(io: std.Io, kv: Kv, config: Config) !Cache {
pub fn init(io: std.Io, allocator: Allocator, kv: Kv, config: Config) !Cache {
const cachez_config = cachez.Config{
.max_size = config.max_size,
.segment_count = config.segment_count,
@ -68,36 +52,25 @@ pub fn init(io: std.Io, kv: Kv, config: Config) !Cache {
.shrink_ratio = config.shrink_ratio,
};
const lock_io = if (state) |current| current.io else io;
state_mutex.lock(lock_io);
defer state_mutex.unlock(lock_io);
if (state) |*current| {
current.memory.deinit();
state = null;
}
state = .{
.io = io,
.allocator = zx.allocator,
.memory = try cachez.Cache(CacheEntry).init(io, zx.allocator, cachez_config),
};
const memory = try allocator.create(cachez.Cache(CacheEntry));
errdefer allocator.destroy(memory);
memory.* = try cachez.Cache(CacheEntry).init(io, allocator, cachez_config);
return .{
.backend = kv,
.namespace = "default",
.allocator = allocator,
.io = io,
.memory = memory,
};
}
/// Deinitialize the cache
pub fn deinit(_: *Cache) void {
const io = if (state) |current| current.io else return;
state_mutex.lock(io);
defer state_mutex.unlock(io);
if (state) |*current| {
current.memory.deinit();
state = null;
pub fn deinit(self: *Cache) void {
if (self.memory) |mem| {
const allocator = mem.allocator;
mem.deinit();
allocator.destroy(mem);
self.memory = null;
}
}
@ -105,14 +78,12 @@ pub fn scoped(self: Cache, comptime ns: @EnumLiteral()) Cache {
return .{
.backend = self.backend,
.namespace = @tagName(ns),
.allocator = self.allocator,
.io = self.io,
.memory = self.memory,
};
}
pub fn scope(self: Cache, comptime ns: @EnumLiteral()) Cache {
return self.scoped(ns);
}
/// The persistent backend bound to this handle's namespace.
fn boundBackend(self: Cache) Kv {
return .{
.vtable = self.backend.vtable,
@ -122,30 +93,29 @@ fn boundBackend(self: Cache) Kv {
}
pub fn get(self: Cache, allocator: std.mem.Allocator, key: []const u8) !?[]u8 {
const io = if (state) |current| current.io else return null;
state_mutex.lock(io);
defer state_mutex.unlock(io);
const io = self.io;
const memory = self.memory orelse return error.CacheUnavailable;
const s = if (state) |*current| current else return null;
const scoped_key = try scopedKey(s.allocator, self.namespace, key);
defer s.allocator.free(scoped_key);
const scoped_key = try scopedKey(allocator, self.namespace, key);
defer allocator.free(scoped_key);
if (s.memory.get(scoped_key)) |entry| {
if (memory.get(scoped_key)) |entry| {
defer entry.release();
return @as(?[]u8, try allocator.dupe(u8, entry.value.bytes));
}
const backend = self.boundBackend();
const encoded = (try backend.get(s.allocator, key)) orelse return null;
defer s.allocator.free(encoded);
const encoded = (try backend.get(allocator, key)) orelse return null;
defer allocator.free(encoded);
const decoded = try decodeStoredEntry(encoded);
if (isExpired(s.io, decoded.expires_at)) {
if (isExpired(io, decoded.expires_at)) {
try backend.delete(key);
_ = memory.del(scoped_key);
return null;
}
putMemoryEntryLocked(s, scoped_key, decoded.payload, ttlFromExpiration(s.io, decoded.expires_at)) catch {};
try putMemoryEntryLocked(self, scoped_key, decoded.payload, ttlFromExpiration(io, decoded.expires_at));
return @as(?[]u8, try allocator.dupe(u8, decoded.payload));
}
@ -166,29 +136,31 @@ pub fn as(self: Cache, allocator: std.mem.Allocator, key: []const u8, comptime T
}
pub fn put(self: Cache, key: []const u8, value: []const u8, opts: PutOptions) !void {
const io = if (state) |current| current.io else return;
state_mutex.lock(io);
defer state_mutex.unlock(io);
_ = self.memory orelse return error.CacheUnavailable;
const s = if (state) |*current| current else return;
const encoded = try encodeStoredEntry(s.io, s.allocator, value, opts);
defer s.allocator.free(encoded);
const io = self.io;
const allocator = self.allocator;
const encoded = try encodeStoredEntry(io, allocator, value, opts);
defer allocator.free(encoded);
try self.boundBackend().put(key, encoded, .{});
const scoped_key = try scopedKey(s.allocator, self.namespace, key);
defer s.allocator.free(scoped_key);
try putMemoryEntryLocked(s, scoped_key, value, ttlFromOptions(s.io, opts));
const scoped_key = try scopedKey(allocator, self.namespace, key);
defer allocator.free(scoped_key);
try putMemoryEntryLocked(self, scoped_key, value, ttlFromOptions(io, opts));
}
pub fn putAs(self: Cache, key: []const u8, value: anytype, opts: PutOptions) !void {
_ = self.memory orelse return error.CacheUnavailable;
const ValueType = @TypeOf(value);
const TypedValue = struct {
hash: u64,
value: ValueType,
};
var writer = std.Io.Writer.Allocating.init(zx.allocator);
const allocator = self.allocator;
var writer = std.Io.Writer.Allocating.init(allocator);
defer writer.deinit();
try zx.util.zxon.serialize(TypedValue{
@ -200,45 +172,39 @@ pub fn putAs(self: Cache, key: []const u8, value: anytype, opts: PutOptions) !vo
}
pub fn delete(self: Cache, key: []const u8) !void {
const io = if (state) |current| current.io else return;
state_mutex.lock(io);
defer state_mutex.unlock(io);
const memory = self.memory orelse return error.CacheUnavailable;
const allocator = self.allocator;
const scoped_key = try scopedKey(allocator, self.namespace, key);
defer allocator.free(scoped_key);
const s = if (state) |*current| current else return;
const scoped_key = try scopedKey(s.allocator, self.namespace, key);
defer s.allocator.free(scoped_key);
_ = s.memory.del(scoped_key);
_ = memory.del(scoped_key);
try self.boundBackend().delete(key);
}
pub fn list(self: Cache, allocator: std.mem.Allocator, prefix: []const u8) ![][]u8 {
const io = if (state) |current| current.io else return &[_][]u8{};
state_mutex.lock(io);
defer state_mutex.unlock(io);
const s = if (state) |*current| current else return &[_][]u8{};
const io = self.io;
const memory = self.memory orelse return error.CacheUnavailable;
const backend = self.boundBackend();
const keys = try backend.list(s.allocator, prefix);
const keys = try backend.list(allocator, prefix);
defer {
for (keys) |key| s.allocator.free(key);
s.allocator.free(keys);
for (keys) |key| allocator.free(key);
allocator.free(keys);
}
var live_keys: std.ArrayList([]u8) = .empty;
defer live_keys.deinit(allocator);
for (keys) |key| {
const encoded = (try backend.get(s.allocator, key)) orelse continue;
defer s.allocator.free(encoded);
const encoded = (try backend.get(allocator, key)) orelse continue;
defer allocator.free(encoded);
const decoded = try decodeStoredEntry(encoded);
if (isExpired(s.io, decoded.expires_at)) {
if (isExpired(io, decoded.expires_at)) {
try backend.delete(key);
const scoped_key = try scopedKey(s.allocator, self.namespace, key);
defer s.allocator.free(scoped_key);
_ = s.memory.del(scoped_key);
const scoped_key = try scopedKey(allocator, self.namespace, key);
defer allocator.free(scoped_key);
_ = memory.del(scoped_key);
continue;
}
@ -249,46 +215,40 @@ pub fn list(self: Cache, allocator: std.mem.Allocator, prefix: []const u8) ![][]
}
pub fn del(self: Cache, key: []const u8) bool {
const io = if (state) |current| current.io else return false;
state_mutex.lock(io);
defer state_mutex.unlock(io);
const s = if (state) |*current| current else return false;
const memory = self.memory orelse return false;
const allocator = self.allocator;
const backend = self.boundBackend();
const existing = backend.get(s.allocator, key) catch null;
const existing = backend.get(allocator, key) catch null;
const existed = if (existing) |bytes| blk: {
s.allocator.free(bytes);
allocator.free(bytes);
break :blk true;
} else false;
const scoped_key = scopedKey(s.allocator, self.namespace, key) catch return existed;
defer s.allocator.free(scoped_key);
const scoped_key = scopedKey(allocator, self.namespace, key) catch return existed;
defer allocator.free(scoped_key);
const memory_deleted = s.memory.del(scoped_key);
const memory_deleted = memory.del(scoped_key);
backend.delete(key) catch {};
return existed or memory_deleted;
}
pub fn delPrefix(self: Cache, prefix: []const u8) !usize {
const io = if (state) |current| current.io else return 0;
state_mutex.lock(io);
defer state_mutex.unlock(io);
const s = if (state) |*current| current else return 0;
const memory = self.memory orelse return error.CacheUnavailable;
const allocator = self.allocator;
const backend = self.boundBackend();
const keys = try backend.list(s.allocator, prefix);
const keys = try backend.list(allocator, prefix);
defer {
for (keys) |key| s.allocator.free(key);
s.allocator.free(keys);
for (keys) |key| allocator.free(key);
allocator.free(keys);
}
var deleted: usize = 0;
for (keys) |key| {
try backend.delete(key);
const scoped_key = try scopedKey(s.allocator, self.namespace, key);
defer s.allocator.free(scoped_key);
_ = s.memory.del(scoped_key);
const scoped_key = try scopedKey(allocator, self.namespace, key);
defer allocator.free(scoped_key);
_ = memory.del(scoped_key);
deleted += 1;
}
@ -354,12 +314,14 @@ fn isExpired(io: std.Io, expires_at: ?u64) bool {
return expiration <= now(io);
}
fn putMemoryEntryLocked(s: *State, scoped_key: []const u8, value: []const u8, ttl_seconds: ?u32) !void {
const value_copy = try s.allocator.dupe(u8, value);
errdefer s.allocator.free(value_copy);
fn putMemoryEntryLocked(self: Cache, scoped_key: []const u8, value: []const u8, ttl_seconds: ?u32) !void {
const io = self.io;
const memory = self.memory orelse return error.CacheUnavailable;
const value_copy = try memory.allocator.dupe(u8, value);
errdefer memory.allocator.free(value_copy);
try s.memory.put(scoped_key, .{ .bytes = value_copy }, .{
.ttl = cachezSafeTtl(s.io, ttl_seconds),
try memory.put(scoped_key, .{ .bytes = value_copy }, .{
.ttl = cachezSafeTtl(io, ttl_seconds),
});
}
@ -389,7 +351,9 @@ fn storedTypeHash(raw: []const u8) !u64 {
}
pub const failing: Cache = .{
.backend = Kv.failing,
.allocator = .failing,
.io = .failing,
.backend = .failing,
};
const cachez_failing = struct {

View File

@ -94,7 +94,7 @@ pub fn list(self: Kv, allocator: std.mem.Allocator, prefix: []const u8) ![][]u8
return self.vtable.list(self.userdata, self.namespace, allocator, prefix);
}
pub fn scope(self: Kv, comptime ns: @EnumLiteral()) Kv {
pub fn scoped(self: Kv, comptime ns: @EnumLiteral()) Kv {
return .{ .userdata = self.userdata, .vtable = self.vtable, .namespace = @tagName(ns) };
}

View File

@ -102,7 +102,11 @@ pub fn render(self: zx.Component, writer: *std.Io.Writer, options: RenderOptions
if (generated_key) |key| {
// Try to get from cache
if (try zx.cache.get(func.allocator, key)) |cached_html| {
const cached = zx.cache.get(func.allocator, key) catch |err| switch (err) {
error.CacheUnavailable => null,
else => return err,
};
if (cached) |cached_html| {
defer func.allocator.free(cached_html);
try writer.writeAll(cached_html);
return;
@ -114,7 +118,10 @@ pub fn render(self: zx.Component, writer: *std.Io.Writer, options: RenderOptions
try render(component, &buf_writer.writer, options);
const rendered = buf_writer.written();
try zx.cache.put(key, rendered, .{ .expiration_ttl = caching.seconds });
zx.cache.put(key, rendered, .{ .expiration_ttl = caching.seconds }) catch |err| switch (err) {
error.CacheUnavailable => {},
else => return err,
};
// Write to actual output
try writer.writeAll(rendered);

View File

@ -13,6 +13,9 @@ fn scopedCache(namespace: []const u8) zx.Cache {
return .{
.backend = cache.backend,
.namespace = namespace,
.allocator = cache.allocator,
.io = cache.io,
.memory = cache.memory,
};
}
@ -37,7 +40,7 @@ const WorkerContext = struct {
const worker_iterations = 100;
fn ensureCache() !void {
cache = try zx.Cache.init(std.testing.io, cache_fs.kv(), .{
cache = try zx.Cache.init(std.testing.io, allocator, cache_fs.kv(), .{
.max_size = 4096,
.segment_count = 8,
});

View File

@ -93,7 +93,7 @@ test "scoped namespaces are isolated" {
var ns_buf: [64]u8 = undefined;
const namespace = try uniqueLabel("kv-scope", &ns_buf);
// Runtime namespace: set the field directly (scope() takes a comptime literal).
// Runtime namespace: set the field directly (scoped() takes a comptime literal).
const scoped = zx.Kv{ .vtable = kv.vtable, .userdata = kv.userdata, .namespace = namespace };
const key = "shared-key";
@ -113,8 +113,8 @@ test "scoped namespaces are isolated" {
try std.testing.expectEqualStrings("default-value", default_value);
}
test "scope() enum literal routes to a distinct namespace" {
const users = kv.scope(.users);
test "scoped() enum literal routes to a distinct namespace" {
const users = kv.scoped(.users);
const key = "u-1";
defer users.delete(key) catch {};