|
| 1 | +//! Async SHA256 verify via `VerifyWorkerPool`, then single-threaded `drainCompleted` → `relayIngestChunk`. |
| 2 | +//! Callers must use a **thread-safe** `pool_alloc` when `n_jobs > 0` (e.g. `std.heap.page_allocator`). |
| 3 | +//! After `init` with `n_jobs > 0`, do not move or copy `RelayAsyncVerifier`: `std.Thread.Pool` workers |
| 4 | +//! pin `&self.pool.pool` to the address used during `init`. |
| 5 | + |
| 6 | +const std = @import("std"); |
| 7 | +const broadcast_types = @import("../layer/broadcast_types.zig"); |
| 8 | +const dedup_registry_mod = @import("../layer/dedup_registry.zig"); |
| 9 | +const rs_strategy = @import("../layer/rs_strategy.zig"); |
| 10 | +const verify_queue_mod = @import("../layer/verify_queue.zig"); |
| 11 | +const verify_workers_mod = @import("../layer/verify_workers.zig"); |
| 12 | +const ChannelRs = @import("channel_rs.zig").ChannelRs; |
| 13 | + |
| 14 | +const Allocator = std.mem.Allocator; |
| 15 | + |
| 16 | +pub const RelayAsyncVerifier = struct { |
| 17 | + allocator: Allocator, |
| 18 | + channel: *ChannelRs, |
| 19 | + registry: ?*dedup_registry_mod.DedupRegistry, |
| 20 | + out_q: verify_queue_mod.VerifyQueue, |
| 21 | + pool: verify_workers_mod.VerifyWorkerPool, |
| 22 | + pending: std.AutoHashMapUnmanaged(u64, Pending) = .{}, |
| 23 | + mu: std.Thread.Mutex = .{}, |
| 24 | + next_handle: std.atomic.Value(u64) = .init(1), |
| 25 | + |
| 26 | + pub const Pending = struct { |
| 27 | + peer: []u8, |
| 28 | + message_id: []u8, |
| 29 | + chunk_id: rs_strategy.ChunkIdent, |
| 30 | + data: []u8, |
| 31 | + dedup: ?*broadcast_types.DedupCancel, |
| 32 | + }; |
| 33 | + |
| 34 | + pub const Error = Allocator.Error || error{ |
| 35 | + UnknownMessage, |
| 36 | + InvalidChunkIndex, |
| 37 | + OrphanVerifyRecord, |
| 38 | + SystemResources, |
| 39 | + Unexpected, |
| 40 | + LockedMemoryLimitExceeded, |
| 41 | + ThreadQuotaExceeded, |
| 42 | + }; |
| 43 | + |
| 44 | + /// `pool_alloc` must be thread-safe if `n_jobs > 0`. `allocator` backs pending metadata and `out_q` |
| 45 | + /// (including worker `push`es); it must be thread-safe when `n_jobs > 0`. |
| 46 | + /// Initializes `self` in place so embedded `std.Thread.Pool` is not copied after worker threads start. |
| 47 | + pub fn init( |
| 48 | + self: *RelayAsyncVerifier, |
| 49 | + allocator: Allocator, |
| 50 | + pool_alloc: Allocator, |
| 51 | + n_jobs: usize, |
| 52 | + channel: *ChannelRs, |
| 53 | + registry: ?*dedup_registry_mod.DedupRegistry, |
| 54 | + ) Error!void { |
| 55 | + self.* = .{ |
| 56 | + .allocator = allocator, |
| 57 | + .channel = channel, |
| 58 | + .registry = registry, |
| 59 | + .out_q = .{}, |
| 60 | + .pool = undefined, |
| 61 | + }; |
| 62 | + try self.pool.init(pool_alloc, allocator, n_jobs, &self.out_q); |
| 63 | + } |
| 64 | + |
| 65 | + pub fn deinit(self: *RelayAsyncVerifier) void { |
| 66 | + self.pool.deinit(); |
| 67 | + while (self.out_q.popFront()) |rec| { |
| 68 | + self.dropCompleted(rec); |
| 69 | + } |
| 70 | + self.out_q.deinit(self.allocator); |
| 71 | + var it = self.pending.iterator(); |
| 72 | + while (it.next()) |ent| { |
| 73 | + freePendingSlices(self.allocator, ent.value_ptr.*); |
| 74 | + } |
| 75 | + self.pending.deinit(self.allocator); |
| 76 | + } |
| 77 | + |
| 78 | + fn dropCompleted(self: *RelayAsyncVerifier, rec: verify_queue_mod.VerifyRecord) void { |
| 79 | + self.mu.lock(); |
| 80 | + const prev = self.pending.fetchRemove(rec.handle); |
| 81 | + self.mu.unlock(); |
| 82 | + if (prev) |kv| { |
| 83 | + freePendingSlices(self.allocator, kv.value); |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + /// Enqueue async verify. When results appear on the internal queue, call `drainCompleted` from the **same** thread that owns `channel`. |
| 88 | + pub fn submit( |
| 89 | + self: *RelayAsyncVerifier, |
| 90 | + message_id: []const u8, |
| 91 | + peer: []const u8, |
| 92 | + chunk_id: rs_strategy.ChunkIdent, |
| 93 | + data: []const u8, |
| 94 | + dedup: ?*broadcast_types.DedupCancel, |
| 95 | + ) Error!void { |
| 96 | + const strat = self.channel.sessionStrategy(message_id) orelse return error.UnknownMessage; |
| 97 | + const idx_i = chunk_id.index; |
| 98 | + if (idx_i < 0) return error.InvalidChunkIndex; |
| 99 | + const idx: usize = @intCast(idx_i); |
| 100 | + if (idx >= strat.preamble.chunk_hashes.len) return error.InvalidChunkIndex; |
| 101 | + |
| 102 | + var expected: [32]u8 = undefined; |
| 103 | + @memcpy(&expected, strat.preamble.chunk_hashes[idx][0..32]); |
| 104 | + |
| 105 | + const handle = self.next_handle.fetchAdd(1, .monotonic); |
| 106 | + |
| 107 | + const peer_o = try self.allocator.dupe(u8, peer); |
| 108 | + errdefer self.allocator.free(peer_o); |
| 109 | + const mid_o = try self.allocator.dupe(u8, message_id); |
| 110 | + errdefer self.allocator.free(mid_o); |
| 111 | + const data_o = try self.allocator.dupe(u8, data); |
| 112 | + errdefer self.allocator.free(data_o); |
| 113 | + |
| 114 | + self.mu.lock(); |
| 115 | + try self.pending.put(self.allocator, handle, .{ |
| 116 | + .peer = peer_o, |
| 117 | + .message_id = mid_o, |
| 118 | + .chunk_id = chunk_id, |
| 119 | + .data = data_o, |
| 120 | + .dedup = dedup, |
| 121 | + }); |
| 122 | + self.mu.unlock(); |
| 123 | + |
| 124 | + self.pool.schedule(.{ |
| 125 | + .handle = handle, |
| 126 | + .expected_hash = expected, |
| 127 | + .data = data, |
| 128 | + }) catch |err| { |
| 129 | + self.mu.lock(); |
| 130 | + if (self.pending.fetchRemove(handle)) |kv| { |
| 131 | + self.mu.unlock(); |
| 132 | + freePendingSlices(self.allocator, kv.value); |
| 133 | + } else { |
| 134 | + self.mu.unlock(); |
| 135 | + } |
| 136 | + return err; |
| 137 | + }; |
| 138 | + } |
| 139 | + |
| 140 | + /// `scheduleWait` + `WaitGroup.wait` + `drainCompleted(1)` for tests and blocking drivers. |
| 141 | + pub fn submitAwaitApply( |
| 142 | + self: *RelayAsyncVerifier, |
| 143 | + message_id: []const u8, |
| 144 | + peer: []const u8, |
| 145 | + chunk_id: rs_strategy.ChunkIdent, |
| 146 | + data: []const u8, |
| 147 | + dedup: ?*broadcast_types.DedupCancel, |
| 148 | + ) Error!void { |
| 149 | + const strat = self.channel.sessionStrategy(message_id) orelse return error.UnknownMessage; |
| 150 | + const idx_i = chunk_id.index; |
| 151 | + if (idx_i < 0) return error.InvalidChunkIndex; |
| 152 | + const idx: usize = @intCast(idx_i); |
| 153 | + if (idx >= strat.preamble.chunk_hashes.len) return error.InvalidChunkIndex; |
| 154 | + |
| 155 | + var expected: [32]u8 = undefined; |
| 156 | + @memcpy(&expected, strat.preamble.chunk_hashes[idx][0..32]); |
| 157 | + |
| 158 | + const handle = self.next_handle.fetchAdd(1, .monotonic); |
| 159 | + |
| 160 | + const peer_o = try self.allocator.dupe(u8, peer); |
| 161 | + errdefer self.allocator.free(peer_o); |
| 162 | + const mid_o = try self.allocator.dupe(u8, message_id); |
| 163 | + errdefer self.allocator.free(mid_o); |
| 164 | + const data_o = try self.allocator.dupe(u8, data); |
| 165 | + errdefer self.allocator.free(data_o); |
| 166 | + |
| 167 | + self.mu.lock(); |
| 168 | + try self.pending.put(self.allocator, handle, .{ |
| 169 | + .peer = peer_o, |
| 170 | + .message_id = mid_o, |
| 171 | + .chunk_id = chunk_id, |
| 172 | + .data = data_o, |
| 173 | + .dedup = dedup, |
| 174 | + }); |
| 175 | + self.mu.unlock(); |
| 176 | + |
| 177 | + var wg: std.Thread.WaitGroup = .{}; |
| 178 | + self.pool.scheduleWait(&wg, .{ |
| 179 | + .handle = handle, |
| 180 | + .expected_hash = expected, |
| 181 | + .data = data, |
| 182 | + }) catch |err| { |
| 183 | + self.mu.lock(); |
| 184 | + if (self.pending.fetchRemove(handle)) |kv| { |
| 185 | + self.mu.unlock(); |
| 186 | + freePendingSlices(self.allocator, kv.value); |
| 187 | + } else { |
| 188 | + self.mu.unlock(); |
| 189 | + } |
| 190 | + return err; |
| 191 | + }; |
| 192 | + wg.wait(); |
| 193 | + |
| 194 | + const n = try self.drainCompleted(1); |
| 195 | + if (n != 1) return error.OrphanVerifyRecord; |
| 196 | + } |
| 197 | + |
| 198 | + /// Pop up to `max` completed verify records and run `relayIngestChunk` for `.accepted`. |
| 199 | + pub fn drainCompleted(self: *RelayAsyncVerifier, max: usize) Error!usize { |
| 200 | + var done: usize = 0; |
| 201 | + var i: usize = 0; |
| 202 | + while (i < max) : (i += 1) { |
| 203 | + const rec = self.out_q.popFront() orelse break; |
| 204 | + |
| 205 | + self.mu.lock(); |
| 206 | + const prev = self.pending.fetchRemove(rec.handle); |
| 207 | + self.mu.unlock(); |
| 208 | + |
| 209 | + const kv = prev orelse return error.OrphanVerifyRecord; |
| 210 | + const pend = kv.value; |
| 211 | + if (rec.verdict == .accepted) { |
| 212 | + _ = try self.channel.relayIngestChunk( |
| 213 | + self.registry, |
| 214 | + pend.message_id, |
| 215 | + pend.peer, |
| 216 | + pend.chunk_id, |
| 217 | + pend.data, |
| 218 | + pend.dedup, |
| 219 | + ); |
| 220 | + } |
| 221 | + freePendingSlices(self.allocator, pend); |
| 222 | + done += 1; |
| 223 | + } |
| 224 | + return done; |
| 225 | + } |
| 226 | +}; |
| 227 | + |
| 228 | +fn freePendingSlices(allocator: Allocator, p: RelayAsyncVerifier.Pending) void { |
| 229 | + allocator.free(p.peer); |
| 230 | + allocator.free(p.message_id); |
| 231 | + allocator.free(p.data); |
| 232 | +} |
| 233 | + |
| 234 | +test "relay async verify submitAwaitApply ingests chunk" { |
| 235 | + const gpa = std.testing.allocator; |
| 236 | + var eng = try @import("engine.zig").Engine.init(gpa, "local", .{}); |
| 237 | + defer eng.deinit(); |
| 238 | + |
| 239 | + const cfg = @import("../layer/rs_init.zig").RsConfig{ |
| 240 | + .data_shards = 4, |
| 241 | + .parity_shards = 2, |
| 242 | + .chunk_len = 0, |
| 243 | + .bitmap_threshold = 0, |
| 244 | + .forward_multiplier = 4, |
| 245 | + .disable_bitmap = false, |
| 246 | + }; |
| 247 | + |
| 248 | + const ch = try eng.attachChannelRs("topic", cfg); |
| 249 | + try ch.addMember("peerA"); |
| 250 | + |
| 251 | + const payload = [_]u8{ 1, 2, 3, 4 }; |
| 252 | + var origin = try rs_strategy.RsStrategy.newOrigin(gpa, cfg, &payload); |
| 253 | + defer origin.deinit(); |
| 254 | + |
| 255 | + try ch.attachRelaySession("m1", &origin.preamble); |
| 256 | + |
| 257 | + // Workers push into `out_q`: use a thread-safe allocator for verifier state, not `gpa`. |
| 258 | + const valloc = std.heap.page_allocator; |
| 259 | + var verifier: RelayAsyncVerifier = undefined; |
| 260 | + try RelayAsyncVerifier.init(&verifier, valloc, valloc, 1, ch, null); |
| 261 | + defer verifier.deinit(); |
| 262 | + |
| 263 | + const c0 = origin.chunks[0]; |
| 264 | + try verifier.submitAwaitApply("m1", "peerA", .{ .index = 0 }, c0, null); |
| 265 | + |
| 266 | + const st = ch.sessionStrategy("m1").?; |
| 267 | + try std.testing.expect(st.haveChunk(.{ .index = 0 })); |
| 268 | +} |
| 269 | + |
| 270 | +test "relay async verify invalid chunk does not ingest" { |
| 271 | + const gpa = std.testing.allocator; |
| 272 | + var eng = try @import("engine.zig").Engine.init(gpa, "local", .{}); |
| 273 | + defer eng.deinit(); |
| 274 | + |
| 275 | + const cfg = @import("../layer/rs_init.zig").RsConfig{ |
| 276 | + .data_shards = 4, |
| 277 | + .parity_shards = 2, |
| 278 | + .chunk_len = 0, |
| 279 | + .bitmap_threshold = 0, |
| 280 | + .forward_multiplier = 4, |
| 281 | + .disable_bitmap = false, |
| 282 | + }; |
| 283 | + |
| 284 | + const ch = try eng.attachChannelRs("topic", cfg); |
| 285 | + try ch.addMember("peerA"); |
| 286 | + |
| 287 | + const payload = [_]u8{ 9, 9, 9 }; |
| 288 | + var origin = try rs_strategy.RsStrategy.newOrigin(gpa, cfg, &payload); |
| 289 | + defer origin.deinit(); |
| 290 | + |
| 291 | + try ch.attachRelaySession("m1", &origin.preamble); |
| 292 | + |
| 293 | + const valloc = std.heap.page_allocator; |
| 294 | + var verifier: RelayAsyncVerifier = undefined; |
| 295 | + try RelayAsyncVerifier.init(&verifier, valloc, valloc, 1, ch, null); |
| 296 | + defer verifier.deinit(); |
| 297 | + |
| 298 | + var bad = try gpa.dupe(u8, origin.chunks[0]); |
| 299 | + defer gpa.free(bad); |
| 300 | + if (bad.len > 0) bad[0] +%= 1; |
| 301 | + |
| 302 | + try verifier.submitAwaitApply("m1", "peerA", .{ .index = 0 }, bad, null); |
| 303 | + |
| 304 | + const st = ch.sessionStrategy("m1").?; |
| 305 | + try std.testing.expect(!st.haveChunk(.{ .index = 0 })); |
| 306 | +} |
0 commit comments