PranitInitial commit439f83c3 months agoHistory
572 lines · 20.2 KB · Zig
RawDownload
const std = @import("std");const io_mod = @import("../shared/io.zig");const content_contract = @import("web_fetch_content_contract.zig");const web_fetch_artifacts = @import("../session/web_fetch_artifacts.zig"); const Allocator = std.mem.Allocator; pub const ttl_ms: i64 = 15 * 60 * 1000;pub const default_max_entries: usize = 1024;pub const default_max_metadata_bytes: usize = 4 * 1024 * 1024; pub const Clock = struct {    ctx: *anyopaque = @ptrCast(&default_clock_context),    now_fn: *const fn (*anyopaque) i64 = defaultNow,     pub fn now(self: Clock) i64 {        return self.now_fn(self.ctx);    }}; var default_clock_context: u8 = 0; fn defaultNow(_: *anyopaque) i64 {    const ts = std.Io.Clock.awake.now(io_mod.getIo());    return @intCast(@divFloor(ts.nanoseconds, 1_000_000));} pub const ArtifactRef = struct {    store_id: []const u8,    handle: []const u8,    display_path: []const u8,    byte_count: usize,}; pub const CacheEntryData = struct {    submitted_url: []const u8,    final_url: []const u8,    status: std.http.Status,    mime_type: []const u8,    content_kind: content_contract.Kind,    converted_content: []const u8 = "",    artifact_ref: ?ArtifactRef = null,}; pub const CacheSnapshot = struct {    submitted_url: []u8,    final_url: []u8,    status: std.http.Status,    mime_type: []u8,    content_kind: content_contract.Kind,    converted_content: []u8,    artifact_ref: ?OwnedArtifactRef = null,    cache_hit: bool = false,     pub const OwnedArtifactRef = struct {        store_id: []u8,        handle: []u8,        display_path: []u8,        byte_count: usize,    };     pub fn deinit(self: *CacheSnapshot, alloc: Allocator) void {        alloc.free(self.submitted_url);        alloc.free(self.final_url);        alloc.free(self.mime_type);        alloc.free(self.converted_content);        if (self.artifact_ref) |ref| {            alloc.free(ref.store_id);            alloc.free(ref.handle);            alloc.free(ref.display_path);        }        self.* = undefined;    }}; pub const Config = struct {    allocator: Allocator = std.heap.c_allocator,    clock: Clock = .{},    max_converted_bytes: usize = 50 * 1024 * 1024,    max_entries: usize = default_max_entries,    max_metadata_bytes: usize = default_max_metadata_bytes,}; pub const Runtime = struct {    config: Config,    mutex: std.Io.Mutex = .init,    entries: std.ArrayList(Entry) = .empty,    converted_bytes: usize = 0,    metadata_bytes: usize = 0,    lru_tick: u64 = 0,    lock_depth: usize = 0,     pub fn init(config: Config) Runtime {        return .{ .config = config };    }     pub fn deinit(self: *Runtime, _: Allocator) void {        const alloc = self.config.allocator;        for (self.entries.items) |*entry| entry.deinit(alloc);        self.entries.deinit(alloc);        self.* = Runtime.init(self.config);    }     pub fn lookup(self: *Runtime, alloc: Allocator, submitted_url: []const u8, artifact_store: ?*web_fetch_artifacts.Store) !?CacheSnapshot {        var snapshot: CacheSnapshot = undefined;        var artifact_handle_for_check: ?[]u8 = null;        errdefer if (artifact_handle_for_check) |handle| alloc.free(handle);         {            self.lock();            defer self.unlock();             self.evictExpiredLocked(self.config.clock.now());            const index = self.findIndexLocked(submitted_url) orelse return null;            if (self.entries.items[index].artifact_ref) |artifact_ref| {                const active_store = artifact_store orelse {                    self.removeAtLocked(index);                    return null;                };                if (!std.mem.eql(u8, artifact_ref.store_id, active_store.id())) {                    self.removeAtLocked(index);                    return null;                }                artifact_handle_for_check = try alloc.dupe(u8, artifact_ref.handle);            }             self.lru_tick += 1;            self.entries.items[index].last_used = self.lru_tick;            snapshot = try self.entries.items[index].snapshot(alloc);            snapshot.cache_hit = true;        }         if (artifact_handle_for_check) |handle| {            artifact_handle_for_check = null;            defer alloc.free(handle);            const active_store = artifact_store.?;            const exists = active_store.contains(handle) catch |err| {                snapshot.deinit(alloc);                return err;            };            if (!exists) {                snapshot.deinit(alloc);                self.removeArtifactRefIfStillMissing(submitted_url, handle);                return null;            }        }        return snapshot;    }     pub fn insert(self: *Runtime, _: Allocator, data: CacheEntryData) !void {        if (data.converted_content.len > content_contract.max_converted_content_bytes) return error.ConvertedContentTooLarge;         self.lock();        defer self.unlock();         self.evictExpiredLocked(self.config.clock.now());        if (self.findIndexLocked(data.submitted_url)) |index| self.removeAtLocked(index);         const metadata_weight = metadataWeight(data);        if (metadata_weight > self.config.max_metadata_bytes) return;        if (data.converted_content.len > self.config.max_converted_bytes) return;         self.lru_tick += 1;        const cache_alloc = self.config.allocator;        var entry = try Entry.fromData(cache_alloc, data, .{            .expires_at_ms = self.config.clock.now() + ttl_ms,            .last_used = self.lru_tick,            .metadata_weight = metadata_weight,        });        errdefer entry.deinit(cache_alloc);         try self.entries.append(cache_alloc, entry);        self.converted_bytes += entry.converted_content.len;        self.metadata_bytes += entry.metadata_weight;        self.evictOverBudgetLocked();    }     pub fn convertedBytes(self: *Runtime) usize {        self.lock();        defer self.unlock();        return self.converted_bytes;    }     pub fn entryCount(self: *Runtime) usize {        self.lock();        defer self.unlock();        return self.entries.items.len;    }     pub fn metadataBytes(self: *Runtime) usize {        self.lock();        defer self.unlock();        return self.metadata_bytes;    }     pub fn lockHeldForTest(self: *Runtime) bool {        return self.lock_depth > 0;    }     fn lock(self: *Runtime) void {        self.mutex.lockUncancelable(io_mod.getIo());        self.lock_depth += 1;    }     fn unlock(self: *Runtime) void {        self.lock_depth -= 1;        self.mutex.unlock(io_mod.getIo());    }     fn findIndexLocked(self: *Runtime, submitted_url: []const u8) ?usize {        for (self.entries.items, 0..) |entry, index| {            if (std.mem.eql(u8, entry.submitted_url, submitted_url)) return index;        }        return null;    }     fn evictExpiredLocked(self: *Runtime, now_ms: i64) void {        var index: usize = 0;        while (index < self.entries.items.len) {            if (self.entries.items[index].expires_at_ms > now_ms) {                index += 1;                continue;            }            self.removeAtLocked(index);        }    }     fn evictOverBudgetLocked(self: *Runtime) void {        while (self.entries.items.len > self.config.max_entries or            self.converted_bytes > self.config.max_converted_bytes or            self.metadata_bytes > self.config.max_metadata_bytes)        {            const index = self.leastRecentlyUsedIndexLocked() orelse return;            self.removeAtLocked(index);        }    }     fn leastRecentlyUsedIndexLocked(self: *Runtime) ?usize {        if (self.entries.items.len == 0) return null;        var selected: usize = 0;        for (self.entries.items[1..], 1..) |entry, index| {            if (entry.last_used < self.entries.items[selected].last_used) selected = index;        }        return selected;    }     fn removeAtLocked(self: *Runtime, index: usize) void {        var entry = self.entries.swapRemove(index);        self.converted_bytes -= entry.converted_content.len;        self.metadata_bytes -= entry.metadata_weight;        entry.deinit(self.config.allocator);    }     fn removeArtifactRefIfStillMissing(self: *Runtime, submitted_url: []const u8, handle: []const u8) void {        self.lock();        defer self.unlock();         const index = self.findIndexLocked(submitted_url) orelse return;        const artifact_ref = self.entries.items[index].artifact_ref orelse return;        if (std.mem.eql(u8, artifact_ref.handle, handle)) self.removeAtLocked(index);    }}; const EntryInit = struct {    expires_at_ms: i64,    last_used: u64,    metadata_weight: usize,}; const Entry = struct {    submitted_url: []u8,    final_url: []u8,    status: std.http.Status,    mime_type: []u8,    content_kind: content_contract.Kind,    converted_content: []u8,    artifact_ref: ?CacheSnapshot.OwnedArtifactRef = null,    expires_at_ms: i64,    last_used: u64,    metadata_weight: usize,     fn fromData(alloc: Allocator, data: CacheEntryData, init: EntryInit) !Entry {        const submitted_url = try alloc.dupe(u8, data.submitted_url);        errdefer alloc.free(submitted_url);        const final_url = try alloc.dupe(u8, data.final_url);        errdefer alloc.free(final_url);        const mime_type = try alloc.dupe(u8, data.mime_type);        errdefer alloc.free(mime_type);        const converted_content = try alloc.dupe(u8, data.converted_content);        errdefer alloc.free(converted_content);        const artifact_ref = if (data.artifact_ref) |ref| try cloneArtifactRef(alloc, ref) else null;        errdefer if (artifact_ref) |owned| freeArtifactRef(alloc, owned);         return .{            .submitted_url = submitted_url,            .final_url = final_url,            .status = data.status,            .mime_type = mime_type,            .content_kind = data.content_kind,            .converted_content = converted_content,            .artifact_ref = artifact_ref,            .expires_at_ms = init.expires_at_ms,            .last_used = init.last_used,            .metadata_weight = init.metadata_weight,        };    }     fn deinit(self: *Entry, alloc: Allocator) void {        alloc.free(self.submitted_url);        alloc.free(self.final_url);        alloc.free(self.mime_type);        alloc.free(self.converted_content);        if (self.artifact_ref) |ref| freeArtifactRef(alloc, ref);        self.* = undefined;    }     fn snapshot(self: Entry, alloc: Allocator) !CacheSnapshot {        const submitted_url = try alloc.dupe(u8, self.submitted_url);        errdefer alloc.free(submitted_url);        const final_url = try alloc.dupe(u8, self.final_url);        errdefer alloc.free(final_url);        const mime_type = try alloc.dupe(u8, self.mime_type);        errdefer alloc.free(mime_type);        const converted_content = try alloc.dupe(u8, self.converted_content);        errdefer alloc.free(converted_content);        const artifact_ref = if (self.artifact_ref) |ref| try cloneOwnedArtifactRef(alloc, ref) else null;        errdefer if (artifact_ref) |owned| freeArtifactRef(alloc, owned);         return .{            .submitted_url = submitted_url,            .final_url = final_url,            .status = self.status,            .mime_type = mime_type,            .content_kind = self.content_kind,            .converted_content = converted_content,            .artifact_ref = artifact_ref,        };    }}; fn metadataWeight(data: CacheEntryData) usize {    var weight: usize = 32;    weight += data.submitted_url.len;    weight += data.final_url.len;    weight += data.mime_type.len;    if (data.artifact_ref) |ref| {        weight += ref.store_id.len;        weight += ref.handle.len;        weight += ref.display_path.len;        weight += @sizeOf(usize);    }    return weight;} fn cloneArtifactRef(alloc: Allocator, ref: ArtifactRef) !CacheSnapshot.OwnedArtifactRef {    const store_id = try alloc.dupe(u8, ref.store_id);    errdefer alloc.free(store_id);    const handle = try alloc.dupe(u8, ref.handle);    errdefer alloc.free(handle);    const display_path = try alloc.dupe(u8, ref.display_path);    return .{ .store_id = store_id, .handle = handle, .display_path = display_path, .byte_count = ref.byte_count };} fn cloneOwnedArtifactRef(alloc: Allocator, ref: CacheSnapshot.OwnedArtifactRef) !CacheSnapshot.OwnedArtifactRef {    return cloneArtifactRef(alloc, .{        .store_id = ref.store_id,        .handle = ref.handle,        .display_path = ref.display_path,        .byte_count = ref.byte_count,    });} fn freeArtifactRef(alloc: Allocator, ref: CacheSnapshot.OwnedArtifactRef) void {    alloc.free(ref.store_id);    alloc.free(ref.handle);    alloc.free(ref.display_path);} const MutableClock = struct {    now_ms: i64,     fn now(raw: *anyopaque) i64 {        const self: *@This() = @ptrCast(@alignCast(raw));        return self.now_ms;    }}; fn testRuntime(clock: *MutableClock, max_converted_bytes: usize, max_entries: usize, max_metadata_bytes: usize) Runtime {    return Runtime.init(.{        .allocator = std.testing.allocator,        .clock = .{ .ctx = @ptrCast(clock), .now_fn = MutableClock.now },        .max_converted_bytes = max_converted_bytes,        .max_entries = max_entries,        .max_metadata_bytes = max_metadata_bytes,    });} test "web_fetch cache expires at fifteen minutes and evicts by converted bytes" {    const alloc = std.testing.allocator;    var clock = MutableClock{ .now_ms = 1000 };    var runtime = testRuntime(&clock, 10, 8, 4096);    defer runtime.deinit(alloc);     try runtime.insert(alloc, .{        .submitted_url = "https://example.com/a",        .final_url = "https://example.com/a",        .status = .ok,        .mime_type = "text/plain",        .content_kind = .text,        .converted_content = "12345",    });    try runtime.insert(alloc, .{        .submitted_url = "https://example.com/b",        .final_url = "https://example.com/b",        .status = .ok,        .mime_type = "text/plain",        .content_kind = .text,        .converted_content = "67890",    });    try runtime.insert(alloc, .{        .submitted_url = "https://example.com/c",        .final_url = "https://example.com/c",        .status = .ok,        .mime_type = "text/plain",        .content_kind = .text,        .converted_content = "abcde",    });     try std.testing.expectEqual(@as(usize, 2), runtime.entryCount());    try std.testing.expect((try runtime.lookup(alloc, "https://example.com/a", null)) == null);     var hit = (try runtime.lookup(alloc, "https://example.com/c", null)) orelse return error.TestExpectedEqual;    defer hit.deinit(alloc);    try std.testing.expect(hit.cache_hit);     clock.now_ms += ttl_ms + 1;    try std.testing.expect((try runtime.lookup(alloc, "https://example.com/c", null)) == null);} test "web_fetch cache snapshots remain valid after post lookup eviction" {    const alloc = std.testing.allocator;    var clock = MutableClock{ .now_ms = 1 };    var runtime = testRuntime(&clock, 6, 8, 4096);    defer runtime.deinit(alloc);     try runtime.insert(alloc, .{        .submitted_url = "https://example.com/a",        .final_url = "https://example.com/a",        .status = .ok,        .mime_type = "text/plain",        .content_kind = .text,        .converted_content = "abc",    });    var snapshot = (try runtime.lookup(alloc, "https://example.com/a", null)) orelse return error.TestExpectedEqual;    defer snapshot.deinit(alloc);     try runtime.insert(alloc, .{        .submitted_url = "https://example.com/b",        .final_url = "https://example.com/b",        .status = .ok,        .mime_type = "text/plain",        .content_kind = .text,        .converted_content = "defghi",    });     try std.testing.expect((try runtime.lookup(alloc, "https://example.com/a", null)) == null);    try std.testing.expectEqualStrings("abc", snapshot.converted_content);} test "web_fetch cache caps entry count and metadata weight" {    const alloc = std.testing.allocator;    var clock = MutableClock{ .now_ms = 1 };    var runtime = testRuntime(&clock, 1024, 2, 120);    defer runtime.deinit(alloc);     try runtime.insert(alloc, .{ .submitted_url = "https://example.com/a", .final_url = "https://example.com/a", .status = .ok, .mime_type = "text/plain", .content_kind = .text });    try runtime.insert(alloc, .{ .submitted_url = "https://example.com/b", .final_url = "https://example.com/b", .status = .ok, .mime_type = "text/plain", .content_kind = .text });    try runtime.insert(alloc, .{ .submitted_url = "https://example.com/c", .final_url = "https://example.com/c", .status = .ok, .mime_type = "text/plain", .content_kind = .text });     try std.testing.expect(runtime.entryCount() <= 2);    try std.testing.expect(runtime.metadataBytes() <= 120);} test "web_fetch durable artifact cache refs are scoped to artifact store identity" {    const alloc = std.testing.allocator;    var clock = MutableClock{ .now_ms = 1 };    var runtime = testRuntime(&clock, 1024, 8, 4096);    defer runtime.deinit(alloc);    var tmp_a = std.testing.tmpDir(.{});    defer tmp_a.cleanup();    var tmp_b = std.testing.tmpDir(.{});    defer tmp_b.cleanup();    const session_a = try io_mod.dirRealpathAlloc(alloc, tmp_a.dir, ".");    defer alloc.free(session_a);    const session_b = try io_mod.dirRealpathAlloc(alloc, tmp_b.dir, ".");    defer alloc.free(session_b);    var store_a = try web_fetch_artifacts.Store.init(alloc, session_a);    defer store_a.deinit();    var store_b = try web_fetch_artifacts.Store.init(alloc, session_b);    defer store_b.deinit();

Lines 1–500 of 572.