Skip to content

Commit 89ab8a7

Browse files
authored
Merge pull request #18 from ch4r10t33r/feat/issue-11-async-verify-apply
zig_ethp2p: async verify then relay ingest (issue #11)
2 parents 9f1ec4d + 461a4d8 commit 89ab8a7

3 files changed

Lines changed: 328 additions & 8 deletions

File tree

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
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+
}

src/layer/verify_workers.zig

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,30 @@ pub const VerifyJob = struct {
1616
};
1717

1818
pub const VerifyWorkerPool = struct {
19+
/// Thread-safe when `n_jobs > 0` (pool internals and per-job `dupe`).
1920
allocator: Allocator,
21+
/// Allocator for `out.push` growth; must match whoever calls `VerifyQueue.deinit`.
22+
queue_allocator: Allocator,
2023
pool: std.Thread.Pool,
2124
out: *verify_queue_mod.VerifyQueue,
2225
out_mutex: std.Thread.Mutex = .{},
2326

24-
pub fn init(allocator: Allocator, n_jobs: usize, out: *verify_queue_mod.VerifyQueue) !VerifyWorkerPool {
25-
var pool: std.Thread.Pool = undefined;
26-
try pool.init(.{ .allocator = allocator, .n_jobs = @as(?usize, n_jobs) });
27-
return .{
28-
.allocator = allocator,
29-
.pool = pool,
27+
/// Initializes `self` in place. Worker threads capture `&self.pool`; the `Thread.Pool` must not be
28+
/// stack-copied after `init` (see `std.Thread.Pool.init`).
29+
pub fn init(
30+
self: *VerifyWorkerPool,
31+
pool_allocator: Allocator,
32+
queue_allocator: Allocator,
33+
n_jobs: usize,
34+
out: *verify_queue_mod.VerifyQueue,
35+
) !void {
36+
self.* = .{
37+
.allocator = pool_allocator,
38+
.queue_allocator = queue_allocator,
39+
.pool = undefined,
3040
.out = out,
3141
};
42+
try self.pool.init(.{ .allocator = pool_allocator, .n_jobs = @as(?usize, n_jobs) });
3243
}
3344

3445
pub fn deinit(self: *VerifyWorkerPool) void {
@@ -72,7 +83,7 @@ fn runOne(
7283

7384
parent.out_mutex.lock();
7485
defer parent.out_mutex.unlock();
75-
parent.out.push(parent.allocator, .{ .handle = handle, .verdict = verdict }) catch {
86+
parent.out.push(parent.queue_allocator, .{ .handle = handle, .verdict = verdict }) catch {
7687
@panic("VerifyWorkerPool: out queue OOM");
7788
};
7889
}
@@ -84,7 +95,8 @@ test "verify worker pool inline pushes verdicts" {
8495
defer q.deinit(gpa);
8596

8697
// No pool threads: testing allocator is single-threaded only. `verifyInline` still runs `runOne`.
87-
var pool = try VerifyWorkerPool.init(gpa, 0, &q);
98+
var pool: VerifyWorkerPool = undefined;
99+
try pool.init(gpa, gpa, 0, &q);
88100
defer pool.deinit();
89101

90102
var good: [32]u8 = undefined;

src/root.zig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ pub const broadcast = struct {
3434
pub const channel_rs = @import("broadcast/channel_rs.zig");
3535
pub const session_rs = @import("broadcast/session_rs.zig");
3636
pub const gossip = @import("broadcast/gossip.zig");
37+
pub const relay_async_verify = @import("broadcast/relay_async_verify.zig");
3738
};
3839

3940
test {
@@ -54,4 +55,5 @@ test {
5455
_ = broadcast.session_rs;
5556
_ = broadcast.observer;
5657
_ = broadcast.gossip;
58+
_ = broadcast.relay_async_verify;
5759
}

0 commit comments

Comments
 (0)