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 }