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

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const alloc_phase = @import("alloc_phase");
  3 const capacity_mod = @import("capacity.zig");
  4 
  5 pub const Exhaustion = error{
  6     WriterStorageInUse,
  7     RootPathCapacityExceeded,
  8     TargetTripleCapacityExceeded,
  9     BuildIdCapacityExceeded,
 10     ModeCapacityExceeded,
 11     EndianCapacityExceeded,
 12     EventCapacityExceeded,
 13 };
 14 
 15 pub const Usage = struct {
 16     root_path_bytes: usize,
 17     target_triple_bytes: usize,
 18     build_id_bytes: usize,
 19     mode_bytes: usize,
 20     endian_bytes: usize,
 21     event_bytes: usize,
 22 };
 23 
 24 pub const Regions = struct {
 25     root_path: []u8,
 26     target_triple: []u8,
 27     build_id: []u8,
 28     mode: []u8,
 29     endian: []u8,
 30     block: []u8,
 31     manifest_io: []u8,
 32     sql: []u8,
 33     manifest: []u8,
 34     paths: [capacity_mod.path_buffer_count][]u8,
 35 };
 36 
 37 pub const Status = struct {
 38     phase: alloc_phase.capacity.Phase,
 39     in_use: bool,
 40     storage_bytes: usize,
 41     event_bytes: usize,
 42     block_bytes: usize,
 43     manifest_io_bytes: usize,
 44     sql_bytes: usize,
 45 };
 46 
 47 pub const Storage = struct {
 48     phase: alloc_phase.capacity.Phase,
 49     capacity: capacity_mod.Capacity,
 50     bytes: []u8,
 51     in_use: bool = false,
 52 
 53     pub const Limits: type = capacity_mod.Limits;
 54     pub const Capacity: type = capacity_mod.Capacity;
 55     pub const Exhaustion: type = @import("storage.zig").Exhaustion;
 56     pub const InitError = std.mem.Allocator.Error || capacity_mod.DeriveError;
 57 
 58     pub const claim: alloc_phase.capacity.Declaration = .{
 59         .source = .{
 60             .id = "trace.trace_writer_storage",
 61             .kind = .phase_static,
 62             .limit_source = .caller,
 63             .storage = .{
 64                 .covered = &.{
 65                     .{
 66                         .id = "retained_trace_path_and_manifest_identity_bytes",
 67                         .lifetime = .steady,
 68                         .detail = "retained trace path and manifest identity bytes",
 69                     },
 70                     .{
 71                         .id = "phase_shared_bounded_binary_event_block_and_manifes_21808f4010ec",
 72                         .lifetime = .steady,
 73                         .detail = "phase-shared bounded binary event block and manifest encoding scratch",
 74                     },
 75                     .{
 76                         .id = "manifest_file_i_o_and_two_simultaneous_path_construction_buffers",
 77                         .lifetime = .steady,
 78                         .detail = "manifest file I/O and two simultaneous path-construction buffers",
 79                     },
 80                     .{
 81                         .id = "bounded_lib_sql_wal_pager_transaction_and_checkpoint_heap",
 82                         .lifetime = .steady,
 83                         .detail = "bounded lib/sql WAL pager transaction and checkpoint heap",
 84                     },
 85                 },
 86                 .excluded = &.{
 87                     "filesystem implementation and kernel file state",
 88                     "recording Session state and event payload pointees",
 89                     "trace reader parsing and replay storage",
 90                 },
 91             },
 92             .capacity = .{
 93                 .inputs = &.{
 94                     alloc_phase.capacity.bindInput(Limits, "build_id_bytes", "build_id_bytes"),
 95                     alloc_phase.capacity.bindInput(Limits, "endian_bytes", "endian_bytes"),
 96                     alloc_phase.capacity.bindInput(Limits, "event_bytes", "event_bytes"),
 97                     alloc_phase.capacity.bindInput(Limits, "manifest_io_bytes", "manifest_io_bytes"),
 98                     alloc_phase.capacity.bindInput(Limits, "mode_bytes", "mode_bytes"),
 99                     alloc_phase.capacity.bindInput(Limits, "root_path_bytes", "root_path_bytes"),
100                     alloc_phase.capacity.bindInput(Limits, "target_triple_bytes", "target_triple_bytes"),
101                 },
102                 .type_selectors = &.{},
103                 .nodes = &.{
104                     .{ .input = 0 },
105                     .{ .input = 1 },
106                     .{ .input = 2 },
107                     .{ .input = 3 },
108                     .{ .input = 4 },
109                     .{ .input = 5 },
110                     .{ .input = 6 },
111                     .{ .add = .{ .left = 0, .right = 1 } },
112                     .{ .add = .{ .left = 7, .right = 2 } },
113                     .{ .add = .{ .left = 8, .right = 3 } },
114                     .{ .add = .{ .left = 9, .right = 4 } },
115                     .{ .add = .{ .left = 10, .right = 5 } },
116                     .{ .add = .{ .left = 11, .right = 6 } },
117                 },
118                 .assertions = &.{.{
119                     .scope = .closure_total,
120                     .measure = .retained,
121                     .relation = .upper_bound,
122                     .expression = 12,
123                 }},
124             },
125             .overload = .{
126                 .kind = .reject_before_mutation,
127                 .detail = "open rejects every retained byte region at max plus one before acquiring storage or mutating the trace path",
128             },
129             .risks = .{
130                 .transitive = .{
131                     .status = .witnessed,
132                     .detail = "open, binary block encode, SQL append, bounded checkpoint, and finish consume only preflighted writer regions after activation",
133                 },
134                 .foreign = .{
135                     .status = .excluded,
136                     .detail = "filesystem calls and kernel buffering remain effects outside the caller allocator and this storage owner",
137                 },
138             },
139             .dependencies = &.{ "sql.wal_writer", "sql.file_transaction_staging" },
140             .obligations = &.{
141                 .{ .key = "trace_trace_writer_capacity", .role = .capacity_model },
142                 .{ .key = "trace_trace_writer_acquisition", .role = .custom },
143                 .{ .key = "trace_trace_writer_oom", .role = .custom },
144                 .{ .key = "trace_trace_writer_boundary", .role = .overload },
145                 .{ .key = "trace_trace_writer_open_atomic", .role = .overload },
146                 .{ .key = "trace_trace_writer_sealed", .role = .transitive_risk },
147                 .{ .key = "trace_trace_writer_rollover", .role = .foreign_risk },
148                 .{ .key = "trace_trace_writer_root", .role = .custom },
149                 .{ .key = "trace_trace_writer_tuning", .role = .custom },
150             },
151         },
152         .bindings = .{
153             .owner = @This(),
154             .seal = .{
155                 .family = alloc_phase.capacity.selector(@This().activate),
156                 .premise = .{
157                     .class = .checked_semantic_fact,
158                     .authority = .checker,
159                 },
160             },
161             .teardown = .{
162                 .family = alloc_phase.capacity.selector(@This().deinit),
163                 .premise = .{
164                     .class = .checked_semantic_fact,
165                     .authority = .checker,
166                 },
167             },
168         },
169     };
170 
171     pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!Storage {
172         const capacity = try Capacity.derive(limits);
173         return .{
174             .phase = .initialization,
175             .capacity = capacity,
176             .bytes = try allocator.alloc(u8, capacity.storage_bytes),
177         };
178     }
179 
180     pub fn activate(self: *Storage) void {
181         std.debug.assert(self.phase == .initialization);
182         std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
183         self.phase = .steady;
184     }
185 
186     pub fn acquire(self: *Storage, usage: Usage) Storage.Exhaustion!Regions {
187         std.debug.assert(self.phase == .steady);
188         if (self.in_use) return error.WriterStorageInUse;
189         if (usage.root_path_bytes > self.capacity.root_path_bytes) return error.RootPathCapacityExceeded;
190         if (usage.target_triple_bytes > self.capacity.target_triple_bytes) return error.TargetTripleCapacityExceeded;
191         if (usage.build_id_bytes > self.capacity.build_id_bytes) return error.BuildIdCapacityExceeded;
192         if (usage.mode_bytes > self.capacity.mode_bytes) return error.ModeCapacityExceeded;
193         if (usage.endian_bytes > self.capacity.endian_bytes) return error.EndianCapacityExceeded;
194         if (usage.event_bytes > self.capacity.event_bytes) return error.EventCapacityExceeded;
195         self.in_use = true;
196         return .{
197             .root_path = self.region(self.capacity.root_path_offset, usage.root_path_bytes),
198             .target_triple = self.region(self.capacity.target_triple_offset, usage.target_triple_bytes),
199             .build_id = self.region(self.capacity.build_id_offset, usage.build_id_bytes),
200             .mode = self.region(self.capacity.mode_offset, usage.mode_bytes),
201             .endian = self.region(self.capacity.endian_offset, usage.endian_bytes),
202             .block = self.region(self.capacity.block_offset, self.capacity.block_bytes),
203             .manifest_io = self.region(self.capacity.manifest_io_offset, self.capacity.manifest_io_bytes),
204             .sql = self.region(self.capacity.sql_offset, self.capacity.sql_bytes),
205             .manifest = self.region(self.capacity.manifest_offset, self.capacity.manifest_bytes),
206             .paths = .{
207                 self.region(self.capacity.path_offsets[0], self.capacity.path_bytes),
208                 self.region(self.capacity.path_offsets[1], self.capacity.path_bytes),
209             },
210         };
211     }
212 
213     pub fn release(self: *Storage) void {
214         std.debug.assert(self.phase == .steady);
215         std.debug.assert(self.in_use);
216         self.in_use = false;
217     }
218 
219     pub fn status(self: *const Storage) Status {
220         return .{
221             .phase = self.phase,
222             .in_use = self.in_use,
223             .storage_bytes = self.capacity.storage_bytes,
224             .event_bytes = self.capacity.event_bytes,
225             .block_bytes = self.capacity.block_bytes,
226             .manifest_io_bytes = self.capacity.manifest_io_bytes,
227             .sql_bytes = self.capacity.sql_bytes,
228         };
229     }
230 
231     pub fn deinit(self: *Storage, allocator: std.mem.Allocator) void {
232         std.debug.assert(self.phase != .teardown);
233         std.debug.assert(!self.in_use);
234         std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
235         self.phase = .teardown;
236         allocator.free(self.bytes);
237         self.bytes = &.{};
238     }
239 
240     fn region(self: *Storage, offset: usize, count: usize) []u8 {
241         return self.bytes[offset..][0..count];
242     }
243 };
244 
245 fn checkInitFailures(allocator: std.mem.Allocator) !void {
246     var storage = try Storage.init(allocator, .{
247         .root_path_bytes = 17,
248         .target_triple_bytes = 19,
249         .build_id_bytes = 23,
250         .mode_bytes = 29,
251         .endian_bytes = 31,
252         .event_bytes = 37,
253         .manifest_io_bytes = 41,
254     });
255     storage.deinit(allocator);
256 }
257 
258 test "trace writer storage acquires one exact region" {
259     comptime {
260         @stardustClaim(
261             @import("alloc_phase").capacity.witness(Storage, "trace_trace_writer_acquisition"),
262             null,
263             null,
264             null,
265             null,
266             null,
267             null,
268         );
269     }
270 
271     var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
272     var storage = try Storage.init(counting.allocator(), .{
273         .root_path_bytes = 17,
274         .event_bytes = 37,
275         .manifest_io_bytes = 41,
276     });
277     defer storage.deinit(counting.allocator());
278     try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
279     try std.testing.expectEqual(storage.capacity.storage_bytes, counting.allocated_bytes);
280     storage.activate();
281     const regions = try storage.acquire(.{
282         .root_path_bytes = 17,
283         .target_triple_bytes = 7,
284         .build_id_bytes = 7,
285         .mode_bytes = 6,
286         .endian_bytes = 6,
287         .event_bytes = 37,
288     });
289     defer storage.release();
290     const base = @intFromPtr(storage.bytes.ptr);
291     try std.testing.expectEqual(base + storage.capacity.root_path_offset, @intFromPtr(regions.root_path.ptr));
292     try std.testing.expectEqual(base + storage.capacity.block_offset, @intFromPtr(regions.block.ptr));
293     try std.testing.expectEqual(@intFromPtr(regions.block.ptr), @intFromPtr(regions.manifest.ptr));
294     try std.testing.expectEqual(base + storage.capacity.manifest_io_offset, @intFromPtr(regions.manifest_io.ptr));
295     try std.testing.expectEqual(base + storage.capacity.sql_offset, @intFromPtr(regions.sql.ptr));
296     try std.testing.expectEqual(base + storage.capacity.path_offsets[0], @intFromPtr(regions.paths[0].ptr));
297     try std.testing.expectEqual(base + storage.capacity.path_offsets[1], @intFromPtr(regions.paths[1].ptr));
298 }
299 
300 test "trace writer storage retries after every allocation failure" {
301     comptime {
302         @stardustClaim(
303             @import("alloc_phase").capacity.witness(Storage, "trace_trace_writer_oom"),
304             null,
305             null,
306             null,
307             null,
308             null,
309             null,
310         );
311     }
312 
313     try std.testing.checkAllAllocationFailures(std.testing.allocator, checkInitFailures, .{});
314 }
315 
316 test "trace writer storage rejects every retained region at max plus one" {
317     comptime {
318         @stardustClaim(
319             @import("alloc_phase").capacity.witness(Storage, "trace_trace_writer_boundary"),
320             null,
321             null,
322             null,
323             null,
324             null,
325             null,
326         );
327     }
328 
329     const limits = capacity_mod.Limits{
330         .root_path_bytes = 2,
331         .target_triple_bytes = 3,
332         .build_id_bytes = 5,
333         .mode_bytes = 7,
334         .endian_bytes = 11,
335         .event_bytes = 13,
336         .manifest_io_bytes = 17,
337     };
338     var storage = try Storage.init(std.testing.allocator, limits);
339     defer storage.deinit(std.testing.allocator);
340     storage.activate();
341     const exact = Usage{
342         .root_path_bytes = 2,
343         .target_triple_bytes = 3,
344         .build_id_bytes = 5,
345         .mode_bytes = 7,
346         .endian_bytes = 11,
347         .event_bytes = 13,
348     };
349     inline for (.{
350         .{ error.RootPathCapacityExceeded, "root_path_bytes" },
351         .{ error.TargetTripleCapacityExceeded, "target_triple_bytes" },
352         .{ error.BuildIdCapacityExceeded, "build_id_bytes" },
353         .{ error.ModeCapacityExceeded, "mode_bytes" },
354         .{ error.EndianCapacityExceeded, "endian_bytes" },
355         .{ error.EventCapacityExceeded, "event_bytes" },
356     }) |entry| {
357         var exceeded = exact;
358         @field(exceeded, entry[1]) += 1;
359         try std.testing.expectError(entry[0], storage.acquire(exceeded));
360         try std.testing.expect(!storage.status().in_use);
361     }
362     _ = try storage.acquire(exact);
363     try std.testing.expectError(error.WriterStorageInUse, storage.acquire(exact));
364     storage.release();
365 }
366 
367 comptime {
368     alloc_phase.capacity.requireAllocatorRejectingOwnerShape(Storage);
369 }