wttr/src/cache/Lru.zig

154 lines
3.9 KiB
Zig

const std = @import("std");
const Lru = @This();
allocator: std.mem.Allocator,
map: std.StringHashMap(Entry),
max_entries: usize,
evict_fn: ?*const fn (ctx: *anyopaque, key: []const u8) void = null,
evict_ctx: ?*anyopaque = null,
const Entry = struct {
value: []const u8,
expires: i64,
access_count: u64,
};
pub fn init(allocator: std.mem.Allocator, max_entries: usize) !Lru {
return .{
.allocator = allocator,
.map = std.StringHashMap(Entry).init(allocator),
.max_entries = max_entries,
};
}
pub fn setEvictionCallback(self: *Lru, ctx: *anyopaque, callback: *const fn (ctx: *anyopaque, key: []const u8) void) void {
self.evict_ctx = ctx;
self.evict_fn = callback;
}
pub fn get(self: *Lru, key: []const u8) ?[]const u8 {
var entry = self.map.getPtr(key) orelse return null;
const now = std.time.milliTimestamp();
if (now > entry.expires) {
self.remove(key);
return null;
}
entry.access_count += 1;
return entry.value;
}
pub fn put(self: *Lru, key: []const u8, value: []const u8, expires: i64) !void {
if (self.map.get(key)) |old_entry| {
self.allocator.free(old_entry.value);
_ = self.map.remove(key);
}
if (self.map.count() >= self.max_entries) {
self.evictOldest();
}
const key_copy = try self.allocator.dupe(u8, key);
const value_copy = try self.allocator.dupe(u8, value);
try self.map.put(key_copy, .{
.value = value_copy,
.expires = expires,
.access_count = 0,
});
}
fn evictOldest(self: *Lru) void {
var oldest_key: ?[]const u8 = null;
var oldest_access: u64 = std.math.maxInt(u64);
var it = self.map.iterator();
while (it.next()) |entry| {
if (entry.value_ptr.access_count < oldest_access) {
oldest_access = entry.value_ptr.access_count;
oldest_key = entry.key_ptr.*;
}
}
if (oldest_key) |key| {
self.remove(key);
}
}
fn remove(self: *Lru, key: []const u8) void {
if (self.map.fetchRemove(key)) |kv| {
if (self.evict_fn) |callback| {
callback(self.evict_ctx.?, kv.key);
}
self.allocator.free(kv.value.value);
self.allocator.free(kv.key);
}
}
pub fn deinit(self: *Lru) void {
var it = self.map.iterator();
while (it.next()) |entry| {
self.allocator.free(entry.value_ptr.value);
self.allocator.free(entry.key_ptr.*);
}
self.map.deinit();
}
pub const Iterator = struct {
inner: std.StringHashMap(Entry).Iterator,
pub const Item = struct {
key: []const u8,
value: []const u8,
expires: i64,
};
pub fn next(self: *Iterator) ?Item {
const entry = self.inner.next() orelse return null;
return Item{
.key = entry.key_ptr.*,
.value = entry.value_ptr.value,
.expires = entry.value_ptr.expires,
};
}
};
pub fn iterator(self: *Lru) Iterator {
return .{ .inner = self.map.iterator() };
}
test "LRU basic operations" {
var lru = try Lru.init(std.testing.allocator, 3);
defer lru.deinit();
try lru.put("key1", "value1", 9999999999999);
try std.testing.expectEqualStrings("value1", lru.get("key1").?);
}
test "LRU eviction" {
var lru = try Lru.init(std.testing.allocator, 2);
defer lru.deinit();
try lru.put("key1", "value1", 9999999999999);
try lru.put("key2", "value2", 9999999999999);
try lru.put("key3", "value3", 9999999999999);
try std.testing.expect(lru.get("key1") == null);
}
test "LRU expired entry returns null" {
var lru = try Lru.init(std.testing.allocator, 10);
defer lru.deinit();
// Put item with past expiration time
const now = std.time.milliTimestamp();
const past_expires = now - 1000;
try lru.put("key1", "value1", past_expires);
// Get should return null for expired entry
const result = lru.get("key1");
try std.testing.expect(result == null);
}