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 }