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 }