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 }