lib/choir/src/core/context/transactions.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const Operation = @import("../root.zig").Operation;
3 const interfaces = @import("../root.zig").interfaces;
4 const memory_ctx = @import("memory.zig");
5
6 const NameList = interfaces.InlineList([]const u8, 0);
7
8 pub const DialectLoadTransaction = struct {
9 dialect_name: []const u8,
10 created_dialect_object: bool,
11 attr_next_id_snapshot: u32,
12 created_ops: NameList,
13 created_types: NameList,
14 created_attrs: NameList,
15 record_storage: [][]const u8,
16
17 pub fn init(dialect_name: []const u8, attr_next_id_snapshot: u32) DialectLoadTransaction {
18 return .{
19 .dialect_name = dialect_name,
20 .created_dialect_object = false,
21 .attr_next_id_snapshot = attr_next_id_snapshot,
22 .created_ops = .{},
23 .created_types = .{},
24 .created_attrs = .{},
25 .record_storage = &.{},
26 };
27 }
28
29 pub fn deinit(self: *DialectLoadTransaction, allocator: std.mem.Allocator) void {
30 self.created_ops.deinit(allocator);
31 self.created_types.deinit(allocator);
32 self.created_attrs.deinit(allocator);
33 if (self.record_storage.len != 0) allocator.free(self.record_storage);
34 self.* = undefined;
35 }
36
37 fn prepareRecordStorage(
38 self: *DialectLoadTransaction,
39 allocator: std.mem.Allocator,
40 additional_operations: usize,
41 additional_types: usize,
42 ) std.mem.Allocator.Error!void {
43 const operation_values = self.created_ops.values();
44 const type_values = self.created_types.values();
45 const operation_capacity = std.math.add(
46 usize,
47 operation_values.len,
48 additional_operations,
49 ) catch return error.OutOfMemory;
50 const type_capacity = std.math.add(
51 usize,
52 type_values.len,
53 additional_types,
54 ) catch return error.OutOfMemory;
55 const total_capacity = std.math.add(
56 usize,
57 operation_capacity,
58 type_capacity,
59 ) catch return error.OutOfMemory;
60 if (total_capacity == 0) return;
61
62 const replacement_storage = try allocator.alloc([]const u8, total_capacity);
63 const replacement_operations = NameList.initBorrowedValues(
64 replacement_storage[0..operation_capacity],
65 operation_values,
66 );
67 const replacement_types = NameList.initBorrowedValues(
68 replacement_storage[operation_capacity..],
69 type_values,
70 );
71
72 self.created_ops.deinit(allocator);
73 self.created_types.deinit(allocator);
74 if (self.record_storage.len != 0) allocator.free(self.record_storage);
75 self.created_ops = replacement_operations;
76 self.created_types = replacement_types;
77 self.record_storage = replacement_storage;
78 }
79 };
80
81 pub fn deinitAll(ctx: anytype) void {
82 for (ctx.dialect_load_transactions.items) |*txn| {
83 txn.deinit(memory_ctx.segmentAllocator(ctx, .configuration_transactions));
84 }
85 ctx.dialect_load_transactions.deinit(memory_ctx.segmentAllocator(ctx, .configuration_transactions));
86 }
87
88 pub fn begin(ctx: anytype, dialect_name: []const u8) !usize {
89 const snapshot = ctx.attr_interner.registry.nextAttributeId();
90 try ctx.dialect_load_transactions.append(
91 memory_ctx.segmentAllocator(ctx, .configuration_transactions),
92 DialectLoadTransaction.init(dialect_name, snapshot),
93 );
94 return ctx.dialect_load_transactions.items.len - 1;
95 }
96
97 pub fn prepareRecordStorage(
98 ctx: anytype,
99 dialect_name: []const u8,
100 operation_count: usize,
101 type_count: usize,
102 ) std.mem.Allocator.Error!void {
103 if (find(ctx, dialect_name)) |txn| {
104 try txn.prepareRecordStorage(memory_ctx.segmentAllocator(ctx, .configuration_transactions), operation_count, type_count);
105 }
106 }
107
108 pub fn commit(ctx: anytype, txn_index: usize) void {
109 std.debug.assert(txn_index + 1 == ctx.dialect_load_transactions.items.len);
110 var txn = ctx.dialect_load_transactions.pop().?;
111 txn.deinit(memory_ctx.segmentAllocator(ctx, .configuration_transactions));
112 }
113
114 pub fn rollback(ctx: anytype, txn_index: usize) void {
115 std.debug.assert(txn_index + 1 == ctx.dialect_load_transactions.items.len);
116 var txn = ctx.dialect_load_transactions.pop().?;
117
118 const created_ops = txn.created_ops.values();
119 var i_ops = created_ops.len;
120 while (i_ops > 0) {
121 i_ops -= 1;
122 _ = ctx.dialect_registry.operation_registry.removeOperation(created_ops[i_ops]);
123 }
124
125 const created_types = txn.created_types.values();
126 var i_types = created_types.len;
127 while (i_types > 0) {
128 i_types -= 1;
129 _ = ctx.type_interner.registry.removeType(created_types[i_types]);
130 }
131
132 const created_attrs = txn.created_attrs.values();
133 var i_attrs = created_attrs.len;
134 while (i_attrs > 0) {
135 i_attrs -= 1;
136 _ = ctx.attr_interner.registry.removeAttribute(created_attrs[i_attrs]);
137 }
138
139 ctx.attr_interner.registry.restoreNextAttributeId(txn.attr_next_id_snapshot);
140
141 if (txn.created_dialect_object) {
142 if (ctx.dialect_registry.loaded.fetchRemove(txn.dialect_name)) |removed| {
143 memory_ctx.segmentAllocator(ctx, .configuration_tables).destroy(removed.value);
144 }
145 }
146
147 _ = ctx.dialect_registry.load_state.remove(txn.dialect_name);
148
149 txn.deinit(memory_ctx.segmentAllocator(ctx, .configuration_transactions));
150 }
151
152 pub fn recordCreatedOperation(
153 ctx: anytype,
154 op_name: []const u8,
155 created: bool,
156 ) !void {
157 if (!created) return;
158 const ns = dialectNamespace(op_name);
159 if (ns.len == 0) return;
160 if (find(ctx, ns)) |txn| {
161 try appendUniqueName(&txn.created_ops, memory_ctx.segmentAllocator(ctx, .configuration_transactions), op_name);
162 }
163 }
164
165 pub fn recordCreatedType(
166 ctx: anytype,
167 type_name: []const u8,
168 created: bool,
169 ) !void {
170 if (!created) return;
171 const dotted_ns = dialectNamespace(type_name);
172 const ns = if (dotted_ns.len > 0) dotted_ns else type_name;
173 if (ns.len == 0) return;
174 if (find(ctx, ns)) |txn| {
175 try appendUniqueName(&txn.created_types, memory_ctx.segmentAllocator(ctx, .configuration_transactions), type_name);
176 }
177 }
178
179 pub fn recordCreatedAttribute(
180 ctx: anytype,
181 attr_name: []const u8,
182 created: bool,
183 ) !void {
184 if (!created) return;
185 const ns = dialectNamespace(attr_name);
186 if (ns.len == 0) return;
187 if (find(ctx, ns)) |txn| {
188 try appendUniqueName(&txn.created_attrs, memory_ctx.segmentAllocator(ctx, .configuration_transactions), attr_name);
189 }
190 }
191
192 pub fn markDialectObjectCreated(ctx: anytype, dialect_name: []const u8) void {
193 if (find(ctx, dialect_name)) |txn| {
194 txn.created_dialect_object = true;
195 }
196 }
197
198 fn find(
199 ctx: anytype,
200 dialect_ns: []const u8,
201 ) ?*DialectLoadTransaction {
202 var i = ctx.dialect_load_transactions.items.len;
203 while (i > 0) {
204 i -= 1;
205 const txn = &ctx.dialect_load_transactions.items[i];
206 if (std.mem.eql(u8, txn.dialect_name, dialect_ns)) return txn;
207 }
208 return null;
209 }
210
211 fn dialectNamespace(name: []const u8) []const u8 {
212 const name_struct = Operation.OperationName.init(name);
213 return name_struct.getDialectNamespace();
214 }
215
216 fn appendUniqueName(
217 list: *NameList,
218 allocator: std.mem.Allocator,
219 name: []const u8,
220 ) !void {
221 for (list.values()) |existing| {
222 if (std.mem.eql(u8, existing, name)) return;
223 }
224 try list.append(allocator, name);
225 }
226
227 test "dialect transaction record storage transfers exactly and spills transactionally" {
228 const testing = std.testing;
229 var failing = testing.FailingAllocator.init(testing.allocator, .{});
230
231 {
232 var txn = DialectLoadTransaction.init("test", 0);
233 defer txn.deinit(failing.allocator());
234
235 const before = failing.alloc_index;
236 const before_allocated = failing.allocated_bytes;
237 try txn.prepareRecordStorage(failing.allocator(), 2, 1);
238 try testing.expectEqual(before + 1, failing.alloc_index);
239 try testing.expectEqual(
240 before_allocated + 3 * @sizeOf([]const u8),
241 failing.allocated_bytes,
242 );
243
244 try appendUniqueName(&txn.created_ops, failing.allocator(), "test.first");
245 try appendUniqueName(&txn.created_ops, failing.allocator(), "test.second");
246 try appendUniqueName(&txn.created_types, failing.allocator(), "test.type");
247 try appendUniqueName(&txn.created_ops, failing.allocator(), "test.first");
248 try testing.expectEqual(before + 1, failing.alloc_index);
249
250 const storage_address = @intFromPtr(txn.record_storage.ptr);
251 const before_overflow = failing.alloc_index;
252 try testing.expectError(
253 error.OutOfMemory,
254 txn.prepareRecordStorage(failing.allocator(), std.math.maxInt(usize), 1),
255 );
256 try testing.expectEqual(before_overflow, failing.alloc_index);
257 try testing.expectEqual(storage_address, @intFromPtr(txn.record_storage.ptr));
258
259 failing.fail_index = failing.alloc_index;
260 try testing.expectError(
261 error.OutOfMemory,
262 txn.prepareRecordStorage(failing.allocator(), 1, 1),
263 );
264 try testing.expectEqual(storage_address, @intFromPtr(txn.record_storage.ptr));
265 try testing.expectEqual(@as(usize, 2), txn.created_ops.values().len);
266 try testing.expectEqual(@as(usize, 1), txn.created_types.values().len);
267
268 failing.fail_index = std.math.maxInt(usize);
269 const before_transfer_freed = failing.freed_bytes;
270 try txn.prepareRecordStorage(failing.allocator(), 1, 1);
271 try testing.expectEqual(@as(usize, 5), txn.record_storage.len);
272 try testing.expectEqual(
273 before_transfer_freed + 3 * @sizeOf([]const u8),
274 failing.freed_bytes,
275 );
276 try testing.expectEqualStrings("test.first", txn.created_ops.values()[0]);
277 try testing.expectEqualStrings("test.second", txn.created_ops.values()[1]);
278 try testing.expectEqualStrings("test.type", txn.created_types.values()[0]);
279
280 try appendUniqueName(&txn.created_ops, failing.allocator(), "test.third");
281 try appendUniqueName(&txn.created_types, failing.allocator(), "test.other_type");
282 const before_spill = failing.alloc_index;
283 failing.fail_index = before_spill;
284 try testing.expectError(
285 error.OutOfMemory,
286 appendUniqueName(&txn.created_ops, failing.allocator(), "test.spill"),
287 );
288 try testing.expectEqual(@as(usize, 3), txn.created_ops.values().len);
289
290 failing.fail_index = std.math.maxInt(usize);
291 try appendUniqueName(&txn.created_ops, failing.allocator(), "test.spill");
292 try testing.expectEqual(before_spill + 1, failing.alloc_index);
293 try testing.expectEqual(@as(usize, 4), txn.created_ops.values().len);
294
295 const before_attr = failing.alloc_index;
296 try appendUniqueName(&txn.created_attrs, failing.allocator(), "test.attr");
297 try testing.expectEqual(before_attr + 1, failing.alloc_index);
298 }
299
300 try testing.expectEqual(failing.allocated_bytes, failing.freed_bytes);
301 }