lib/http/src/client/operation/storage.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const alloc_phase = @import("alloc_phase");
  3 const model = @import("root.zig");
  4 
  5 const Scratch = model.Scratch;
  6 
  7 pub const StorageExhaustion = error{ClientOperationCapacityExceeded};
  8 
  9 pub const Storage = struct {
 10     phase: alloc_phase.capacity.Phase,
 11     capacity: model.Capacity,
 12     bytes: []u8,
 13 
 14     pub const Limits: type = model.Limits;
 15     pub const Capacity: type = model.Capacity;
 16     pub const Exhaustion: type = StorageExhaustion;
 17     pub const InitError = std.mem.Allocator.Error || error{CapacityOverflow};
 18 
 19     pub const claim: alloc_phase.capacity.Declaration = .{
 20         .source = .{
 21             .id = "http.client_operation_storage",
 22             .kind = .phase_static,
 23             .limit_source = .caller,
 24             .storage = .{
 25                 .covered = &.{
 26                     .{
 27                         .id = "fixed_decoded_host_bytes_for_every_concurrent_client_operation",
 28                         .lifetime = .steady,
 29                         .detail = "fixed decoded host bytes for every concurrent client operation",
 30                     },
 31                     .{
 32                         .id = "normalized_request_target_bytes_for_every_client_operation",
 33                         .lifetime = .steady,
 34                         .detail = "normalized request-target bytes for every client operation",
 35                     },
 36                     .{
 37                         .id = "plain_socket_windows_for_every_client_operation",
 38                         .lifetime = .steady,
 39                         .detail = "plain-socket windows for every client operation",
 40                     },
 41                 },
 42                 .excluded = &.{
 43                     "TLS buffers, certificate bundles, sockets, DNS state, and kernel queues",
 44                     "borrowed request inputs, response storage, handlers, and handler output",
 45                 },
 46             },
 47             .capacity = .{
 48                 .inputs = &.{
 49                     alloc_phase.capacity.bindInput(Limits, "host_bytes_per_operation", "host_bytes_per_operation"),
 50                     alloc_phase.capacity.bindInput(Limits, "target_bytes_per_operation", "target_bytes_per_operation"),
 51                     alloc_phase.capacity.bindInput(Limits, "plain_read_bytes_per_operation", "plain_read_bytes_per_operation"),
 52                     alloc_phase.capacity.bindInput(Limits, "plain_write_bytes_per_operation", "plain_write_bytes_per_operation"),
 53                     alloc_phase.capacity.bindInput(Limits, "operation_count", "operation_count"),
 54                 },
 55                 .type_selectors = &.{},
 56                 .nodes = &.{
 57                     .{ .input = 0 },
 58                     .{ .input = 1 },
 59                     .{ .input = 2 },
 60                     .{ .input = 3 },
 61                     .{ .add = .{ .left = 0, .right = 1 } },
 62                     .{ .add = .{ .left = 4, .right = 2 } },
 63                     .{ .add = .{ .left = 5, .right = 3 } },
 64                     .{ .input = 4 },
 65                     .{ .product = .{ .left = 7, .right = 6 } },
 66                 },
 67                 .assertions = &.{.{
 68                     .scope = .closure_total,
 69                     .measure = .retained,
 70                     .relation = .exact,
 71                     .expression = 8,
 72                 }},
 73             },
 74             .overload = .{
 75                 .kind = .terminal,
 76                 .detail = "capacity exhaustion rejects before connection or output publication",
 77             },
 78             .risks = .{
 79                 .transitive = .{
 80                     .status = .witnessed,
 81                     .detail = "URL preparation and request serialization allocate no storage",
 82                 },
 83                 .foreign = .{
 84                     .status = .excluded,
 85                     .detail = "TLS and operating-system network owners remain independent",
 86                 },
 87             },
 88             .obligations = &.{
 89                 .{ .key = "http_client_operation_capacity", .role = .capacity_model },
 90                 .{ .key = "http_client_operation_oom_retry", .role = .custom },
 91                 .{ .key = "http_client_operation_partition", .role = .custom },
 92                 .{ .key = "http_client_operation_sealed", .role = .transitive_risk },
 93                 .{ .key = "http_client_operation_boundary", .role = .custom },
 94                 .{ .key = "http_client_operation_atomic", .role = .overload },
 95                 .{ .key = "http_client_operation_network_overload", .role = .overload },
 96                 .{ .key = "http_client_operation_network_foreign_risk", .role = .foreign_risk },
 97             },
 98         },
 99         .bindings = .{
100             .owner = @This(),
101             .seal = .{
102                 .family = alloc_phase.capacity.selector(@This().activate),
103                 .premise = .{
104                     .class = .checked_semantic_fact,
105                     .authority = .checker,
106                 },
107             },
108             .teardown = .{
109                 .family = alloc_phase.capacity.selector(@This().deinit),
110                 .premise = .{
111                     .class = .checked_semantic_fact,
112                     .authority = .checker,
113                 },
114             },
115         },
116     };
117 
118     pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!Storage {
119         const capacity = try Capacity.derive(limits);
120         const bytes = if (capacity.storage_bytes == 0)
121             @as([]u8, &.{})
122         else
123             try allocator.alloc(u8, capacity.storage_bytes);
124         std.debug.assert(bytes.len == capacity.storage_bytes);
125         return .{
126             .phase = .initialization,
127             .capacity = capacity,
128             .bytes = bytes,
129         };
130     }
131 
132     pub fn activate(self: *Storage) void {
133         std.debug.assert(self.phase == .initialization);
134         std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
135         self.phase = .steady;
136     }
137 
138     pub fn operation(self: *Storage, index: usize) Exhaustion!Scratch {
139         std.debug.assert(self.phase == .steady);
140         if (index >= self.capacity.operation_count) {
141             return error.ClientOperationCapacityExceeded;
142         }
143         const host_start = index * self.capacity.host_bytes_per_operation;
144         const target_region_start = self.capacity.host_bytes;
145         const target_start = target_region_start +
146             index * self.capacity.target_bytes_per_operation;
147         const plain_read_region_start = target_region_start + self.capacity.target_bytes;
148         const plain_read_start = plain_read_region_start +
149             index * self.capacity.plain_read_bytes_per_operation;
150         const plain_write_region_start = plain_read_region_start + self.capacity.plain_read_bytes;
151         const plain_write_start = plain_write_region_start +
152             index * self.capacity.plain_write_bytes_per_operation;
153         const plain_read_region = self.bytes[plain_read_start..];
154         const plain_write_region = self.bytes[plain_write_start..];
155         std.debug.assert(host_start <= self.capacity.host_bytes);
156         std.debug.assert(target_start <= plain_read_region_start);
157         std.debug.assert(plain_read_start <= plain_write_region_start);
158         std.debug.assert(plain_write_start <= self.bytes.len);
159         return .{
160             .host = self.bytes[host_start..][0..self.capacity.host_bytes_per_operation],
161             .target = self.bytes[target_start..][0..self.capacity.target_bytes_per_operation],
162             .plain_read = plain_read_region[0..self.capacity.plain_read_bytes_per_operation],
163             .plain_write = plain_write_region[0..self.capacity.plain_write_bytes_per_operation],
164         };
165     }
166 
167     pub fn deinit(self: *Storage, allocator: std.mem.Allocator) void {
168         std.debug.assert(self.phase != .teardown);
169         std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
170         self.phase = .teardown;
171         if (self.bytes.len != 0) allocator.free(self.bytes);
172         self.bytes = &.{};
173     }
174 };
175 
176 comptime {
177     alloc_phase.capacity.requireAllocatorRejectingOwnerShape(Storage);
178 }
179 
180 fn checkStorageInitFailures(allocator: std.mem.Allocator) !void {
181     var storage = try Storage.init(allocator, .{
182         .operation_count = 3,
183         .host_bytes_per_operation = 17,
184         .target_bytes_per_operation = 31,
185         .plain_read_bytes_per_operation = 47,
186         .plain_write_bytes_per_operation = 61,
187     });
188     storage.deinit(allocator);
189 }
190 
191 test "Client operation storage retries after every allocation failure" {
192     comptime {
193         @stardustClaim(
194             @import("alloc_phase").capacity.witness(Storage, "http_client_operation_oom_retry"),
195             null,
196             null,
197             null,
198             null,
199             null,
200             null,
201         );
202     }
203 
204     try std.testing.checkAllAllocationFailures(
205         std.testing.allocator,
206         checkStorageInitFailures,
207         .{},
208     );
209 }
210 
211 test "Client operation storage partitions reusable slots" {
212     comptime {
213         @stardustClaim(
214             @import("alloc_phase").capacity.witness(Storage, "http_client_operation_partition"),
215             null,
216             null,
217             null,
218             null,
219             null,
220             null,
221         );
222     }
223 
224     var storage = try Storage.init(std.testing.allocator, .{
225         .operation_count = 2,
226         .host_bytes_per_operation = 3,
227         .target_bytes_per_operation = 5,
228         .plain_read_bytes_per_operation = 7,
229         .plain_write_bytes_per_operation = 11,
230     });
231     defer storage.deinit(std.testing.allocator);
232     storage.activate();
233 
234     const first = try storage.operation(0);
235     const second = try storage.operation(1);
236     try std.testing.expect(first.host.ptr + first.host.len == second.host.ptr);
237     try std.testing.expect(first.target.ptr + first.target.len == second.target.ptr);
238     try std.testing.expect(first.plain_read.ptr + first.plain_read.len == second.plain_read.ptr);
239     try std.testing.expect(first.plain_write.ptr + first.plain_write.len == second.plain_write.ptr);
240     try std.testing.expect(
241         @intFromPtr(first.target.ptr) ==
242             @intFromPtr(storage.bytes.ptr) + storage.capacity.host_bytes,
243     );
244     try std.testing.expect(
245         @intFromPtr(first.plain_read.ptr) ==
246             @intFromPtr(storage.bytes.ptr) +
247                 storage.capacity.host_bytes + storage.capacity.target_bytes,
248     );
249     try std.testing.expect(
250         @intFromPtr(first.plain_write.ptr) ==
251             @intFromPtr(storage.bytes.ptr) + storage.capacity.host_bytes +
252                 storage.capacity.target_bytes + storage.capacity.plain_read_bytes,
253     );
254     try std.testing.expectError(
255         error.ClientOperationCapacityExceeded,
256         storage.operation(2),
257     );
258 }
259 
260 test "Client operation remains allocation-free after storage seals" {
261     comptime {
262         @stardustClaim(
263             @import("alloc_phase").capacity.witness(Storage, "http_client_operation_sealed"),
264             null,
265             null,
266             null,
267             null,
268             null,
269             null,
270         );
271     }
272 
273     var phase_allocator = try alloc_phase.SealedPhaseAllocator.init(std.testing.allocator);
274     var storage = Storage.init(phase_allocator.initializationAllocator(), .{
275         .operation_count = 1,
276         .host_bytes_per_operation = 64,
277         .target_bytes_per_operation = 128,
278         .plain_read_bytes_per_operation = 16,
279         .plain_write_bytes_per_operation = 16,
280     }) catch |err| {
281         phase_allocator.abortInitialization();
282         phase_allocator.deinit();
283         return err;
284     };
285     errdefer {
286         if (phase_allocator.phase() == .initialization) phase_allocator.abortInitialization();
287         if (phase_allocator.phase() == .steady) phase_allocator.beginTeardown();
288         if (storage.phase != .teardown) {
289             storage.deinit(phase_allocator.teardownAllocator());
290         }
291         phase_allocator.deinit();
292     }
293 
294     const pointer = storage.bytes.ptr;
295     const capacity = storage.capacity;
296     phase_allocator.seal();
297     storage.activate();
298     const scratch = try storage.operation(0);
299     const prepared = try model.ClientOperation.prepare(
300         scratch,
301         "https://api.example.test/v1/responses?mode=sealed",
302     );
303     var bytes: [512]u8 = undefined;
304     var writer = std.Io.Writer.fixed(&bytes);
305     try model.ClientOperation.writeRequest(
306         &writer,
307         "POST",
308         prepared.host,
309         prepared.target,
310         &.{.{ .name = "Content-Type", .value = "application/json" }},
311         "{}",
312     );
313     try std.testing.expect(writer.buffered().len != 0);
314     try std.testing.expect(storage.bytes.ptr == pointer);
315     try std.testing.expectEqual(capacity, storage.capacity);
316     try std.testing.expectEqual(alloc_phase.PhaseViolations{}, phase_allocator.violations());
317 
318     phase_allocator.beginTeardown();
319     storage.deinit(phase_allocator.teardownAllocator());
320     phase_allocator.deinit();
321 }