prepare multi threaded node

This commit is contained in:
Simon Wicky
2026-05-19 11:31:14 +02:00
parent f73f1a5219
commit df566933ba
6 changed files with 69 additions and 95 deletions
Generated
+1
View File
@@ -7490,6 +7490,7 @@ name = "nym-lp-data"
version = "1.21.0"
dependencies = [
"bytes",
"dashmap",
"num_enum",
"nym-common",
"nym-lp",
+1
View File
@@ -14,6 +14,7 @@ documentation.workspace = true
[dependencies]
bytes.workspace = true
dashmap.workspace = true
num_enum.workspace = true
tracing.workspace = true
thiserror.workspace = true
@@ -1,12 +1,17 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use tracing::{debug, trace, warn};
use crate::fragmentation::fragment::{Fragment, FragmentHashKey, FragmentMetadata};
use std::collections::HashMap;
use dashmap::DashMap;
use dashmap::mapref::entry::Entry;
use std::fmt::Debug;
use std::ops::Add;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tracing::{debug, trace, warn};
pub const DEFAULT_FRAGMENT_TIMEOUT_DURATION: Duration = Duration::from_secs(30);
/// Per-message buffer that collects every `Fragment` of a fragmented message
/// and reassembles the original payload once they are all in.
@@ -129,7 +134,7 @@ where
/// In-flight messages keyed on `(id, frame_kind)`. The frame kind is
/// part of the key so that a random-id collision between two unrelated
/// kinds cannot accidentally route fragments into the same buffer.
in_flight_messages: HashMap<FragmentHashKey, MessageBuffer<Ts>>,
in_flight_messages: Arc<DashMap<FragmentHashKey, MessageBuffer<Ts>>>,
/// How long an incomplete message is allowed to sit before it is
/// dropped on the next `cleanup_stale_buffers` pass.
@@ -149,34 +154,13 @@ where
}
}
/// Whether the message at `key` is present and has all of its fragments.
fn is_message_fully_received(&self, key: FragmentHashKey) -> bool {
self.in_flight_messages
.get(&key)
.map(|buf| buf.is_complete)
.unwrap_or(false)
}
/// Pop the buffer at `key` and reassemble its message bytes. The caller
/// must ensure `is_message_fully_received(key)` holds.
fn reconstruct_message(&mut self, key: FragmentHashKey) -> Vec<u8> {
debug_assert!(self.is_message_fully_received(key));
// SAFETY: the precondition above guarantees the entry exists, so
// `remove` returns `Some`. `is_message_fully_received` also implies
// the buffer's `is_complete` flag is set, which is the precondition
// `into_message` relies on.
#[allow(clippy::unwrap_used)]
self.in_flight_messages.remove(&key).unwrap().into_message()
}
/// Insert `fragment` into the buffer for its message and, if it was the
/// last outstanding fragment, return the reassembled message bytes
/// alongside the original metadata.
///
/// Stale incomplete messages are evicted on every call.
pub fn insert_new_fragment(
&mut self,
&self,
fragment: Fragment,
timestamp: Ts,
) -> Option<(Vec<u8>, FragmentMetadata)> {
@@ -188,24 +172,34 @@ where
// from any of the buffer's slots would be equivalent.
let metadata = (fragment.frame_kind(), fragment.kind_metadata()).into();
let buf = self
.in_flight_messages
.entry(key)
.or_insert_with(|| MessageBuffer::new(total_fragments, timestamp.clone()));
buf.insert_fragment(fragment, timestamp.clone());
let maybe_message = if self.is_message_fully_received(key) {
Some((self.reconstruct_message(key), metadata))
} else {
None
let maybe_message = match self.in_flight_messages.entry(key) {
Entry::Occupied(mut entry) => {
entry.get_mut().insert_fragment(fragment, timestamp.clone());
entry
.get()
.is_complete
.then(|| (entry.remove().into_message(), metadata))
}
Entry::Vacant(entry) => {
let mut buf = MessageBuffer::new(total_fragments, timestamp.clone());
buf.insert_fragment(fragment, timestamp.clone());
if buf.is_complete {
Some((buf.into_message(), metadata))
} else {
entry.insert(buf);
None
}
}
};
// This might be a bit slow, keep an eye on it
self.cleanup_stale_buffers(timestamp.clone());
maybe_message
}
/// Drop incomplete messages whose `last_fragment_timestamp` is older
/// than `incomplete_message_timeout` ago.
pub fn cleanup_stale_buffers(&mut self, timestamp: Ts) {
pub fn cleanup_stale_buffers(&self, timestamp: Ts) {
trace!("Cleaning up stale buffers");
self.in_flight_messages.retain(|_, buf| {
let keep = buf.last_fragment_timestamp.clone()
@@ -224,6 +218,12 @@ where
}
}
impl Default for MessageReconstructor<Instant, Duration> {
fn default() -> Self {
MessageReconstructor::new(DEFAULT_FRAGMENT_TIMEOUT_DURATION)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
@@ -335,7 +335,7 @@ mod tests {
let mut fragments = split(message, SPHINX, 64);
assert_eq!(fragments.len(), 1);
let mut rec = MessageReconstructor::<u64, u64>::new(60);
let rec = MessageReconstructor::<u64, u64>::new(60);
let out = rec.insert_new_fragment(fragments.pop().unwrap(), 0);
let (bytes, metadata) = out.expect("single fragment must complete the message");
assert_eq!(bytes, message);
@@ -348,7 +348,7 @@ mod tests {
let fragments = split(&message, SPHINX, 16);
assert!(fragments.len() > 1);
let mut rec = MessageReconstructor::<u64, u64>::new(60);
let rec = MessageReconstructor::<u64, u64>::new(60);
let total = fragments.len();
let mut completed = None;
for (i, f) in fragments.into_iter().enumerate() {
@@ -371,7 +371,7 @@ mod tests {
// Reverse arrival order.
fragments.reverse();
let mut rec = MessageReconstructor::<u64, u64>::new(60);
let rec = MessageReconstructor::<u64, u64>::new(60);
let mut last = None;
for (i, f) in fragments.into_iter().enumerate() {
last = rec.insert_new_fragment(f, i as u64);
@@ -384,7 +384,7 @@ mod tests {
fn reconstructor_propagates_frame_kind() {
let message = b"outfox bytes";
let mut fragments = split(message, OUTFOX, 64);
let mut rec = MessageReconstructor::<u64, u64>::new(60);
let rec = MessageReconstructor::<u64, u64>::new(60);
let (_, metadata) = rec
.insert_new_fragment(fragments.pop().unwrap(), 0)
.expect("complete");
@@ -403,7 +403,7 @@ mod tests {
make_fragment(2, 2, 1, SPHINX, vec![0xb2]),
];
let mut rec = MessageReconstructor::<u64, u64>::new(60);
let rec = MessageReconstructor::<u64, u64>::new(60);
// Interleave.
assert!(rec.insert_new_fragment(a.remove(0), 0).is_none());
assert!(rec.insert_new_fragment(b.remove(0), 1).is_none());
@@ -422,7 +422,7 @@ mod tests {
let o1 = make_fragment(42, 2, 0, OUTFOX, vec![0x20]);
let o2 = make_fragment(42, 2, 1, OUTFOX, vec![0x21]);
let mut rec = MessageReconstructor::<u64, u64>::new(60);
let rec = MessageReconstructor::<u64, u64>::new(60);
assert!(rec.insert_new_fragment(s1, 0).is_none());
assert!(rec.insert_new_fragment(o1, 1).is_none());
let (s_msg, s_metadata) = rec.insert_new_fragment(s2, 2).unwrap();
@@ -437,7 +437,7 @@ mod tests {
#[test]
fn reconstructor_clears_buffer_after_emitting_message() {
let f = make_fragment(99, 1, 0, SPHINX, vec![0xff]);
let mut rec = MessageReconstructor::<u64, u64>::new(60);
let rec = MessageReconstructor::<u64, u64>::new(60);
let _ = rec.insert_new_fragment(f, 0).unwrap();
assert!(
rec.in_flight_messages.is_empty(),
@@ -450,7 +450,7 @@ mod tests {
#[test]
fn cleanup_evicts_buffers_older_than_timeout() {
let f = make_fragment(1, 2, 0, SPHINX, vec![0]);
let mut rec = MessageReconstructor::<u64, u64>::new(10);
let rec = MessageReconstructor::<u64, u64>::new(10);
// First (and only) fragment received at t=0; the message stays
// incomplete.
assert!(rec.insert_new_fragment(f, 0).is_none());
@@ -473,7 +473,7 @@ mod tests {
let stale = make_fragment(1, 2, 0, SPHINX, vec![0]);
let fresh = make_fragment(2, 1, 0, SPHINX, vec![0xff]);
let mut rec = MessageReconstructor::<u64, u64>::new(10);
let rec = MessageReconstructor::<u64, u64>::new(10);
assert!(rec.insert_new_fragment(stale, 0).is_none());
assert_eq!(rec.in_flight_messages.len(), 1);
@@ -489,7 +489,7 @@ mod tests {
// A buffer that keeps receiving fragments must not be evicted
// even if the absolute time exceeds the timeout, as long as the
// gap between fragments stays under it.
let mut rec = MessageReconstructor::<u64, u64>::new(10);
let rec = MessageReconstructor::<u64, u64>::new(10);
assert!(
rec.insert_new_fragment(make_fragment(1, 3, 0, SPHINX, vec![0xa]), 0)
+8 -10
View File
@@ -15,14 +15,14 @@
//! ```
//!
use crate::node::lp::data::handler::pipeline::{MixnodeDataPipeline, MixnodeDataPipelineConfig};
use crate::node::lp::data::handler::pipeline::MixnodeDataPipeline;
use crate::node::lp::data::shared::SharedLpDataState;
use nym_lp_data::AddressedTimedData;
use nym_lp_data::mixnodes::traits::MixnodeProcessingPipeline;
use nym_lp_data::packet::EncryptedLpPacket;
use nym_metrics::inc;
use rand::rngs::OsRng;
use std::sync::mpsc;
use std::sync::{Arc, mpsc};
use std::time::Instant;
use std::{net::SocketAddr, time::Duration};
use tokio::sync::mpsc::error::TrySendError;
@@ -35,10 +35,12 @@ mod pipeline;
mod processing;
const PIPELINE_TICKING_DURATION: Duration = Duration::from_millis(1);
const FRAGMENT_TIMEOUT_DURATION: Duration = Duration::from_secs(30);
/// LP Data Handler for UDP data plane, act as a pipeline driver and buffer for delaying packet
pub struct LpDataHandler {
/// Shared state
_shared_state: Arc<SharedLpDataState>,
/// Channel to receive incoming data
input_rx: mpsc::Receiver<EncryptedLpPacket>,
@@ -60,16 +62,12 @@ impl LpDataHandler {
output_tx: tokio::sync::mpsc::Sender<(EncryptedLpPacket, SocketAddr)>,
shutdown: nym_task::ShutdownToken,
) -> Self {
let shared_state = Arc::new(state);
Self {
_shared_state: shared_state.clone(),
input_rx,
output_tx,
pipeline: MixnodeDataPipeline::new(
state,
MixnodeDataPipelineConfig {
fragment_timeout: FRAGMENT_TIMEOUT_DURATION,
},
OsRng,
),
pipeline: MixnodeDataPipeline::new(shared_state, OsRng),
outgoing_pkt_buffer: Vec::new(),
shutdown,
}
+7 -21
View File
@@ -1,10 +1,7 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::{
net::SocketAddr,
time::{Duration, Instant},
};
use std::{net::SocketAddr, sync::Arc, time::Instant};
use nym_lp_data::{
AddressedTimedData, PipelinePayload, TimedData, TimedPayload,
@@ -12,7 +9,7 @@ use nym_lp_data::{
Framing, FramingUnwrap, Transport, TransportUnwrap, WireUnwrappingPipeline,
WireWrappingPipeline,
},
fragmentation::{fragment::fragment_payload, reconstruction::MessageReconstructor},
fragmentation::fragment::fragment_payload,
mixnodes::traits::MixnodeProcessingPipeline,
packet::{
EncryptedLpPacket, LpFrame, LpHeader, LpPacket, MalformedLpPacketError,
@@ -27,19 +24,12 @@ use crate::node::lp::data::{
shared::SharedLpDataState,
};
#[derive(Clone, Copy, Debug)]
pub struct MixnodeDataPipelineConfig {
pub fragment_timeout: Duration,
}
pub struct MixnodeDataPipeline<R>
where
R: Rng,
{
config: MixnodeDataPipelineConfig,
/// Shared data state
state: SharedLpDataState,
fragment_reconstructor: MessageReconstructor<Instant, Duration>,
state: Arc<SharedLpDataState>,
rng: R,
}
@@ -47,13 +37,8 @@ impl<R> MixnodeDataPipeline<R>
where
R: Rng,
{
pub fn new(state: SharedLpDataState, config: MixnodeDataPipelineConfig, rng: R) -> Self {
Self {
state,
config,
rng,
fragment_reconstructor: MessageReconstructor::new(config.fragment_timeout),
}
pub fn new(state: Arc<SharedLpDataState>, rng: R) -> Self {
Self { state, rng }
}
}
@@ -190,7 +175,8 @@ where
.inspect_err(|e| tracing::error!("Failed to recover a fragment : {e}"))
.ok()?;
let (payload, metadata) = self
.fragment_reconstructor
.state
.message_reconstructor
.insert_new_fragment(fragment, frame.timestamp)?;
let message_kind = metadata
.try_into()
+5 -17
View File
@@ -6,6 +6,7 @@ use crate::config::LpConfig;
use crate::node::key_rotation::active_keys::ActiveSphinxKeys;
use crate::node::key_rotation::active_keys::SphinxKeyGuard;
use crate::node::replay_protection::bloomfilter::ReplayProtectionBloomfilters;
use nym_lp_data::fragmentation::reconstruction::MessageReconstructor;
use nym_node_metrics::NymNodeMetrics;
use nym_node_metrics::mixnet::PacketKind;
use nym_sphinx_framing::processing::{
@@ -15,35 +16,19 @@ use nym_sphinx_params::SphinxKeyRotation;
use nym_task::ShutdownToken;
use std::net::IpAddr;
use std::time::Duration;
use std::time::Instant;
use tracing::Span;
use tracing::warn;
#[derive(Clone, Copy)]
pub(crate) struct ProcessingConfig {
pub(crate) maximum_packet_delay: Duration,
// /// how long the task is willing to skip mutex acquisition before it will block the thread
// /// until it actually obtains it
// pub(crate) maximum_replay_detection_deferral: Duration,
// /// how many packets the task is willing to queue before it will block the thread
// /// until it obtains the mutex
// pub(crate) maximum_replay_detection_pending_packets: usize,
}
impl ProcessingConfig {
pub(crate) fn new(config: &Config) -> Self {
ProcessingConfig {
maximum_packet_delay: config.mixnet.debug.maximum_forward_packet_delay,
// maximum_replay_detection_deferral: config
// .mixnet
// .replay_protection
// .debug
// .maximum_replay_detection_deferral,
// maximum_replay_detection_pending_packets: config
// .mixnet
// .replay_protection
// .debug
// .maximum_replay_detection_pending_packets,
}
}
}
@@ -60,6 +45,8 @@ pub(crate) struct SharedLpDataState {
pub replay_protection_filter: ReplayProtectionBloomfilters,
pub message_reconstructor: MessageReconstructor<Instant, Duration>,
/// Metrics collection
pub metrics: NymNodeMetrics,
@@ -86,6 +73,7 @@ impl SharedLpDataState {
lp_config: config.lp,
sphinx_keys,
replay_protection_filter,
message_reconstructor: Default::default(),
metrics,
shutdown_token,
}