tiny.sql.history.verification
Defined in history.
API (14)
Actions
Public operations.
Capacity.deriveWorkers.deinitWorkers.startWorkers.verifyWorkspace.activateWorkspace.resetverifyFile: Reports the earliest bad record while leaving the file untouched.
Types and contracts
Public types and contracts.
CapacityExhaustionLimits: A complete scan uses caller storage and never opens the history for write.ReportStorageWorkersWorkspace
Source
Source: lib/sql/src/history/validate.zig:1042
zig
pub const Inspection = struct { last_valid_boundary: usize, bad: ?BadRecord = null,};Source: lib/sql/src/history/root.zig:7
zig
pub const verification = @import("verify.zig");Source: lib/sql/src/history/verify.zig
zig
const std = @import("std");const sys = @import("sys");const sql = @import("../root.zig");const record_mod = @import("record.zig");const batch_mod = @import("batch.zig");const validate_mod = @import("validate.zig");const version = sql.version;const max_lanes: usize = 4;/// A complete scan uses caller storage and never opens the history for write.pub const Limits = struct { metadata_bytes_max: usize = 4 * 1024 * 1024, dependencies_max: usize = 2 * 1024 * 1024, conflict_bytes_max: usize = 4 * 1024 * 1024, working_bytes_max: usize,};pub const Capacity = struct { bytes: usize, pub fn derive(limits: Limits) error{CapacityOverflow}!Capacity { if (limits.metadata_bytes_max == 0 or limits.dependencies_max == 0 or limits.conflict_bytes_max == 0 or limits.working_bytes_max == 0) { return error.CapacityOverflow; } if (limits.conflict_bytes_max > std.math.maxInt(u32)) { return error.CapacityOverflow; } _ = std.math.add( usize, limits.metadata_bytes_max, limits.conflict_bytes_max, ) catch return error.CapacityOverflow; return .{ .bytes = limits.working_bytes_max }; }};pub const Storage = struct { bytes: []u8,};pub const Exhaustion = struct { name: []const u8, bytes_in_use: usize, requested: usize, available: usize,};pub const Workspace = struct { storage: []u8, fixed: std.heap.FixedBufferAllocator, limits: Limits, exhaustion: ?Exhaustion = null, fn allocator(self: *Workspace) std.mem.Allocator { return .{ .ptr = self, .vtable = &.{ .alloc = alloc, .resize = resize, .remap = remap, .free = free, }, }; } fn alloc(ctx: *anyopaque, len: usize, alignment: std.mem.Alignment, ra: usize) ?[*]u8 { const self: *Workspace = @ptrCast(@alignCast(ctx)); const underlying = self.fixed.allocator(); const result = underlying.vtable.alloc(underlying.ptr, len, alignment, ra); if (result == null) self.noteExhaustion(len); return result; } fn resize( ctx: *anyopaque, memory: []u8, alignment: std.mem.Alignment, len: usize, ra: usize, ) bool { const self: *Workspace = @ptrCast(@alignCast(ctx)); const underlying = self.fixed.allocator(); return underlying.vtable.resize(underlying.ptr, memory, alignment, len, ra); } fn remap( ctx: *anyopaque, memory: []u8, alignment: std.mem.Alignment, len: usize, ra: usize, ) ?[*]u8 { const self: *Workspace = @ptrCast(@alignCast(ctx)); const underlying = self.fixed.allocator(); return underlying.vtable.remap(underlying.ptr, memory, alignment, len, ra); } fn free(ctx: *anyopaque, memory: []u8, alignment: std.mem.Alignment, ra: usize) void { const self: *Workspace = @ptrCast(@alignCast(ctx)); const underlying = self.fixed.allocator(); underlying.vtable.free(underlying.ptr, memory, alignment, ra); } fn noteExhaustion(self: *Workspace, requested: usize) void { const used = self.fixed.end_index; self.exhaustion = .{ .name = "working_bytes_max", .bytes_in_use = used, .requested = requested, .available = self.storage.len - used, }; } pub fn activate(limits: Limits, storage: Storage) error{ CapacityOverflow, CapacityExceeded, }!Workspace { const capacity = try Capacity.derive(limits); if (storage.bytes.len < capacity.bytes) return error.CapacityExceeded; const bytes = storage.bytes[0..capacity.bytes]; return .{ .storage = bytes, .fixed = std.heap.FixedBufferAllocator.init(bytes), .limits = limits, }; } pub fn reset(self: *Workspace) void { self.fixed.reset(); self.exhaustion = null; }};pub const Report = validate_mod.Inspection;/// Reports the earliest bad record while leaving the file untouched.pub fn verifyFile( workspace: *Workspace, io: std.Io, file: std.Io.File, end: usize, control: sql.wal.Control,) !Report { workspace.reset(); return validate_mod.inspectWhole( workspace.allocator(), io, file, end, .{ .metadata_bytes_max = workspace.limits.metadata_bytes_max, .dependencies_max = workspace.limits.dependencies_max, .conflict_bytes_max = workspace.limits.conflict_bytes_max, }, control, ) catch |err| switch (err) { error.OutOfMemory => { std.debug.assert(workspace.exhaustion != null); return error.CapacityExceeded; }, else => return err, };}const Work = struct { bytes: []const u8 = &.{}, records: []batch_mod.Record = &.{},};pub const Workers = struct { handles: [max_lanes - 1]sys.thread.JoinHandle = undefined, started: usize = 0, mutex: sys.thread.Mutex = .{}, ready: sys.thread.Condition = .{}, done: sys.thread.Condition = .{}, generation: usize = 0, completed: usize = 0, shutdown: bool = false, work: Work = .{}, next_index: std.atomic.Value(usize) = std.atomic.Value(usize).init(0), pub fn start(self: *Workers, requested_lanes: usize) void { if (comptime !sys.thread.threadsSupported()) return; const background = @min(requested_lanes -| 1, self.handles.len); while (self.started < background) { self.handles[self.started] = sys.thread.spawn(workerLoop, .{self}) catch return; self.started += 1; } } pub fn deinit(self: *Workers) void { self.mutex.lock(); self.shutdown = true; self.ready.broadcast(); self.mutex.unlock(); for (self.handles[0..self.started]) |handle| handle.join(); self.* = undefined; } pub fn verify(self: *Workers, bytes: []const u8, records: []batch_mod.Record) void { if (records.len == 0) return; if (self.started == 0 or records.len == 1) { verifySerial(bytes, records); return; } self.mutex.lock(); self.work = .{ .bytes = bytes, .records = records }; self.next_index.store(0, .monotonic); self.completed = 0; self.generation +%= 1; self.ready.broadcast(); self.mutex.unlock(); runWork(self, self.work); self.mutex.lock(); while (self.completed != self.started) self.done.wait(&self.mutex); self.mutex.unlock(); } fn workerLoop(self: *Workers) void { var observed_generation: usize = 0; self.mutex.lock(); while (true) { while (!self.shutdown and self.generation == observed_generation) self.ready.wait(&self.mutex); if (self.shutdown) { self.mutex.unlock(); return; } observed_generation = self.generation; const work = self.work; self.mutex.unlock(); runWork(self, work); self.mutex.lock(); self.completed += 1; if (self.completed == self.started) self.done.signal(); } }};fn runWork(workers: *Workers, work: Work) void { while (true) { const index = workers.next_index.fetchAdd(1, .monotonic); if (index >= work.records.len) return; verifyRecord(work.bytes, &work.records[index]); }}fn verifySerial(bytes: []const u8, records: []batch_mod.Record) void { for (records) |*record| verifyRecord(bytes, record);}fn verifyRecord(bytes: []const u8, record: *batch_mod.Record) void { const payload = bytes[record.payload_start..][0..record.payload_len]; record.valid_hash = version.same(record.expected, record_mod.recordHash(record.kind_value, payload));}test "history verification reports a missing dependency without writing" { const store_mod = @import("store.zig"); const allocator = std.testing.allocator; const io = std.Options.debug_io; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var file = try tmp.dir.createFile(io, "verify.history", .{ .read = true }); defer file.close(io); var payload: std.ArrayList(u8) = .empty; defer payload.deinit(allocator); const root = version.emptyHash("verify-root"); try record_mod.appendHash(allocator, &payload, root); try record_mod.appendU32(allocator, &payload, 0); const boundary = try store_mod.writeTestingRecord(file, 0, .commit, payload.items); payload.clearRetainingCapacity(); const missing = version.emptyHash("verify-missing"); try record_mod.appendHash(allocator, &payload, missing); try record_mod.appendBytes(allocator, &payload, "main"); const end = try store_mod.writeTestingRecord(file, boundary, .ref, payload.items); var before: [256]u8 = undefined; try std.testing.expect(end <= before.len); try std.testing.expectEqual(end, try file.readPositionalAll(io, before[0..end], 0)); const limits = Limits{ .metadata_bytes_max = 128, .dependencies_max = 16, .conflict_bytes_max = 128, .working_bytes_max = 1024 * 1024, }; const capacity = try Capacity.derive(limits); const bytes = try allocator.alloc(u8, capacity.bytes); defer allocator.free(bytes); var workspace = try Workspace.activate(limits, .{ .bytes = bytes }); const report = try verifyFile(&workspace, io, file, end, .{}); try std.testing.expectEqual(boundary, report.last_valid_boundary); try std.testing.expectEqual(boundary, report.bad.?.offset); try std.testing.expectEqual(record_mod.RecordKind.ref, report.bad.?.kind.?); try std.testing.expect(version.same(missing, report.bad.?.dependency.?.key)); var after: [256]u8 = undefined; try std.testing.expectEqual(end, try file.readPositionalAll(io, after[0..end], 0)); try std.testing.expectEqualSlices(u8, before[0..end], after[0..end]); after[boundary] ^= 0xff; try file.writePositionalAll(io, after[boundary..][0..1], boundary); const malformed = try verifyFile(&workspace, io, file, end, .{}); try std.testing.expectEqual(boundary, malformed.last_valid_boundary); try std.testing.expectEqual(boundary, malformed.bad.?.offset); try std.testing.expect(malformed.bad.?.kind == null); var unchanged: [256]u8 = undefined; try std.testing.expectEqual(end, try file.readPositionalAll(io, unchanged[0..end], 0)); try std.testing.expectEqualSlices(u8, after[0..end], unchanged[0..end]); const small_limits = Limits{ .metadata_bytes_max = 128, .dependencies_max = 16, .conflict_bytes_max = 128, .working_bytes_max = 64, }; var small_storage: [64]u8 = undefined; var small = try Workspace.activate(small_limits, .{ .bytes = &small_storage }); try std.testing.expectError(error.CapacityExceeded, verifyFile(&small, io, file, end, .{})); const exhausted = small.exhaustion.?; try std.testing.expectEqualStrings("working_bytes_max", exhausted.name); try std.testing.expectEqual(small.fixed.end_index, exhausted.bytes_in_use); try std.testing.expectEqual(small_storage.len - exhausted.bytes_in_use, exhausted.available); try std.testing.expect(exhausted.requested > 0);}Audit
| Definitions | 15 |
|---|---|
| Public names | 15 |
| Members | 26 |
| Version | 26.7.0 |
| Revision | daab053ee433 |