Skip to content

Commit 1785a95

Browse files
committed
coordinate number of flows between generator and blackhole
1 parent 1046f0d commit 1785a95

3 files changed

Lines changed: 60 additions & 23 deletions

File tree

lading/src/blackhole/tcp_rr.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,14 @@ pub struct Config {
6161
#[serde(default = "default_control_port")]
6262
pub control_port: u16,
6363
/// Number of OS server threads. Default 1. When > 1, uses `SO_REUSEPORT`
64-
/// with an eBPF program for load balancing
64+
/// with an eBPF program for load balancing.
6565
#[serde(default = "default_nonzero_u16")]
6666
pub threads: NonZeroU16,
67+
/// Total number of TCP flows the generator should open.
68+
/// Default 1. Sent to the generator over the control connection at
69+
/// startup; the generator does not configure this independently.
70+
#[serde(default = "default_nonzero_u16")]
71+
pub flows: NonZeroU16,
6772
/// Bytes to read per request. Default 1.
6873
#[serde(default = "default_nonzero_usize")]
6974
pub request_size: NonZeroUsize,
@@ -123,6 +128,7 @@ impl TcpRr {
123128
data_addr: SocketAddr::new(self.config.addr, self.config.data_port),
124129
control_addr: SocketAddr::new(self.config.addr, self.config.control_port),
125130
threads: self.config.threads.get(),
131+
flows: self.config.flows.get(),
126132
request_size: self.config.request_size.get(),
127133
response_size: self.config.response_size.get(),
128134
no_delay: self.config.no_delay,

lading/src/generator/tcp_rr.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ fn default_data_port() -> u16 {
4848
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)]
4949
#[serde(deny_unknown_fields)]
5050
/// Configuration for the `tcp_rr` generator.
51+
///
52+
/// Flow count is *not* configured here — it is owned by the
53+
/// `tcp_rr` blackhole and communicated to the generator over the control port
54+
/// during startup.
5155
pub struct Config {
5256
/// The IP address of the `tcp_rr` server.
5357
pub addr: String,
@@ -60,9 +64,6 @@ pub struct Config {
6064
/// Number of OS threads (neper -T). Default 1.
6165
#[serde(default = "default_nonzero_u16")]
6266
pub threads: NonZeroU16,
63-
/// Total number of TCP flows/connections (neper -F). Default 1.
64-
#[serde(default = "default_nonzero_u16")]
65-
pub flows: NonZeroU16,
6667
/// Bytes per request. Default 1.
6768
#[serde(default = "default_nonzero_usize")]
6869
pub request_size: NonZeroUsize,
@@ -117,7 +118,6 @@ impl TcpRr {
117118
data_addr: SocketAddr::new(ip, self.config.data_port),
118119
control_addr: SocketAddr::new(ip, self.config.control_port),
119120
threads: self.config.threads.get(),
120-
flows: self.config.flows.get(),
121121
request_size: self.config.request_size.get(),
122122
response_size: self.config.response_size.get(),
123123
no_delay: self.config.no_delay,

lading/src/neper/rr.rs

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,16 @@ pub enum Error {
4747
}
4848

4949
/// Parameters for [`run_client`].
50+
///
51+
/// Flow count is *not* a client parameter — it is owned by the server and
52+
/// communicated to the client over the control connection during startup.
5053
pub(crate) struct ClientParams {
5154
/// Address of the server's data port.
5255
pub(crate) data_addr: SocketAddr,
5356
/// Address of the server's control port.
5457
pub(crate) control_addr: SocketAddr,
5558
/// Number of OS threads.
5659
pub(crate) threads: u16,
57-
/// Total number of TCP flows.
58-
pub(crate) flows: u16,
5960
/// Bytes per request.
6061
pub(crate) request_size: usize,
6162
/// Bytes per response.
@@ -72,6 +73,9 @@ pub(crate) struct ServerParams {
7273
pub(crate) control_addr: SocketAddr,
7374
/// Number of OS server threads.
7475
pub(crate) threads: u16,
76+
/// Total number of TCP flows the client should open. Sent to the client
77+
/// over the control connection during startup.
78+
pub(crate) flows: u16,
7579
/// Bytes to read per request.
7680
pub(crate) request_size: usize,
7781
/// Bytes to send per response.
@@ -94,6 +98,12 @@ enum ServerState {
9498

9599
const LISTENER_TOKEN: Token = Token(0);
96100

101+
/// Control-channel handshake: server writes `flows` to the accepted control
102+
/// connection as a 2-byte big-endian `u16` and closes; client reads the same
103+
/// 2 bytes after connecting. Internal protocol — no magic / version byte.
104+
const HANDSHAKE_LEN: usize = 2;
105+
const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(5);
106+
97107
/// Run the neper-style client (generator side).
98108
///
99109
/// Connects `flows` TCP flows distributed across `threads` OS threads, then
@@ -112,16 +122,10 @@ pub(crate) async fn run_client(
112122
shutdown: lading_signal::Watcher,
113123
thread_prefix: &'static str,
114124
) -> Result<(), Error> {
115-
if params.threads > params.flows {
116-
return Err(Error::Config(format!(
117-
"threads ({}) must be <= flows ({})",
118-
params.threads, params.flows
119-
)));
120-
}
121-
122125
let shutdown_flag = thread::new_shutdown_flag();
123126

124-
// Wait for the blackhole to be ready by connecting to its control port.
127+
// Wait for the blackhole to be ready by connecting to its control port,
128+
// then read the flow count over that connection.
125129
info!(
126130
"waiting for blackhole control port at {}",
127131
params.control_addr
@@ -135,7 +139,7 @@ pub(crate) async fn run_client(
135139
flag.store(true, Relaxed);
136140
});
137141
}
138-
loop {
142+
let flows: u16 = loop {
139143
if shutdown_flag.load(Relaxed) {
140144
return Err(Error::Io(io::Error::new(
141145
ErrorKind::ConnectionRefused,
@@ -146,9 +150,14 @@ pub(crate) async fn run_client(
146150
)));
147151
}
148152
match net::TcpStream::connect(params.control_addr) {
149-
Ok(_conn) => {
150-
info!("blackhole ready, starting flows");
151-
break;
153+
Ok(mut conn) => {
154+
conn.set_read_timeout(Some(HANDSHAKE_TIMEOUT))
155+
.expect("set_read_timeout on connected TcpStream must succeed");
156+
let mut buf = [0u8; HANDSHAKE_LEN];
157+
conn.read_exact(&mut buf)?;
158+
let received = u16::from_be_bytes(buf);
159+
info!("blackhole ready, {received} flows to open");
160+
break received;
152161
}
153162
Err(e) => {
154163
if Instant::now() >= deadline {
@@ -163,9 +172,16 @@ pub(crate) async fn run_client(
163172
std::thread::sleep(Duration::from_millis(100));
164173
}
165174
}
175+
};
176+
177+
if params.threads > flows {
178+
return Err(Error::Config(format!(
179+
"threads ({}) must be <= flows received from blackhole ({flows})",
180+
params.threads
181+
)));
166182
}
167183

168-
let flow_dist = thread::distribute_flows(params.flows, params.threads);
184+
let flow_dist = thread::distribute_flows(flows, params.threads);
169185

170186
let thread_metrics = Arc::new(
171187
(0..params.threads)
@@ -389,6 +405,7 @@ pub(crate) async fn run_server(
389405

390406
let mut handles = Vec::with_capacity(num_threads as usize);
391407
let mut thread0_listener = thread0_listener;
408+
let flows = params.flows;
392409
for i in 0..num_threads {
393410
let request_size = params.request_size;
394411
let response_size = params.response_size;
@@ -409,6 +426,7 @@ pub(crate) async fn run_server(
409426
binding_addr,
410427
prebuilt,
411428
backlog,
429+
flows,
412430
request_size,
413431
response_size,
414432
no_delay,
@@ -453,14 +471,24 @@ pub(crate) async fn run_server(
453471
flag.store(true, Relaxed);
454472
});
455473
let mut generator_connected = false;
474+
let flows_bytes = params.flows.to_be_bytes();
456475
loop {
457476
if shutdown_flag.load(Relaxed) {
458477
info!("shutdown before generator connected");
459478
break;
460479
}
461480
match control_listener.accept() {
462-
Ok((_conn, peer)) => {
463-
info!("generator connected from {peer}, data threads running");
481+
Ok((mut conn, peer)) => {
482+
// accept(2) on Linux returns a blocking socket regardless of
483+
// the listener's O_NONBLOCK; a small write_timeout guards
484+
// against a generator that connects but never reads.
485+
conn.set_write_timeout(Some(HANDSHAKE_TIMEOUT))
486+
.expect("set_write_timeout on accepted TcpStream must succeed");
487+
conn.write_all(&flows_bytes)?;
488+
info!(
489+
"generator connected from {peer}, sent flows={}, data threads running",
490+
params.flows
491+
);
464492
generator_connected = true;
465493
break;
466494
}
@@ -547,6 +575,7 @@ fn server_thread_main(
547575
binding_addr: SocketAddr,
548576
prebuilt_listener: Option<net::TcpListener>,
549577
backlog: i32,
578+
num_flows: u16,
550579
request_size: usize,
551580
response_size: usize,
552581
no_delay: bool,
@@ -566,7 +595,9 @@ fn server_thread_main(
566595

567596
let mut listener = TcpListener::from_std(std_listener);
568597
let mut poll = Poll::new().expect("failed to create mio::Poll");
569-
let mut events = Events::with_capacity(256);
598+
// Worst case under SO_REUSEPORT: every flow lands on this thread, so size
599+
// for the total flow count plus the listener token.
600+
let mut events = Events::with_capacity(num_flows as usize + 1);
570601

571602
poll.registry()
572603
.register(&mut listener, LISTENER_TOKEN, Interest::READABLE)

0 commit comments

Comments
 (0)