1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
//! DPDK replication transport — smoltcp-based sender and receiver paths.
//!
//! Mirrors the kernel-TCP variants in `mod.rs` but uses `DpdkTransport`
//! (a `smoltcp` socket over DPDK queue pairs) instead of `TcpStream`.
//! The wire protocol is identical — see `protocol.rs` for the message catalogue.
#![cfg(feature = "dpdk")]
use std::io;
use std::marker::PhantomData;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use tracing::{debug, info, warn};
use melin_app::Application;
use melin_journal::JournalWrite;
use melin_journal::replication::ReplicationConsumer;
use melin_transport_core::pipeline::{JournalStage, JournalStageRun};
use super::receiver_transport::{
FrameResult, ReceiverTransport, SessionExit, compact_recv_buf, streaming_loop,
try_extract_frame,
};
use super::{
ReceiverResult, ReplicaPipelineHandles, ReplicationMetrics,
build_replica_pipeline_with_threads, sleep_checking_flags, teardown_replica_pipeline,
update_dual_replication_cursor,
};
use melin_transport_core::replication::catchup::{
CatchUpResult, can_catch_up_from_journal, catch_up_from_journal_with, snapshot_transfer_with,
};
use melin_transport_core::replication::protocol::{
Ack, Handshake, MAX_CONTROL_FRAME, MAX_DATA_FRAME, PrimaryMessage, ReplicaMessage,
decode_primary_message, decode_replica_message, encode_ack, encode_handshake, encode_heartbeat,
encode_stream_start,
};
// ---------------------------------------------------------------------------
// DPDK ReceiverTransport implementation
// ---------------------------------------------------------------------------
/// DPDK/smoltcp-backed receiver transport for the replica side.
///
/// Wraps a `DpdkTransport` + `SocketHandle` and implements
/// [`ReceiverTransport`] so the generic [`streaming_loop`] can drive it
/// identically to the kernel io_uring path.
struct DpdkReceiverTransport<'a> {
transport: &'a mut melin_dpdk::DpdkTransport,
handle: melin_dpdk::SocketHandle,
send_buf: Vec<u8>,
}
const ACK_RETRY_CAP: u32 = 32;
impl ReceiverTransport for DpdkReceiverTransport<'_> {
fn poll_recv(&mut self, recv_buf: &mut Vec<u8>) -> io::Result<bool> {
self.transport.poll();
let before = recv_buf.len();
self.transport.recv_into_vec(self.handle, recv_buf);
Ok(recv_buf.len() > before)
}
fn send_ack(&mut self, ack: &Ack) -> io::Result<bool> {
self.send_buf.clear();
encode_ack(ack, &mut self.send_buf);
let mut attempts: u32 = 0;
loop {
if self.transport.queue_send(self.handle, &self.send_buf) {
// Flush immediately so the ACK reaches the primary
// without waiting for the next poll_recv iteration.
self.transport.poll();
return Ok(true);
}
attempts += 1;
if attempts >= ACK_RETRY_CAP {
tracing::warn!("DPDK ack send failed after {ACK_RETRY_CAP} retries");
return Ok(false);
}
self.transport.poll();
if !self.transport.is_active(self.handle) {
return Err(io::Error::other("replica disconnected during ack send"));
}
}
}
fn ack_in_flight(&self) -> bool {
false
}
fn is_connected(&mut self) -> bool {
self.transport.is_active(self.handle)
}
}
/// Per-slot state for the DPDK replication sender.
enum SlotState {
/// No replica connected on this slot.
Idle,
/// Replica connected, performing handshake.
Handshaking(melin_dpdk::SocketHandle),
/// Streaming journal data to replica.
Streaming(melin_dpdk::SocketHandle),
}
/// Per-replica slot — owns its ring consumer and state machine.
struct DpdkReplicaSlot {
state: SlotState,
consumer: ReplicationConsumer,
active_flag: Arc<AtomicBool>,
evict_flag: Arc<AtomicBool>,
recv_buf: Vec<u8>,
send_buf: Vec<u8>,
last_send: std::time::Instant,
last_sequence: u64,
/// Per-slot acked cursor. `u64::MAX` when not streaming —
/// doesn't block the replication cursor (min of both slots).
acked_cursor: u64,
}
/// Step-able DPDK replication state. Owns both slot state machines and the
/// shared cursors / metrics, but does NOT own the `DpdkTransport` — it
/// reaches into one supplied by the caller per call. This shape lets the
/// primary's single DPDK poll thread drive replication alongside client
/// traffic by calling `tick()` once per poll iteration and dispatching
/// `accept_connection()` for any `AcceptedConnection` that arrives on the
/// replication listen port.
///
/// Parameterised over `A: Application` so successive `tick` calls cannot
/// disagree on the application type — the journal catch-up and snapshot
/// transfer paths both decode events through `A`, so a mid-flight switch
/// would silently corrupt the wire. The driver carries no `A`-typed
/// data, hence the `PhantomData`.
pub struct DpdkReplicationDriver<A: Application> {
slots: [DpdkReplicaSlot; 2],
replication_cursor: Arc<AtomicU64>,
fastest_replica_cursor: Arc<AtomicU64>,
genesis_entry: Vec<u8>,
journal_path: std::path::PathBuf,
replica_ready: Arc<AtomicBool>,
replicas_connected: Arc<AtomicU32>,
metrics: Arc<ReplicationMetrics>,
batch_size: usize,
heartbeat_interval: std::time::Duration,
// Anchors the `A` type parameter — the struct holds no app-typed
// state, but `tick`'s journal-catchup and snapshot-transfer paths
// do, and we want the type system to enforce that the same `A`
// flows through every call rather than relying on every call site
// to spell `::<A>` consistently.
_app: PhantomData<fn(A)>,
}
impl<A: Application> DpdkReplicationDriver<A> {
#[allow(clippy::too_many_arguments)]
pub fn new(
repl_consumers: [ReplicationConsumer; 2],
replication_cursor: Arc<AtomicU64>,
fastest_replica_cursor: Arc<AtomicU64>,
genesis_entry: Vec<u8>,
journal_path: std::path::PathBuf,
replica_ready: Arc<AtomicBool>,
replicas_connected: Arc<AtomicU32>,
evict_flags: [Arc<AtomicBool>; 2],
active_flags: [Arc<AtomicBool>; 2],
metrics: Arc<ReplicationMetrics>,
batch_size: usize,
heartbeat_secs: u64,
) -> Self {
let [consumer_0, consumer_1] = repl_consumers;
let now = std::time::Instant::now();
DpdkReplicationDriver {
slots: [
DpdkReplicaSlot {
state: SlotState::Idle,
consumer: consumer_0,
active_flag: Arc::clone(&active_flags[0]),
evict_flag: Arc::clone(&evict_flags[0]),
recv_buf: Vec::with_capacity(4096),
send_buf: Vec::with_capacity(512 * 1024),
last_send: now,
last_sequence: 0,
acked_cursor: u64::MAX,
},
DpdkReplicaSlot {
state: SlotState::Idle,
consumer: consumer_1,
active_flag: Arc::clone(&active_flags[1]),
evict_flag: Arc::clone(&evict_flags[1]),
recv_buf: Vec::with_capacity(4096),
send_buf: Vec::with_capacity(512 * 1024),
last_send: now,
last_sequence: 0,
acked_cursor: u64::MAX,
},
],
replication_cursor,
fastest_replica_cursor,
genesis_entry,
journal_path,
replica_ready,
replicas_connected,
metrics,
batch_size,
heartbeat_interval: std::time::Duration::from_secs(heartbeat_secs),
_app: PhantomData,
}
}
/// Take ownership of a freshly-accepted connection on the replication
/// port. Assigns it to the first idle slot, or closes it if both slots
/// are occupied (dual-repl cap). Caller is responsible for filtering by
/// `AcceptedConnection::listen_port`.
pub fn accept_connection(
&mut self,
peer: std::net::SocketAddr,
handle: melin_dpdk::SocketHandle,
transport: &mut melin_dpdk::DpdkTransport,
) {
let idle_slot = self
.slots
.iter()
.position(|s| matches!(s.state, SlotState::Idle));
if let Some(idx) = idle_slot {
info!(peer = ?peer, slot = idx, "replica connected via DPDK");
self.replicas_connected.fetch_add(1, Ordering::Release);
self.slots[idx].recv_buf.clear();
self.slots[idx].state = SlotState::Handshaking(handle);
} else {
debug!(peer = ?peer, "replica rejected — both slots occupied");
transport.close(handle);
}
}
/// Drive both slots' state machines for one poll iteration. Returns
/// `true` if at least one slot is currently active (Handshaking or
/// Streaming) — caller can use this to decide whether to busy-spin
/// on idle.
pub fn tick(
&mut self,
transport: &mut melin_dpdk::DpdkTransport,
shutdown: &AtomicBool,
) -> bool {
// Local rebinds for readability — the body below was lifted from
// the previous run_sender_dpdk thread, mostly verbatim, so keep
// the variable names matching.
let slots = &mut self.slots;
let replication_cursor = &self.replication_cursor;
let fastest_replica_cursor = &self.fastest_replica_cursor;
let genesis_entry = &self.genesis_entry;
let journal_path = &self.journal_path;
let replica_ready = &self.replica_ready;
let replicas_connected = &self.replicas_connected;
let metrics = &self.metrics;
let batch_size = self.batch_size;
let heartbeat_interval = self.heartbeat_interval;
// Check eviction flags from the journal stage.
for i in 0..2 {
let evicting = slots[i].evict_flag.load(Ordering::Acquire)
&& !matches!(slots[i].state, SlotState::Idle);
if !evicting {
continue;
}
metrics.evictions_total.fetch_add(1, Ordering::Relaxed);
warn!(
slot = i,
"evicting slow replica (ring backpressure timeout, DPDK)"
);
let slot = &mut slots[i];
if let SlotState::Streaming(h) | SlotState::Handshaking(h) = slot.state {
transport.close(h);
}
// Zero per-slot metrics BEFORE the active_flag Release so
// a reader cannot observe `active=true` paired with `cursor=0`
// on weak-memory architectures (see B2 in the follow-ups doc).
metrics.acked_sequence[i].store(0, Ordering::Relaxed);
metrics.in_memory_sequence[i].store(0, Ordering::Relaxed);
metrics.catching_up[i].store(false, Ordering::Relaxed);
slot.active_flag.store(false, Ordering::Release);
slot.evict_flag.store(false, Ordering::Release);
slot.acked_cursor = u64::MAX;
slot.recv_buf.clear();
// Drop any unread ring entries so a reconnecting replica
// on this slot doesn't replay pre-eviction data and stall
// the primary's replication cursor. See kernel-TCP path
// in tcp_sender.rs for the detailed rationale.
slot.consumer.skip_to_producer();
slot.state = SlotState::Idle;
replicas_connected.fetch_sub(1, Ordering::Release);
// Recompute the shared replication cursor from the *surviving*
// slot's progress so the response stage's gate can advance.
// Without this, `replication_cursor` stays frozen at its pre-
// eviction value (the min that included this slot's last
// ack), and the primary stops acking client requests even
// though the surviving replica is healthy. The kernel-TCP
// path does the equivalent in tcp_sender.rs.
let other_acked = slots[1 - i].acked_cursor;
update_dual_replication_cursor(
u64::MAX,
other_acked,
replication_cursor,
fastest_replica_cursor,
);
if replicas_connected.load(Ordering::Relaxed) == 0 {
warn!("all replicas disconnected — trading halted");
}
}
let mut any_active = false;
for slot_idx in 0..2 {
// Split the array to get disjoint mutable/immutable borrows.
// This lets us read the other slot's acked_cursor while
// mutably borrowing the current slot.
let (slot, other_acked) = {
let (left, right) = slots.split_at_mut(1);
if slot_idx == 0 {
(&mut left[0], right[0].acked_cursor)
} else {
(&mut right[0], left[0].acked_cursor)
}
};
match slot.state {
SlotState::Idle => {
// Drain ring to keep it flowing. The journal stage
// skips inactive rings (active_flag=false), but there
// may be residual entries from before the flag was cleared.
while slot.consumer.try_read().is_some() {
slot.consumer.commit();
}
}
SlotState::Handshaking(handle) => {
any_active = true;
// Check for disconnect during handshake.
if !transport.is_active(handle) {
warn!(
slot = slot_idx,
"replica disconnected during handshake (DPDK)"
);
slot.state = SlotState::Idle;
slot.acked_cursor = u64::MAX;
slot.recv_buf.clear();
replicas_connected.fetch_sub(1, Ordering::Release);
update_dual_replication_cursor(
u64::MAX,
other_acked,
replication_cursor,
fastest_replica_cursor,
);
continue;
}
// Try to read handshake frame.
transport.recv_into_vec(handle, &mut slot.recv_buf);
match try_extract_frame(&slot.recv_buf, MAX_CONTROL_FRAME) {
FrameResult::Complete(payload_start, frame_end) => {
let payload = &slot.recv_buf[payload_start..frame_end];
match decode_replica_message(payload) {
Ok(ReplicaMessage::Handshake(h)) => {
info!(
slot = slot_idx,
last_sequence = h.last_sequence,
"replica handshake received (DPDK)"
);
metrics.catching_up[slot_idx].store(true, Ordering::Relaxed);
// Probe whether journal catch-up is possible.
let can_catch_up = match can_catch_up_from_journal(
journal_path,
h.last_sequence,
) {
Ok(v) => v,
Err(e) => {
warn!(slot = slot_idx, error = %e, "catch-up probe failed — disconnecting");
transport.close(handle);
slot.state = SlotState::Idle;
slot.recv_buf.clear();
replicas_connected.fetch_sub(1, Ordering::Release);
if replicas_connected.load(Ordering::Relaxed) == 0 {
replication_cursor
.store(u64::MAX, Ordering::Release);
}
continue;
}
};
compact_recv_buf(&mut slot.recv_buf, frame_end);
// DPDK publisher: queue_send + poll to keep
// smoltcp timers alive during bulk transfer.
let mut dpdk_publish = |buf: &[u8]| -> std::io::Result<()> {
loop {
if transport.queue_send(handle, buf) {
break;
}
transport.poll();
if !transport.is_active(handle) {
return Err(std::io::Error::other(
"replica disconnected during send (TX backpressure)",
));
}
}
transport.poll();
Ok(())
};
let catchup_err = if can_catch_up {
slot.send_buf.clear();
encode_stream_start(
h.last_sequence,
genesis_entry,
&mut slot.send_buf,
);
dpdk_publish(&slot.send_buf).and_then(|()| {
match catch_up_from_journal_with::<A::Event>(
journal_path,
h.last_sequence,
&mut dpdk_publish,
shutdown,
)? {
CatchUpResult::Ok(_) => Ok(()),
CatchUpResult::NeedSnapshot => {
Err(io::Error::other(
"catch-up failed unexpectedly after probe",
))
}
}
}).err()
} else {
match snapshot_transfer_with::<A::Event>(
journal_path,
genesis_entry,
&mut dpdk_publish,
shutdown,
) {
Ok(CatchUpResult::Ok(_)) => None,
Ok(CatchUpResult::NeedSnapshot) => {
Some(io::Error::other(
"catch-up failed even after snapshot transfer",
))
}
Err(e) => Some(e),
}
};
if let Some(e) = catchup_err {
warn!(slot = slot_idx, error = %e, "catch-up/snapshot failed — disconnecting");
transport.close(handle);
slot.state = SlotState::Idle;
slot.recv_buf.clear();
metrics.catching_up[slot_idx]
.store(false, Ordering::Relaxed);
replicas_connected.fetch_sub(1, Ordering::Release);
if replicas_connected.load(Ordering::Relaxed) == 0 {
replication_cursor.store(u64::MAX, Ordering::Release);
}
continue;
}
// Set cursor to this replica's acked position.
slot.acked_cursor = h.last_sequence + 1;
slot.last_sequence = h.last_sequence;
slot.last_send = std::time::Instant::now();
// Drain overlapping ring entries from catch-up.
// Ring chunks are wire-ready InputBatch frames;
// forward as-is.
while let Some((meta, data)) = slot.consumer.try_read() {
if meta.end_sequence > h.last_sequence {
slot.send_buf.clear();
slot.send_buf.extend_from_slice(data);
slot.consumer.commit();
transport.queue_send(handle, &slot.send_buf);
slot.send_buf.clear();
slot.last_sequence = meta.end_sequence;
break;
}
slot.consumer.commit();
}
// Seed the per-slot metrics cursors before flipping
// active so a reader that observes active=true also
// observes a non-zero cursor pair. Without this, a
// degraded gate freezes after a replica rejoins —
// see tcp_sender for the full rationale. Relaxed is
// fine because the active_flag Release below
// publishes these stores in program order.
metrics.acked_sequence[slot_idx]
.store(h.last_sequence, Ordering::Relaxed);
metrics.in_memory_sequence[slot_idx]
.store(h.last_sequence, Ordering::Relaxed);
// Mark ring active before signaling readiness
// so the journal stage publishes when seeds flow.
slot.active_flag.store(true, Ordering::Release);
replica_ready.store(true, Ordering::Release);
update_dual_replication_cursor(
slot.acked_cursor,
other_acked,
replication_cursor,
fastest_replica_cursor,
);
metrics.catching_up[slot_idx].store(false, Ordering::Relaxed);
slot.state = SlotState::Streaming(handle);
}
Ok(ReplicaMessage::Ack(_)) => {
warn!(
slot = slot_idx,
"expected Handshake, got Ack — disconnecting"
);
transport.close(handle);
slot.state = SlotState::Idle;
slot.recv_buf.clear();
replicas_connected.fetch_sub(1, Ordering::Release);
if replicas_connected.load(Ordering::Relaxed) == 0 {
replication_cursor.store(u64::MAX, Ordering::Release);
fastest_replica_cursor.store(u64::MAX, Ordering::Release);
}
}
Err(e) => {
warn!(slot = slot_idx, error = %e, "failed to decode handshake — disconnecting");
transport.close(handle);
slot.state = SlotState::Idle;
slot.recv_buf.clear();
replicas_connected.fetch_sub(1, Ordering::Release);
if replicas_connected.load(Ordering::Relaxed) == 0 {
replication_cursor.store(u64::MAX, Ordering::Release);
fastest_replica_cursor.store(u64::MAX, Ordering::Release);
}
}
}
}
FrameResult::Oversized => {
warn!(slot = slot_idx, "oversized handshake frame — disconnecting");
transport.close(handle);
slot.state = SlotState::Idle;
slot.recv_buf.clear();
replicas_connected.fetch_sub(1, Ordering::Release);
if replicas_connected.load(Ordering::Relaxed) == 0 {
replication_cursor.store(u64::MAX, Ordering::Release);
fastest_replica_cursor.store(u64::MAX, Ordering::Release);
}
}
FrameResult::Incomplete => {} // Wait for more data.
}
}
SlotState::Streaming(handle) => {
any_active = true;
// 1. Process acks (non-blocking).
transport.recv_into_vec(handle, &mut slot.recv_buf);
let mut consumed = 0;
let mut ack_error = false;
loop {
let remaining = &slot.recv_buf[consumed..];
match try_extract_frame(remaining, MAX_CONTROL_FRAME) {
FrameResult::Complete(payload_start, frame_end) => {
let payload = &remaining[payload_start..frame_end];
if let Ok(ReplicaMessage::Ack(ack)) =
decode_replica_message(payload)
{
slot.acked_cursor = ack.acked_sequence + 1;
metrics.acked_sequence[slot_idx]
.store(ack.acked_sequence, Ordering::Relaxed);
metrics.in_memory_sequence[slot_idx]
.store(ack.in_memory_sequence, Ordering::Relaxed);
update_dual_replication_cursor(
slot.acked_cursor,
other_acked,
replication_cursor,
fastest_replica_cursor,
);
}
consumed += frame_end;
}
FrameResult::Oversized => {
warn!(
slot = slot_idx,
"oversized ack frame from replica — disconnecting"
);
ack_error = true;
break;
}
FrameResult::Incomplete => break,
}
}
compact_recv_buf(&mut slot.recv_buf, consumed);
if ack_error {
transport.close(handle);
// Zero metrics before active_flag Release — see B2.
metrics.acked_sequence[slot_idx].store(0, Ordering::Relaxed);
metrics.in_memory_sequence[slot_idx].store(0, Ordering::Relaxed);
slot.active_flag.store(false, Ordering::Release);
slot.acked_cursor = u64::MAX;
slot.recv_buf.clear();
slot.state = SlotState::Idle;
replicas_connected.fetch_sub(1, Ordering::Release);
update_dual_replication_cursor(
u64::MAX,
other_acked,
replication_cursor,
fastest_replica_cursor,
);
if replicas_connected.load(Ordering::Relaxed) == 0 {
warn!("all replicas disconnected — trading halted");
}
continue;
}
// 2. Send data batches. Ring chunks are wire-ready
// `InputBatch` frames produced by the journal stage
// — the sender is a passthrough. Pre-check the
// per-socket TX queue: if we commit a batch from
// the ring but `queue_send` rejects it (TX full),
// the data is gone from the ring without ever
// reaching the replica — replica never acks,
// replication_cursor stalls, and the response
// gate freezes the whole exchange. We saw this
// exact symptom on dpdk-dual-repl.
let max_tx = transport.max_tx_queue_size(handle);
let used = transport.tx_queue_bytes(handle);
let mut available = max_tx.saturating_sub(used);
slot.send_buf.clear();
let mut batches_sent = 0;
let mut tx_overflow = false;
while batches_sent < batch_size {
let Some((meta, data)) = slot.consumer.try_read() else {
break;
};
let data_len = data.len();
if data_len > available {
// Batch doesn't fit — flush what we have so
// far, poll to drain the wire, then re-check
// with a clean slate.
if !slot.send_buf.is_empty() {
metrics.bytes_sent[slot_idx]
.fetch_add(slot.send_buf.len() as u64, Ordering::Relaxed);
if !transport.queue_send(handle, &slot.send_buf) {
slot.send_buf.clear();
tx_overflow = true;
break;
}
slot.send_buf.clear();
}
transport.poll();
let used = transport.tx_queue_bytes(handle);
available = max_tx.saturating_sub(used);
if data_len > available {
break;
}
}
slot.send_buf.extend_from_slice(data);
slot.consumer.commit();
slot.last_sequence = meta.end_sequence;
batches_sent += 1;
available = available.saturating_sub(data_len);
}
if !tx_overflow && !slot.send_buf.is_empty() {
metrics.bytes_sent[slot_idx]
.fetch_add(slot.send_buf.len() as u64, Ordering::Relaxed);
if !transport.queue_send(handle, &slot.send_buf) {
tx_overflow = true;
}
}
if tx_overflow {
slot.send_buf.clear();
warn!(
slot = slot_idx,
"TX overflow on replica socket — disconnecting"
);
transport.close(handle);
// Zero metrics before active_flag Release — see B2.
metrics.acked_sequence[slot_idx].store(0, Ordering::Relaxed);
metrics.in_memory_sequence[slot_idx].store(0, Ordering::Relaxed);
slot.active_flag.store(false, Ordering::Release);
slot.acked_cursor = u64::MAX;
slot.recv_buf.clear();
slot.state = SlotState::Idle;
replicas_connected.fetch_sub(1, Ordering::Release);
update_dual_replication_cursor(
u64::MAX,
other_acked,
replication_cursor,
fastest_replica_cursor,
);
if replicas_connected.load(Ordering::Relaxed) == 0 {
warn!("all replicas disconnected — trading halted");
}
continue;
}
if !slot.send_buf.is_empty() {
// Flush immediately so replication data hits the
// wire without waiting for the next outer-loop
// poll. Without this, a client-traffic burst
// starves the replication TX path: the TxQueue
// fills, the driver backs off, the ring overflows,
// and the response gate freezes the exchange.
transport.poll();
slot.last_send = std::time::Instant::now();
}
// 3. Heartbeat if idle.
if batches_sent == 0 && slot.last_send.elapsed() >= heartbeat_interval {
slot.send_buf.clear();
encode_heartbeat(slot.last_sequence, &mut slot.send_buf);
transport.queue_send(handle, &slot.send_buf);
slot.last_send = std::time::Instant::now();
}
// 4. Check for disconnect.
if !transport.is_active(handle) {
warn!(slot = slot_idx, "replica disconnected (DPDK)");
// Zero metrics before active_flag Release — see B2.
metrics.acked_sequence[slot_idx].store(0, Ordering::Relaxed);
metrics.in_memory_sequence[slot_idx].store(0, Ordering::Relaxed);
slot.active_flag.store(false, Ordering::Release);
slot.acked_cursor = u64::MAX;
slot.recv_buf.clear();
slot.state = SlotState::Idle;
replicas_connected.fetch_sub(1, Ordering::Release);
update_dual_replication_cursor(
u64::MAX,
other_acked,
replication_cursor,
fastest_replica_cursor,
);
if replicas_connected.load(Ordering::Relaxed) == 0 {
warn!("all replicas disconnected — trading halted");
}
continue;
}
// Eviction is handled by the journal-stage evict_flag check
// at the top of the loop (lines 3254+). No timeout-based
// eviction here — try_read() returning None means the
// consumer caught up, not that it's slow.
}
}
}
any_active
}
}
/// DPDK variant of the replication receiver. Uses a `DpdkTransport` (smoltcp)
/// to connect to the primary via DPDK instead of kernel TCP.
///
/// Includes reconnection with exponential backoff (1s → 30s) and snapshot
/// transfer support — matching the TCP receiver's feature set.
///
/// The protocol is identical to `run_receiver` — same wire format, same
/// fsync-then-ack-then-replay pattern. Only the I/O primitives differ.
#[allow(clippy::too_many_arguments)]
pub fn run_receiver_dpdk<A, W>(
mut transport: melin_dpdk::DpdkTransport,
primary_ip: std::net::Ipv4Addr,
primary_port: u16,
journal_path: &std::path::Path,
shutdown: &AtomicBool,
promote: &AtomicBool,
snapshot_interval_ms: u64,
snapshot_path: std::path::PathBuf,
cores: crate::server::PipelineCores,
receiver_core: usize,
group_commit_delay: std::time::Duration,
pipeline_depth: usize,
busy_spin: bool,
rotation: Option<(u64, std::sync::Arc<AtomicBool>)>,
// Application factory: see the kernel-TCP `run_receiver` for the
// shape and rationale. Carries operator policy (rate limits, caps,
// ...) alongside the empty-app constructor.
factory: std::sync::Arc<dyn melin_app::app_factory::AppFactory<App = A>>,
) -> ReceiverResult<A, W>
where
A: Application + Send + 'static,
A::Event: Send + Sync + 'static,
A::Report: Send + 'static,
A::QueryResponse: Send + 'static,
W: JournalWrite<A::Event> + Send + 'static,
JournalStage<A::Event, W>: JournalStageRun<A::Event, Writer = W>,
{
// Recover local state from journal (if any). On first call this may
// be (None, None) for a fresh replica. After a reconnect, the pipeline
// shutdown returns the App + writer directly.
let (mut exchange, mut journal_writer, mut last_sequence, mut chain_hash) =
if journal_path.exists() {
let engine = if snapshot_path.exists() {
info!("recovering replica from snapshot + journal (DPDK)");
melin_transport_core::JournaledApp::<A, W>::recover_from_snapshot(
&snapshot_path,
journal_path,
)?
} else {
melin_transport_core::JournaledApp::<A, W>::recover(factory.empty(), journal_path)?
};
let next = engine.next_sequence();
let last = next.saturating_sub(1);
let hash = engine.chain_hash().unwrap_or([0u8; 32]);
let (mut exchange, writer) = engine.into_parts();
factory.apply_operator_policy(&mut exchange);
(Some(exchange), Some(writer), last, hash)
} else {
(None, None, 0u64, [0u8; 32])
};
// Exponential backoff for reconnection: 1s → 2s → 4s → … → 30s max.
// Reset to 1s on successful streaming (first InputBatch received).
let mut backoff = std::time::Duration::from_secs(1);
const MAX_BACKOFF: std::time::Duration = std::time::Duration::from_secs(30);
// Reusable buffers — survive across reconnections.
let mut send_buf = Vec::with_capacity(64);
let mut recv_buf: Vec<u8> = Vec::with_capacity(4096);
// Ephemeral port counter for outbound connections. Each reconnect uses
// a different local port to avoid smoltcp TIME_WAIT conflicts.
let mut local_port: u16 = 40000;
// Live pipeline state — built once on first connect (or after a snapshot
// transfer), persists across `Disconnected` reconnects so we don't pay
// the journal-recover + thread-spawn + warm-up cost on every drop.
// None = no pipeline yet (first iteration, or just torn down for
// snapshot transfer); Some = running pipeline with threads + atomics
// we can read for the next reconnect handshake.
let mut pipeline: Option<ReplicaPipelineHandles<A, W>> = None;
// --- Outer reconnect loop ---
//
// Each iteration: connect → handshake → (snapshot rebuild?) →
// (build pipeline if absent) → stream. On disconnect the pipeline
// stays live — we just refresh handshake state from its atomics and
// reconnect. Only `Promote` / `Shutdown` / snapshot-transfer / fatal
// error tear it down.
loop {
// Refresh handshake state from the running pipeline, if any.
if let Some(p) = pipeline.as_ref() {
last_sequence = p.last_seq.load(Ordering::Acquire);
if let Some(ref lock) = p.chain_hash_lock {
chain_hash = lock.load().chain_hash;
}
}
if shutdown.load(Ordering::Relaxed) {
if let Some(p) = pipeline.take() {
let _ = teardown_replica_pipeline::<A, W>(p);
}
return Ok(None);
}
if promote.load(Ordering::Acquire) {
info!("promotion triggered while disconnected (DPDK)");
if let Some(p) = pipeline.take()
&& let Some((e, w)) = teardown_replica_pipeline::<A, W>(p)
{
exchange = Some(e);
journal_writer = Some(w);
}
return match (exchange, journal_writer) {
(Some(e), Some(w)) => Ok(Some((e, w))),
_ => Err("promotion requested but no local state available".into()),
};
}
info!(
primary_ip = %primary_ip,
primary_port,
"connecting to primary as replica (DPDK)"
);
// Seed the primary's MAC into smoltcp's neighbor cache. Without
// this, smoltcp emits a broadcast ARP on connect which the SR-IOV
// PF silently drops, and the SYN never goes out — the replica
// spins on "failed to connect (DPDK)" forever. VF MACs follow the
// 02:00:<IP-bytes> convention set by dpdk-setup-sriov.sh, matching
// what the bench client does on its outbound connect.
let primary_mac = [
0x02,
0x00,
primary_ip.octets()[0],
primary_ip.octets()[1],
primary_ip.octets()[2],
primary_ip.octets()[3],
];
transport.seed_neighbor(primary_ip, primary_mac);
// Drain the injected ARP reply through smoltcp so the neighbor
// cache is populated BEFORE connect_to() runs. Without this poll
// smoltcp's connect tries to resolve ARP from an empty cache,
// queues a broadcast request that the PF drops, and the SYN
// never ships.
transport.poll();
// Connect to primary via smoltcp.
// Replication streams large journal batches; use bigger RX buffer
// so smoltcp can advertise a window large enough to sustain throughput.
const REPL_RX_BUF: usize = 512 * 1024;
const REPL_TX_BUF: usize = 64 * 1024;
const REPL_TX_QUEUE: usize = 64 * 1024;
let handle = transport.connect_to_with_buffers(
primary_ip,
primary_port,
local_port,
REPL_RX_BUF,
REPL_TX_BUF,
REPL_TX_QUEUE,
);
local_port = local_port.wrapping_add(1).max(40000);
// Poll until TCP handshake completes (with timeout).
let connect_start = std::time::Instant::now();
const CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
let connected = loop {
if shutdown.load(Ordering::Relaxed) {
return Ok(None);
}
transport.poll();
if transport.is_connected(handle) {
break true;
}
if !transport.is_active(handle) || connect_start.elapsed() >= CONNECT_TIMEOUT {
break false;
}
std::thread::yield_now();
};
if !connected {
warn!(
backoff_secs = backoff.as_secs(),
"failed to connect to primary (DPDK) — retrying"
);
transport.close(handle);
sleep_checking_flags(backoff, shutdown, promote);
if shutdown.load(Ordering::Relaxed) {
if let Some(p) = pipeline.take() {
let _ = teardown_replica_pipeline::<A, W>(p);
}
return Ok(None);
}
if promote.load(Ordering::Acquire) {
info!("promotion triggered during reconnect backoff (DPDK)");
if let Some(p) = pipeline.take()
&& let Some((e, w)) = teardown_replica_pipeline::<A, W>(p)
{
exchange = Some(e);
journal_writer = Some(w);
}
return match (exchange, journal_writer) {
(Some(e), Some(w)) => Ok(Some((e, w))),
_ => Err("promotion requested but no local state available".into()),
};
}
backoff = (backoff * 2).min(MAX_BACKOFF);
continue;
}
info!("connected to primary (DPDK)");
// Send handshake.
send_buf.clear();
let handshake = Handshake {
last_sequence,
chain_hash,
};
encode_handshake(&handshake, &mut send_buf);
transport.queue_send(handle, &send_buf);
send_buf.clear();
// Read protocol response (StreamStart / NeedSnapshot / HashMismatch).
// Helper macro: shut the pipeline down before bubbling up a fatal
// error from the handshake. Borrows `pipeline` directly so we don't
// leak the threads on the way out.
macro_rules! fatal_err_dpdk {
($msg:expr) => {{
if let Some(p) = pipeline.take() {
let _ = teardown_replica_pipeline::<A, W>(p);
}
return Err($msg);
}};
}
recv_buf.clear();
let primary_genesis_entry = 'handshake: loop {
if shutdown.load(Ordering::Relaxed) {
if let Some(p) = pipeline.take() {
let _ = teardown_replica_pipeline::<A, W>(p);
}
return Ok(None);
}
transport.poll();
transport.recv_into_vec(handle, &mut recv_buf);
match try_extract_frame(&recv_buf, MAX_CONTROL_FRAME) {
FrameResult::Complete(payload_start, frame_end) => {
let payload = &recv_buf[payload_start..frame_end];
let response = decode_primary_message(payload)?;
compact_recv_buf(&mut recv_buf, frame_end);
match response {
PrimaryMessage::StreamStart {
start_sequence,
genesis_entry,
} => {
info!(start_sequence, "streaming started (DPDK)");
break 'handshake genesis_entry;
}
PrimaryMessage::NeedSnapshot => {
info!("primary requires snapshot transfer — receiving snapshot (DPDK)");
// Tear down the live pipeline before wiping its
// backing journal — the journal stage holds the
// file open and the App lives on the matching
// thread; both must exit cleanly first.
if let Some(p) = pipeline.take() {
let _ = teardown_replica_pipeline::<A, W>(p);
}
// Remove stale local state. Invalidate the in-memory
// App and SectorWriter — their underlying files
// are about to be deleted. Without this, a failed
// snapshot transfer would leave stale state that
// the reconnect loop mistakes for valid.
exchange = None;
journal_writer = None;
let _ = std::fs::remove_file(journal_path);
let _ = std::fs::remove_file(&snapshot_path);
// Receive snapshot via DPDK transport.
match receive_snapshot_dpdk::<A>(
handle,
&mut transport,
&mut recv_buf,
&snapshot_path,
shutdown,
) {
Ok((snap_exchange, snap_seq, snap_hash)) => {
exchange = Some(snap_exchange);
let writer = W::create_continuing(
journal_path,
snap_seq + 1,
snap_hash,
)?;
journal_writer = Some(writer);
last_sequence = snap_seq;
chain_hash = snap_hash;
// After snapshot, expect StreamStart.
continue;
}
Err(e) => {
warn!(error = %e, "snapshot transfer failed (DPDK) — retrying");
transport.close(handle);
sleep_checking_flags(backoff, shutdown, promote);
backoff = (backoff * 2).min(MAX_BACKOFF);
break 'handshake Vec::new(); // will be caught by the empty check below
}
}
}
PrimaryMessage::HashMismatch => {
fatal_err_dpdk!(
"chain hash mismatch — replica has divergent history".into()
);
}
other => {
fatal_err_dpdk!(format!("unexpected response: {other:?}").into());
}
}
}
FrameResult::Oversized => {
fatal_err_dpdk!("oversized frame from primary during handshake".into());
}
FrameResult::Incomplete => {}
}
if !transport.is_active(handle) {
warn!("disconnected from primary during handshake (DPDK)");
transport.close(handle);
sleep_checking_flags(backoff, shutdown, promote);
backoff = (backoff * 2).min(MAX_BACKOFF);
break Vec::new(); // trigger reconnect via empty check
}
std::thread::yield_now();
};
// Empty genesis entry means the handshake loop exited via a
// failure path (disconnect or snapshot error) — reconnect.
if primary_genesis_entry.is_empty() {
continue;
}
// Create journal for fresh replica (first connection only).
//
// Gate on `pipeline.is_none()` rather than `journal_writer.is_none()`:
// the writer is moved into the pipeline on first connect and never
// returned, so on every subsequent reconnect `journal_writer` is
// `None` even though a live writer is still mid-stream inside the
// pipeline. `pipeline.is_none()` distinguishes "true first connect or
// post-snapshot rebuild" from "reconnect against an existing
// pipeline" — the latter must not recreate the journal file.
if pipeline.is_none() && journal_writer.is_none() {
let writer =
melin_journal::create_fresh_replica::<_, W>(journal_path, &primary_genesis_entry)?;
let mut fresh = factory.empty();
factory.apply_operator_policy(&mut fresh);
exchange = Some(fresh);
journal_writer = Some(writer);
}
// --- Build pipeline if absent ---
//
// Built once on first connect, or after a snapshot transfer tore
// the previous one down. On disconnect the pipeline lives, so
// this branch is skipped.
if pipeline.is_none() {
// If we still have no state after all the handshake logic, reconnect.
if exchange.is_none() || journal_writer.is_none() {
continue;
}
let cur_exchange = exchange.take().expect("exchange initialized");
let cur_writer = journal_writer.take().expect("journal_writer initialized");
// Unpin before spawning the pipeline. Same rationale as the
// kernel-TCP receiver: `pin_to_core` sets `SCHED_FIFO` and on
// post-snapshot rebuilds this thread is already pinned —
// children would inherit `{receiver_core}` + FIFO and never
// preempt the busy-spinning receiver to reach their own
// self-pin.
if let Err(e) = melin_app::affinity::clear_affinity() {
tracing::warn!(error = e, "failed to clear receiver affinity before spawn");
}
pipeline = Some(build_replica_pipeline_with_threads::<A, W>(
cur_exchange,
cur_writer,
cores,
snapshot_interval_ms,
snapshot_path.clone(),
group_commit_delay,
busy_spin,
rotation.clone(),
)?);
// Pipeline children are spawned and self-pinned. Now safe to
// pin the receive thread — mirrors the primary's reader pin
// so the thread producing input-ring entries from the network
// isn't migrated across L3s mid-batch.
melin_app::affinity::pin_thread("receiver", receiver_core);
}
// --- Streaming session (transport-agnostic) ---
let result = {
let p = pipeline.as_mut().expect("pipeline must exist by here");
let input_producer = &mut p.input_producer;
let journal_cursor = p.journal_cursor.as_ref();
let mut dpdk_transport = DpdkReceiverTransport {
transport: &mut transport,
handle,
send_buf: std::mem::take(&mut send_buf),
};
let r = streaming_loop::<DpdkReceiverTransport<'_>, A::Event>(
&mut dpdk_transport,
input_producer,
journal_cursor,
shutdown,
promote,
pipeline_depth,
busy_spin,
std::mem::take(&mut recv_buf),
None,
);
send_buf = dpdk_transport.send_buf;
r
};
// Publish sentinel for terminal exits.
if !matches!(result.exit, SessionExit::Disconnected)
&& let Some(p) = pipeline.as_mut()
{
p.input_producer.publish(
melin_transport_core::pipeline::InputSlot::<A::Event>::shutdown_sentinel(),
);
}
match result.exit {
SessionExit::Shutdown => {
if let Some(p) = pipeline.take() {
let _ = teardown_replica_pipeline::<A, W>(p);
}
return Ok(None);
}
SessionExit::Promote => {
return match pipeline.take() {
Some(p) => match teardown_replica_pipeline::<A, W>(p) {
Some((ex, wr)) => Ok(Some((ex, wr))),
None => Err("pipeline thread panicked during promotion (DPDK)".into()),
},
None => Err("pipeline missing on promote (DPDK)".into()),
};
}
SessionExit::Fatal(e) => {
if let Some(p) = pipeline.take() {
let _ = teardown_replica_pipeline::<A, W>(p);
}
return Err(e);
}
SessionExit::Disconnected => {
// Pipeline stays live — `last_sequence` and `chain_hash`
// refresh from its atomics at the top of the next iteration.
}
}
if result.received_data {
backoff = std::time::Duration::from_secs(1);
}
warn!(
last_sequence,
backoff_secs = backoff.as_secs(),
"reconnecting to primary (DPDK)"
);
sleep_checking_flags(backoff, shutdown, promote);
backoff = (backoff * 2).min(MAX_BACKOFF);
}
}
/// Receive a snapshot from the primary via DPDK transport.
/// Expects: SnapshotBegin → SnapshotChunk* → SnapshotEnd.
/// Returns the loaded App, snapshot sequence, and chain hash.
fn receive_snapshot_dpdk<A: Application>(
handle: melin_dpdk::SocketHandle,
transport: &mut melin_dpdk::DpdkTransport,
recv_buf: &mut Vec<u8>,
snapshot_path: &std::path::Path,
shutdown: &AtomicBool,
) -> Result<(A, u64, [u8; 32]), Box<dyn std::error::Error + Send + Sync>> {
// Read SnapshotBegin.
let (snap_len, snap_sequence, snap_chain_hash) = loop {
if shutdown.load(Ordering::Relaxed) {
return Err("shutdown during snapshot transfer".into());
}
transport.poll();
transport.recv_into_vec(handle, recv_buf);
match try_extract_frame(recv_buf, MAX_CONTROL_FRAME) {
FrameResult::Complete(payload_start, frame_end) => {
let payload = &recv_buf[payload_start..frame_end];
let msg = decode_primary_message(payload)?;
compact_recv_buf(recv_buf, frame_end);
match msg {
PrimaryMessage::SnapshotBegin {
snapshot_len,
snap_sequence,
snap_chain_hash,
} => break (snapshot_len, snap_sequence, snap_chain_hash),
other => return Err(format!("expected SnapshotBegin, got {other:?}").into()),
}
}
FrameResult::Oversized => {
return Err("oversized frame during snapshot transfer".into());
}
FrameResult::Incomplete => {}
}
if !transport.is_active(handle) {
return Err("disconnected during snapshot transfer".into());
}
std::thread::yield_now();
};
info!(snap_sequence, snap_len, "receiving snapshot (DPDK)");
// Receive snapshot chunks into a temp file.
let tmp_path = snapshot_path.with_extension("snapshot.tmp");
{
let mut tmp_file = std::fs::File::create(&tmp_path)?;
let mut received: u64 = 0;
let mut running_crc: u32 = 0;
'snap_recv: loop {
if shutdown.load(Ordering::Relaxed) {
let _ = std::fs::remove_file(&tmp_path);
return Err("shutdown during snapshot transfer".into());
}
transport.poll();
transport.recv_into_vec(handle, recv_buf);
// Process all complete frames in the buffer.
let mut consumed = 0;
loop {
let remaining = &recv_buf[consumed..];
match try_extract_frame(remaining, MAX_DATA_FRAME) {
FrameResult::Complete(payload_start, frame_end) => {
let payload = &remaining[payload_start..frame_end];
match decode_primary_message(payload)? {
PrimaryMessage::SnapshotChunk(data) => {
std::io::Write::write_all(&mut tmp_file, &data)?;
received += data.len() as u64;
running_crc = crc32c::crc32c_append(running_crc, &data);
}
PrimaryMessage::SnapshotEnd {
crc32c: expected_crc,
} => {
tmp_file.sync_all()?;
drop(tmp_file);
if received != snap_len {
let _ = std::fs::remove_file(&tmp_path);
return Err(format!(
"snapshot length mismatch: expected {snap_len}, got {received}"
)
.into());
}
if running_crc != expected_crc {
let _ = std::fs::remove_file(&tmp_path);
return Err(format!(
"snapshot CRC mismatch: expected {expected_crc:#x}, got {running_crc:#x}"
)
.into());
}
std::fs::rename(&tmp_path, snapshot_path)?;
info!(
snap_sequence,
received, "snapshot received and verified (DPDK)"
);
consumed += frame_end;
compact_recv_buf(recv_buf, consumed);
break 'snap_recv;
}
other => {
let _ = std::fs::remove_file(&tmp_path);
return Err(
format!("expected SnapshotChunk/End, got {other:?}").into()
);
}
}
consumed += frame_end;
}
FrameResult::Oversized => {
let _ = std::fs::remove_file(&tmp_path);
return Err("oversized frame during snapshot chunk transfer".into());
}
FrameResult::Incomplete => break,
}
}
compact_recv_buf(recv_buf, consumed);
if !transport.is_active(handle) {
let _ = std::fs::remove_file(&tmp_path);
return Err("disconnected during snapshot chunk transfer".into());
}
std::thread::yield_now();
}
} // tmp_file dropped here if not already dropped in SnapshotEnd path
// Load and verify the snapshot.
let (snap_exchange, _snap_seq, snap_hash) =
melin_transport_core::snapshot::load::<A>(snapshot_path)?;
if snap_hash != snap_chain_hash {
return Err(format!(
"snapshot chain hash mismatch: primary sent {snap_chain_hash:02x?}, \
loaded snapshot has {snap_hash:02x?}"
)
.into());
}
Ok((snap_exchange, snap_sequence, snap_chain_hash))
}