lib/zen/src/site/page/storage.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const alloc_phase = @import("alloc_phase");
  3 
  4 const capacity_mod = @import("capacity.zig");
  5 const model = @import("model.zig");
  6 
  7 pub const Status = struct {
  8     phase: alloc_phase.capacity.Phase,
  9     in_use: bool,
 10     storage_bytes: usize,
 11     max_page_bytes: usize,
 12     loaded_page_bytes: usize,
 13     high_water_page_bytes: usize,
 14     rejected_page_count: u64,
 15 };
 16 
 17 pub const Storage = struct {
 18     phase: alloc_phase.capacity.Phase,
 19     capacity: capacity_mod.Capacity,
 20     bytes: []u8,
 21     in_use: bool = false,
 22     loaded_page_bytes: usize = 0,
 23     high_water_page_bytes: usize = 0,
 24     rejected_page_count: u64 = 0,
 25 
 26     pub const Limits: type = capacity_mod.Limits;
 27     pub const Capacity: type = capacity_mod.Capacity;
 28     pub const Exhaustion: type = model.Exhaustion;
 29     pub const InitError = std.mem.Allocator.Error || capacity_mod.DeriveError;
 30 
 31     pub const claim: alloc_phase.capacity.Declaration = .{
 32         .source = .{
 33             .id = "zen.site_page_storage",
 34             .kind = .phase_static,
 35             .limit_source = .caller,
 36             .storage = .{
 37                 .covered = &.{
 38                     .{
 39                         .id = "one_reusable_surveyed_markdown_page_plus_overload_lookahead_byte",
 40                         .lifetime = .steady,
 41                         .detail = "one reusable surveyed Markdown page plus overload lookahead byte",
 42                     },
 43                 },
 44                 .excluded = &.{
 45                     "caller-owned page paths, source directory capability, and kernel file state",
 46                     "frontmatter, retained site catalog, rendered page, theme, asset, and filesystem output owners",
 47                 },
 48             },
 49             .capacity = .{
 50                 .inputs = &.{
 51                     alloc_phase.capacity.bindInput(Limits, "max_page_bytes", "max_page_bytes"),
 52                 },
 53                 .type_selectors = &.{},
 54                 .nodes = &.{
 55                     .{ .input = 0 },
 56                     .{ .constant = 1 },
 57                     .{ .add = .{ .left = 0, .right = 1 } },
 58                 },
 59                 .assertions = &.{.{
 60                     .scope = .closure_total,
 61                     .measure = .retained,
 62                     .relation = .exact,
 63                     .expression = 2,
 64                 }},
 65             },
 66             .overload = .{
 67                 .kind = .drop,
 68                 .detail = "an oversized or concurrently grown page is rejected before parser mutation and the reusable input region remains available",
 69             },
 70             .risks = .{
 71                 .transitive = .{
 72                     .status = .witnessed,
 73                     .detail = "activated page acquisition uses only the preacquired byte region and consumers release every borrow before reuse",
 74                 },
 75                 .foreign = .{
 76                     .status = .excluded,
 77                     .detail = "directory reads and kernel file state remain effects outside the reusable page input owner",
 78                 },
 79             },
 80             .obligations = &.{
 81                 .{ .key = "zen_site_page_capacity", .role = .capacity_model },
 82                 .{ .key = "zen_site_page_acquisition", .role = .custom },
 83                 .{ .key = "zen_site_page_oom", .role = .custom },
 84                 .{ .key = "zen_site_page_boundaries", .role = .overload },
 85                 .{ .key = "zen_site_page_growth", .role = .overload },
 86                 .{ .key = "zen_site_page_reuse", .role = .custom },
 87                 .{ .key = "zen_site_page_sealed", .role = .transitive_risk },
 88                 .{ .key = "zen_site_page_root", .role = .custom },
 89                 .{ .key = "zen_site_page_consumer_transitive_risk", .role = .transitive_risk },
 90                 .{ .key = "zen_site_page_consumer_foreign_risk", .role = .foreign_risk },
 91             },
 92         },
 93         .bindings = .{
 94             .owner = @This(),
 95             .seal = .{
 96                 .family = alloc_phase.capacity.selector(@This().activate),
 97                 .premise = .{
 98                     .class = .checked_semantic_fact,
 99                     .authority = .checker,
100                 },
101             },
102             .teardown = .{
103                 .family = alloc_phase.capacity.selector(@This().deinit),
104                 .premise = .{
105                     .class = .checked_semantic_fact,
106                     .authority = .checker,
107                 },
108             },
109         },
110     };
111 
112     pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!Storage {
113         const capacity = try Capacity.derive(limits);
114         const bytes = try allocator.alloc(u8, capacity.storage_bytes);
115         return .{
116             .phase = .initialization,
117             .capacity = capacity,
118             .bytes = bytes,
119         };
120     }
121 
122     pub fn activate(self: *Storage) void {
123         std.debug.assert(self.phase == .initialization);
124         std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
125         std.debug.assert(self.bytes.len > self.capacity.page_bytes);
126         self.phase = .steady;
127     }
128 
129     pub fn readFile(
130         self: *Storage,
131         directory: std.Io.Dir,
132         io: std.Io,
133         relative_path: []const u8,
134     ) ![]const u8 {
135         try self.requireAvailable();
136         const page = directory.readFile(io, relative_path, self.bytes) catch |err| switch (err) {
137             error.FileTooBig => return self.reject(),
138             else => return err,
139         };
140         return try self.admit(page.len);
141     }
142 
143     pub fn release(self: *Storage) void {
144         std.debug.assert(self.phase == .steady);
145         std.debug.assert(self.in_use);
146         self.in_use = false;
147         self.loaded_page_bytes = 0;
148     }
149 
150     pub fn status(self: *const Storage) Status {
151         return .{
152             .phase = self.phase,
153             .in_use = self.in_use,
154             .storage_bytes = self.capacity.storage_bytes,
155             .max_page_bytes = self.capacity.page_bytes,
156             .loaded_page_bytes = self.loaded_page_bytes,
157             .high_water_page_bytes = self.high_water_page_bytes,
158             .rejected_page_count = self.rejected_page_count,
159         };
160     }
161 
162     pub fn deinit(self: *Storage, allocator: std.mem.Allocator) void {
163         std.debug.assert(self.phase != .teardown);
164         std.debug.assert(!self.in_use);
165         std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
166         self.phase = .teardown;
167         allocator.free(self.bytes);
168         self.bytes = &.{};
169         self.loaded_page_bytes = 0;
170     }
171 
172     fn requireAvailable(self: *Storage) Exhaustion!void {
173         std.debug.assert(self.phase == .steady);
174         std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
175         std.debug.assert(self.loaded_page_bytes <= self.capacity.page_bytes);
176         if (self.in_use) return error.SitePageStorageInUse;
177         std.debug.assert(self.loaded_page_bytes == 0);
178     }
179 
180     fn admit(self: *Storage, page_bytes: usize) Exhaustion![]const u8 {
181         std.debug.assert(self.phase == .steady);
182         std.debug.assert(!self.in_use);
183         std.debug.assert(page_bytes <= self.capacity.storage_bytes);
184         if (page_bytes > self.capacity.page_bytes) return self.reject();
185         self.in_use = true;
186         self.loaded_page_bytes = page_bytes;
187         self.high_water_page_bytes = @max(self.high_water_page_bytes, page_bytes);
188         return self.bytes[0..page_bytes];
189     }
190 
191     fn reject(self: *Storage) Exhaustion {
192         std.debug.assert(self.phase == .steady);
193         std.debug.assert(!self.in_use);
194         std.debug.assert(self.loaded_page_bytes == 0);
195         self.rejected_page_count +|= 1;
196         return error.SitePageCapacityExceeded;
197     }
198 };
199 
200 fn checkInitFailures(allocator: std.mem.Allocator) !void {
201     var storage = try Storage.init(allocator, .{ .max_page_bytes = 19 });
202     storage.deinit(allocator);
203 }
204 
205 test "site page storage acquires one exact surveyed region" {
206     comptime {
207         @stardustClaim(
208             @import("alloc_phase").capacity.witness(Storage, "zen_site_page_acquisition"),
209             null,
210             null,
211             null,
212             null,
213             null,
214             null,
215         );
216     }
217 
218     var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
219     var storage = try Storage.init(counting.allocator(), .{ .max_page_bytes = 714_460 });
220     defer storage.deinit(counting.allocator());
221     try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
222     try std.testing.expectEqual(@as(usize, 0), counting.resize_index);
223     try std.testing.expectEqual(@as(usize, 714_461), counting.allocated_bytes);
224     storage.activate();
225 }
226 
227 test "site page storage retries after every allocation failure" {
228     comptime {
229         @stardustClaim(
230             @import("alloc_phase").capacity.witness(Storage, "zen_site_page_oom"),
231             null,
232             null,
233             null,
234             null,
235             null,
236             null,
237         );
238     }
239 
240     try std.testing.checkAllAllocationFailures(std.testing.allocator, checkInitFailures, .{});
241 }
242 
243 test "site page storage accepts the exact limit and rejects max plus one" {
244     comptime {
245         @stardustClaim(
246             @import("alloc_phase").capacity.witness(Storage, "zen_site_page_boundaries"),
247             null,
248             null,
249             null,
250             null,
251             null,
252             null,
253         );
254     }
255 
256     var temporary = std.testing.tmpDir(.{});
257     defer temporary.cleanup();
258     const io = std.Options.debug_io;
259     var storage = try Storage.init(std.testing.allocator, .{ .max_page_bytes = 5 });
260     defer storage.deinit(std.testing.allocator);
261     storage.activate();
262 
263     try temporary.dir.writeFile(io, .{ .sub_path = "exact.md", .data = "abcde" });
264     const exact = try storage.readFile(temporary.dir, io, "exact.md");
265     try std.testing.expectEqualStrings("abcde", exact);
266     storage.release();
267 
268     try temporary.dir.writeFile(io, .{ .sub_path = "oversized.md", .data = "abcdef" });
269     try std.testing.expectError(
270         error.SitePageCapacityExceeded,
271         storage.readFile(temporary.dir, io, "oversized.md"),
272     );
273     try std.testing.expectEqual(@as(u64, 1), storage.status().rejected_page_count);
274 }
275 
276 test "site page storage recovers after concurrent file growth" {
277     comptime {
278         @stardustClaim(
279             @import("alloc_phase").capacity.witness(Storage, "zen_site_page_growth"),
280             null,
281             null,
282             null,
283             null,
284             null,
285             null,
286         );
287     }
288 
289     var temporary = std.testing.tmpDir(.{});
290     defer temporary.cleanup();
291     const io = std.Options.debug_io;
292     var storage = try Storage.init(std.testing.allocator, .{ .max_page_bytes = 3 });
293     defer storage.deinit(std.testing.allocator);
294     storage.activate();
295 
296     try temporary.dir.writeFile(io, .{ .sub_path = "page.md", .data = "grew" });
297     try std.testing.expectError(
298         error.SitePageCapacityExceeded,
299         storage.readFile(temporary.dir, io, "page.md"),
300     );
301     try temporary.dir.writeFile(io, .{ .sub_path = "page.md", .data = "new" });
302     const recovered = try storage.readFile(temporary.dir, io, "page.md");
303     try std.testing.expectEqualStrings("new", recovered);
304     storage.release();
305 }
306 
307 test "site page borrow requires release before address reuse" {
308     comptime {
309         @stardustClaim(
310             @import("alloc_phase").capacity.witness(Storage, "zen_site_page_reuse"),
311             null,
312             null,
313             null,
314             null,
315             null,
316             null,
317         );
318     }
319 
320     var temporary = std.testing.tmpDir(.{});
321     defer temporary.cleanup();
322     const io = std.Options.debug_io;
323     var storage = try Storage.init(std.testing.allocator, .{ .max_page_bytes = 5 });
324     defer storage.deinit(std.testing.allocator);
325     storage.activate();
326 
327     try temporary.dir.writeFile(io, .{ .sub_path = "first.md", .data = "first" });
328     try temporary.dir.writeFile(io, .{ .sub_path = "next.md", .data = "next" });
329     const first = try storage.readFile(temporary.dir, io, "first.md");
330     try std.testing.expectError(
331         error.SitePageStorageInUse,
332         storage.readFile(temporary.dir, io, "next.md"),
333     );
334     const address = first.ptr;
335     storage.release();
336     const next = try storage.readFile(temporary.dir, io, "next.md");
337     try std.testing.expectEqual(address, next.ptr);
338     storage.release();
339 }
340 
341 test "site page rejection telemetry saturates" {
342     var temporary = std.testing.tmpDir(.{});
343     defer temporary.cleanup();
344     const io = std.Options.debug_io;
345     var storage = try Storage.init(std.testing.allocator, .{ .max_page_bytes = 0 });
346     defer storage.deinit(std.testing.allocator);
347     storage.activate();
348     storage.rejected_page_count = std.math.maxInt(u64);
349 
350     try temporary.dir.writeFile(io, .{ .sub_path = "one.md", .data = "x" });
351     try std.testing.expectError(
352         error.SitePageCapacityExceeded,
353         storage.readFile(temporary.dir, io, "one.md"),
354     );
355     try std.testing.expectEqual(std.math.maxInt(u64), storage.status().rejected_page_count);
356 }
357 
358 test "site page reads remain allocation-free after storage seals" {
359     comptime {
360         @stardustClaim(
361             @import("alloc_phase").capacity.witness(Storage, "zen_site_page_sealed"),
362             null,
363             null,
364             null,
365             null,
366             null,
367             null,
368         );
369     }
370 
371     var temporary = std.testing.tmpDir(.{});
372     defer temporary.cleanup();
373     const io = std.Options.debug_io;
374     try temporary.dir.writeFile(io, .{ .sub_path = "page.md", .data = "sealed" });
375 
376     var phase_allocator = try alloc_phase.SealedPhaseAllocator.init(std.testing.allocator);
377     var maybe_storage: ?Storage = null;
378     defer {
379         if (phase_allocator.phase() == .initialization) phase_allocator.abortInitialization();
380         if (phase_allocator.phase() == .steady) phase_allocator.beginTeardown();
381         if (maybe_storage) |*storage| {
382             if (storage.in_use) storage.release();
383             if (storage.phase != .teardown) storage.deinit(phase_allocator.teardownAllocator());
384         }
385         phase_allocator.deinit();
386     }
387 
388     maybe_storage = try Storage.init(
389         phase_allocator.initializationAllocator(),
390         .{ .max_page_bytes = 6 },
391     );
392     phase_allocator.seal();
393     const storage = &maybe_storage.?;
394     storage.activate();
395     const page = try storage.readFile(temporary.dir, io, "page.md");
396     try std.testing.expectEqualStrings("sealed", page);
397     storage.release();
398 }
399 
400 comptime {
401     alloc_phase.capacity.requireAllocatorRejectingOwnerShape(Storage);
402 }