@@ -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.
5053pub ( 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
9599const 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