lib/trace/src/store/reader/storage.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const alloc_phase = @import("alloc_phase");
  3 const format = @import("../format/root.zig");
  4 const capacity_mod = @import("capacity.zig");
  5 
  6 pub const Exhaustion = error{
  7     ReaderStorageInUse,
  8     RootPathCapacityExceeded,
  9     EventCapacityExceeded,
 10     ChunkCapacityExceeded,
 11     JsonNestingCapacityExceeded,
 12 };
 13 
 14 pub const Usage = struct {
 15     root_path_bytes: usize,
 16 };
 17 
 18 pub const Regions = struct {
 19     root_path: []u8,
 20     input: []u8,
 21     path: []u8,
 22     json_stack: []u8,
 23     sql: []u8,
 24 };
 25 
 26 pub const Status = struct {
 27     phase: alloc_phase.capacity.Phase,
 28     in_use: bool,
 29     storage_bytes: usize,
 30     event_bytes: usize,
 31     chunk_bytes: usize,
 32     block_bytes: usize,
 33     json_nesting: usize,
 34     sql_bytes: usize,
 35 };
 36 
 37 pub const Storage = struct {
 38     phase: alloc_phase.capacity.Phase,
 39     capacity: capacity_mod.Capacity,
 40     bytes: []u8,
 41     in_use: bool = false,
 42 
 43     pub const Limits: type = capacity_mod.Limits;
 44     pub const Capacity: type = capacity_mod.Capacity;
 45     pub const Exhaustion: type = @import("storage.zig").Exhaustion;
 46     pub const InitError = std.mem.Allocator.Error || capacity_mod.DeriveError;
 47 
 48     pub const claim: alloc_phase.capacity.Declaration = .{
 49         .source = .{
 50             .id = "trace.trace_reader_storage",
 51             .kind = .phase_static,
 52             .limit_source = .caller,
 53             .storage = .{
 54                 .covered = &.{
 55                     .{
 56                         .id = "retained_trace_root_path",
 57                         .lifetime = .steady,
 58                         .detail = "retained trace root path",
 59                     },
 60                     .{
 61                         .id = "phase_shared_bounded_manifest_and_binary_event_block_input",
 62                         .lifetime = .steady,
 63                         .detail = "phase-shared bounded manifest and binary event-block input",
 64                     },
 65                     .{
 66                         .id = "joined_path_construction_and_manifest_json_nesting_bits",
 67                         .lifetime = .steady,
 68                         .detail = "joined-path construction and manifest JSON nesting bits",
 69                     },
 70                     .{
 71                         .id = "bounded_lib_sql_pager_and_read_heap",
 72                         .lifetime = .steady,
 73                         .detail = "bounded lib/sql pager and read heap",
 74                     },
 75                 },
 76                 .excluded = &.{
 77                     "filesystem implementation and kernel file state",
 78                     "replay Session state and consumer-owned event clones",
 79                     "trace writer encoding storage",
 80                 },
 81             },
 82             .capacity = .{
 83                 .inputs = &.{
 84                     alloc_phase.capacity.bindInput(Limits, "chunk_bytes", "chunk_bytes"),
 85                     alloc_phase.capacity.bindInput(Limits, "event_bytes", "event_bytes"),
 86                     alloc_phase.capacity.bindInput(Limits, "json_nesting", "json_nesting"),
 87                     alloc_phase.capacity.bindInput(Limits, "root_path_bytes", "root_path_bytes"),
 88                 },
 89                 .type_selectors = &.{},
 90                 .nodes = &.{
 91                     .{ .input = 0 },
 92                     .{ .input = 1 },
 93                     .{ .input = 2 },
 94                     .{ .input = 3 },
 95                     .{ .add = .{ .left = 0, .right = 1 } },
 96                     .{ .add = .{ .left = 4, .right = 2 } },
 97                     .{ .add = .{ .left = 5, .right = 3 } },
 98                 },
 99                 .assertions = &.{.{
100                     .scope = .closure_total,
101                     .measure = .retained,
102                     .relation = .upper_bound,
103                     .expression = 6,
104                 }},
105             },
106             .overload = .{
107                 .kind = .reject_before_mutation,
108                 .detail = "open rejects root path and manifest event/chunk limits at max plus one before publishing reader state",
109             },
110             .risks = .{
111                 .transitive = .{
112                     .status = .witnessed,
113                     .detail = "open, SQL block identity admission, binary event decoding, logical chunk rollover, and verification consume only acquired reader regions after activation",
114                 },
115                 .foreign = .{
116                     .status = .excluded,
117                     .detail = "filesystem calls and kernel buffering remain effects outside the caller allocator and this storage owner",
118                 },
119             },
120             .dependencies = &.{ "sql.wal_writer", "sql.file_transaction_staging" },
121             .obligations = &.{
122                 .{ .key = "trace_trace_reader_capacity", .role = .capacity_model },
123                 .{ .key = "trace_trace_reader_acquisition", .role = .custom },
124                 .{ .key = "trace_trace_reader_oom", .role = .custom },
125                 .{ .key = "trace_trace_reader_boundary", .role = .overload },
126                 .{ .key = "trace_trace_reader_nesting", .role = .overload },
127                 .{ .key = "trace_trace_reader_open_atomic", .role = .overload },
128                 .{ .key = "trace_trace_reader_sealed", .role = .transitive_risk },
129                 .{ .key = "trace_trace_reader_rollover", .role = .foreign_risk },
130                 .{ .key = "trace_trace_reader_root", .role = .custom },
131                 .{ .key = "trace_trace_reader_tuning", .role = .custom },
132             },
133         },
134         .bindings = .{
135             .owner = @This(),
136             .seal = .{
137                 .family = alloc_phase.capacity.selector(@This().activate),
138                 .premise = .{
139                     .class = .checked_semantic_fact,
140                     .authority = .checker,
141                 },
142             },
143             .teardown = .{
144                 .family = alloc_phase.capacity.selector(@This().deinit),
145                 .premise = .{
146                     .class = .checked_semantic_fact,
147                     .authority = .checker,
148                 },
149             },
150         },
151     };
152 
153     pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!Storage {
154         const capacity = try Capacity.derive(limits);
155         return .{
156             .phase = .initialization,
157             .capacity = capacity,
158             .bytes = try allocator.alloc(u8, capacity.storage_bytes),
159         };
160     }
161 
162     pub fn activate(self: *Storage) void {
163         std.debug.assert(self.phase == .initialization);
164         std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
165         self.phase = .steady;
166     }
167 
168     pub fn acquire(self: *Storage, usage: Usage) Storage.Exhaustion!Regions {
169         std.debug.assert(self.phase == .steady);
170         if (self.in_use) return error.ReaderStorageInUse;
171         if (usage.root_path_bytes > self.capacity.root_path_bytes) return error.RootPathCapacityExceeded;
172         self.in_use = true;
173         return .{
174             .root_path = self.region(self.capacity.root_path_offset, usage.root_path_bytes),
175             .input = self.region(self.capacity.input_offset, self.capacity.input_bytes),
176             .path = self.region(self.capacity.path_offset, self.capacity.path_bytes),
177             .json_stack = self.region(self.capacity.json_stack_offset, self.capacity.json_stack_bytes),
178             .sql = self.region(self.capacity.sql_offset, self.capacity.sql_bytes),
179         };
180     }
181 
182     pub fn admit(self: *const Storage, limits: format.Limits) Storage.Exhaustion!void {
183         std.debug.assert(self.phase == .steady);
184         std.debug.assert(self.in_use);
185         if (limits.max_event_bytes > self.capacity.event_bytes) return error.EventCapacityExceeded;
186         if (limits.max_chunk_bytes > self.capacity.chunk_bytes) return error.ChunkCapacityExceeded;
187     }
188 
189     pub fn release(self: *Storage) void {
190         std.debug.assert(self.phase == .steady);
191         std.debug.assert(self.in_use);
192         self.in_use = false;
193     }
194 
195     pub fn status(self: *const Storage) Status {
196         return .{
197             .phase = self.phase,
198             .in_use = self.in_use,
199             .storage_bytes = self.capacity.storage_bytes,
200             .event_bytes = self.capacity.event_bytes,
201             .chunk_bytes = self.capacity.chunk_bytes,
202             .block_bytes = self.capacity.block_bytes,
203             .json_nesting = self.capacity.json_nesting,
204             .sql_bytes = self.capacity.sql_bytes,
205         };
206     }
207 
208     pub fn deinit(self: *Storage, allocator: std.mem.Allocator) void {
209         std.debug.assert(self.phase != .teardown);
210         std.debug.assert(!self.in_use);
211         std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
212         self.phase = .teardown;
213         allocator.free(self.bytes);
214         self.bytes = &.{};
215     }
216 
217     fn region(self: *Storage, offset: usize, count: usize) []u8 {
218         return self.bytes[offset..][0..count];
219     }
220 };
221 
222 fn checkInitFailures(allocator: std.mem.Allocator) !void {
223     var storage = try Storage.init(allocator, .{
224         .root_path_bytes = 17,
225         .event_bytes = 37,
226         .chunk_bytes = 41,
227         .json_nesting = 23,
228     });
229     storage.deinit(allocator);
230 }
231 
232 test "trace reader storage acquires one exact region" {
233     comptime {
234         @stardustClaim(
235             @import("alloc_phase").capacity.witness(Storage, "trace_trace_reader_acquisition"),
236             null,
237             null,
238             null,
239             null,
240             null,
241             null,
242         );
243     }
244 
245     var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
246     var storage = try Storage.init(counting.allocator(), .{
247         .root_path_bytes = 17,
248         .event_bytes = 37,
249         .chunk_bytes = 41,
250         .json_nesting = 23,
251     });
252     defer storage.deinit(counting.allocator());
253     try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
254     try std.testing.expectEqual(storage.capacity.storage_bytes, counting.allocated_bytes);
255     storage.activate();
256     const regions = try storage.acquire(.{ .root_path_bytes = 17 });
257     defer storage.release();
258     const base = @intFromPtr(storage.bytes.ptr);
259     try std.testing.expectEqual(base + storage.capacity.root_path_offset, @intFromPtr(regions.root_path.ptr));
260     try std.testing.expectEqual(base + storage.capacity.input_offset, @intFromPtr(regions.input.ptr));
261     try std.testing.expectEqual(base + storage.capacity.path_offset, @intFromPtr(regions.path.ptr));
262     try std.testing.expectEqual(base + storage.capacity.json_stack_offset, @intFromPtr(regions.json_stack.ptr));
263     try std.testing.expectEqual(base + storage.capacity.sql_offset, @intFromPtr(regions.sql.ptr));
264 }
265 
266 test "trace reader storage retries after every allocation failure" {
267     comptime {
268         @stardustClaim(
269             @import("alloc_phase").capacity.witness(Storage, "trace_trace_reader_oom"),
270             null,
271             null,
272             null,
273             null,
274             null,
275             null,
276         );
277     }
278 
279     try std.testing.checkAllAllocationFailures(std.testing.allocator, checkInitFailures, .{});
280 }
281 
282 test "trace reader storage rejects every admitted region at max plus one" {
283     comptime {
284         @stardustClaim(
285             @import("alloc_phase").capacity.witness(Storage, "trace_trace_reader_boundary"),
286             null,
287             null,
288             null,
289             null,
290             null,
291             null,
292         );
293     }
294 
295     var storage = try Storage.init(std.testing.allocator, .{
296         .root_path_bytes = 2,
297         .event_bytes = 13,
298         .chunk_bytes = 19,
299     });
300     defer storage.deinit(std.testing.allocator);
301     storage.activate();
302     try std.testing.expectError(error.RootPathCapacityExceeded, storage.acquire(.{ .root_path_bytes = 3 }));
303     try std.testing.expect(!storage.status().in_use);
304     _ = try storage.acquire(.{ .root_path_bytes = 2 });
305     defer storage.release();
306     try storage.admit(.{ .max_event_bytes = 13, .max_chunk_bytes = 19 });
307     try std.testing.expectError(
308         error.EventCapacityExceeded,
309         storage.admit(.{ .max_event_bytes = 14, .max_chunk_bytes = 19 }),
310     );
311     try std.testing.expectError(
312         error.ChunkCapacityExceeded,
313         storage.admit(.{ .max_event_bytes = 13, .max_chunk_bytes = 20 }),
314     );
315     try std.testing.expectError(error.ReaderStorageInUse, storage.acquire(.{ .root_path_bytes = 2 }));
316 }
317 
318 comptime {
319     alloc_phase.capacity.requireAllocatorRejectingOwnerShape(Storage);
320 }