Skip to content

Commit 97b8acf

Browse files
authored
perf(runtime): use parking_lot for TCP stream registry (#11065)
Signed-off-by: jthomson04 <jwillthomson19@gmail.com>
1 parent af3c1ee commit 97b8acf

1 file changed

Lines changed: 133 additions & 64 deletions

File tree

  • lib/runtime/src/pipeline/network/tcp

lib/runtime/src/pipeline/network/tcp/server.rs

Lines changed: 133 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ use std::{
99
sync::Arc,
1010
time::Duration,
1111
};
12-
use tokio::sync::Mutex;
1312
use tokio::time::Instant;
1413

1514
/// Tombstone lifetime. Bridges the `register()` → `associate_instance()`
@@ -22,6 +21,7 @@ use bytes::Bytes;
2221
use derive_builder::Builder;
2322
use futures::{SinkExt, StreamExt};
2423
use local_ip_address::{Error, list_afinet_netifas, local_ip, local_ipv6};
24+
use parking_lot::Mutex;
2525

2626
use serde::{Deserialize, Serialize};
2727
use tokio::{
@@ -259,7 +259,7 @@ impl TcpStreamServer {
259259
send_subject: Option<&str>,
260260
id: &EndpointInstanceId,
261261
) -> bool {
262-
let mut state = self.state.lock().await;
262+
let mut state = self.state.lock();
263263
let now = Instant::now();
264264
prune_tombstones(&mut state.removed_instances, now);
265265
if state.removed_instances.contains_key(id) {
@@ -296,7 +296,7 @@ impl TcpStreamServer {
296296
/// Cancel one pending response-stream registration. Drops the
297297
/// `oneshot::Sender` so the waiting receiver resolves with `RecvError`.
298298
pub async fn cancel_recv_stream(&self, subject: &str) {
299-
let mut state = self.state.lock().await;
299+
let mut state = self.state.lock();
300300
state.rx_subjects.remove(subject);
301301
if let Some(key) = state.subject_instance.remove(subject)
302302
&& let Some(subjects) = state.instance_subjects.get_mut(&key)
@@ -314,7 +314,7 @@ impl TcpStreamServer {
314314
/// `(StreamType::Request, _)` tag from `instance_subjects` so the per-
315315
/// instance bookkeeping stays consistent.
316316
pub async fn cancel_send_stream(&self, subject: &str) {
317-
let mut state = self.state.lock().await;
317+
let mut state = self.state.lock();
318318
state.tx_subjects.remove(subject);
319319
if let Some(key) = state.subject_instance.remove(subject)
320320
&& let Some(subjects) = state.instance_subjects.get_mut(&key)
@@ -331,7 +331,7 @@ impl TcpStreamServer {
331331
/// `associate_instance` — and tombstone the id so any racing associate
332332
/// for the same id cancels too. Returns the number of streams cancelled.
333333
pub async fn cancel_instance_streams(&self, id: &EndpointInstanceId) -> usize {
334-
let mut state = self.state.lock().await;
334+
let mut state = self.state.lock();
335335
let now = Instant::now();
336336
prune_tombstones(&mut state.removed_instances, now);
337337
state.removed_instances.insert(id.clone(), now);
@@ -357,25 +357,63 @@ impl TcpStreamServer {
357357
/// Drop the tombstone for an instance that has reappeared in discovery,
358358
/// so future subjects for that identity are tracked normally.
359359
pub async fn clear_instance_tombstone(&self, id: &EndpointInstanceId) {
360-
let mut state = self.state.lock().await;
360+
let mut state = self.state.lock();
361361
state.removed_instances.remove(id);
362362
}
363363

364-
#[allow(clippy::await_holding_lock)]
365364
async fn start(local_ip: String, local_port: u16, state: Arc<Mutex<State>>) -> Result<u16> {
366365
let addr = format!("{}:{}", local_ip, local_port);
367366
let state_clone = state.clone();
368-
let mut guard = state.lock().await;
369-
if guard.handle.is_some() {
370-
panic!("TcpStreamServer already started");
371-
}
372367
let (ready_tx, ready_rx) = tokio::sync::oneshot::channel::<Result<u16>>();
373-
let handle = tokio::spawn(tcp_listener(addr, state_clone, ready_tx));
374-
guard.handle = Some(handle);
375-
drop(guard);
368+
{
369+
let mut guard = state.lock();
370+
if guard.handle.is_some() {
371+
panic!("TcpStreamServer already started");
372+
}
373+
guard.handle = Some(tokio::spawn(tcp_listener(addr, state_clone, ready_tx)));
374+
}
376375
let local_port = ready_rx.await??;
377376
Ok(local_port)
378377
}
378+
379+
fn insert_request_stream(&self, subject: String, connection: RequestedSendConnection) {
380+
self.state.lock().tx_subjects.insert(subject, connection);
381+
}
382+
383+
fn insert_response_stream(&self, subject: String, connection: RequestedRecvConnection) {
384+
self.state.lock().rx_subjects.insert(subject, connection);
385+
}
386+
387+
fn take_request_stream(state: &Mutex<State>, subject: &str) -> Option<RequestedSendConnection> {
388+
let mut state = state.lock();
389+
let connection = state.tx_subjects.remove(subject);
390+
if let Some(key) = state.subject_instance.remove(subject)
391+
&& let Some(subjects) = state.instance_subjects.get_mut(&key)
392+
{
393+
subjects.remove(&(StreamType::Request, subject.to_string()));
394+
if subjects.is_empty() {
395+
state.instance_subjects.remove(&key);
396+
}
397+
}
398+
connection
399+
}
400+
401+
fn take_response_stream(
402+
state: &Mutex<State>,
403+
subject: &str,
404+
) -> Option<RequestedRecvConnection> {
405+
let mut state = state.lock();
406+
let connection = state.rx_subjects.remove(subject);
407+
if let Some(key) = state.subject_instance.remove(subject)
408+
&& let Some(subjects) = state.instance_subjects.get_mut(&key)
409+
{
410+
subjects.remove(&(StreamType::Response, subject.to_string()));
411+
if subjects.is_empty() {
412+
state.instance_subjects.remove(&key);
413+
}
414+
}
415+
connection
416+
}
379417
}
380418

381419
// todo - possible rename ResponseService to ResponseServer
@@ -409,6 +447,7 @@ impl ResponseService for TcpStreamServer {
409447

410448
let send_stream = if options.enable_request_stream {
411449
let sender_subject = uuid::Uuid::new_v4().to_string();
450+
let registry_subject = sender_subject.clone();
412451

413452
let (pending_sender_tx, pending_sender_rx) = oneshot::channel();
414453

@@ -418,11 +457,6 @@ impl ResponseService for TcpStreamServer {
418457
send_buffer_count: options.send_buffer_count,
419458
};
420459

421-
let mut state = self.state.lock().await;
422-
state
423-
.tx_subjects
424-
.insert(sender_subject.clone(), connection_info);
425-
426460
let cleanup_subject = sender_subject.clone();
427461
let cleanup_state = self.state.clone();
428462
let registered_stream = RegisteredStream::new(
@@ -438,7 +472,7 @@ impl ResponseService for TcpStreamServer {
438472
.with_cleanup(move || {
439473
// Drop is sync; fire-and-forget the lock acquisition.
440474
tokio::spawn(async move {
441-
let mut state = cleanup_state.lock().await;
475+
let mut state = cleanup_state.lock();
442476
state.tx_subjects.remove(&cleanup_subject);
443477
if let Some(key) = state.subject_instance.remove(&cleanup_subject)
444478
&& let Some(subjects) = state.instance_subjects.get_mut(&key)
@@ -451,6 +485,8 @@ impl ResponseService for TcpStreamServer {
451485
});
452486
});
453487

488+
self.insert_request_stream(registry_subject, connection_info);
489+
454490
Some(registered_stream)
455491
} else {
456492
None
@@ -459,18 +495,14 @@ impl ResponseService for TcpStreamServer {
459495
let recv_stream = if options.enable_response_stream {
460496
let (pending_recver_tx, pending_recver_rx) = oneshot::channel();
461497
let receiver_subject = uuid::Uuid::new_v4().to_string();
498+
let registry_subject = receiver_subject.clone();
462499

463500
let connection_info = RequestedRecvConnection {
464501
context: options.context.clone(),
465502
connection: pending_recver_tx,
466503
send_buffer_count: options.send_buffer_count,
467504
};
468505

469-
let mut state = self.state.lock().await;
470-
state
471-
.rx_subjects
472-
.insert(receiver_subject.clone(), connection_info);
473-
474506
let cleanup_subject = receiver_subject.clone();
475507
let cleanup_state = self.state.clone();
476508
let registered_stream = RegisteredStream::new(
@@ -486,7 +518,7 @@ impl ResponseService for TcpStreamServer {
486518
.with_cleanup(move || {
487519
// Drop is sync; fire-and-forget the lock acquisition.
488520
tokio::spawn(async move {
489-
let mut state = cleanup_state.lock().await;
521+
let mut state = cleanup_state.lock();
490522
state.rx_subjects.remove(&cleanup_subject);
491523
if let Some(key) = state.subject_instance.remove(&cleanup_subject)
492524
&& let Some(subjects) = state.instance_subjects.get_mut(&key)
@@ -499,6 +531,8 @@ impl ResponseService for TcpStreamServer {
499531
});
500532
});
501533

534+
self.insert_response_stream(registry_subject, connection_info);
535+
502536
Some(registered_stream)
503537
} else {
504538
None
@@ -653,22 +687,12 @@ async fn tcp_listener(
653687
// Request stream is unidirectional; we don't read from the downstream.
654688
drop(reader);
655689

656-
let request_stream = {
657-
let mut guard = state.lock().await;
658-
let conn = guard.tx_subjects.remove(&subject).ok_or(error!(
690+
let request_stream = TcpStreamServer::take_request_stream(&state, &subject).ok_or_else(|| {
691+
error!(
659692
"Subject not found: {}; downstream subscriber specified a subject unknown to the upstream publisher",
660693
subject
661-
))?;
662-
if let Some(key) = guard.subject_instance.remove(&subject)
663-
&& let Some(subjects) = guard.instance_subjects.get_mut(&key)
664-
{
665-
subjects.remove(&(StreamType::Request, subject.clone()));
666-
if subjects.is_empty() {
667-
guard.instance_subjects.remove(&key);
668-
}
669-
}
670-
conn
671-
};
694+
)
695+
})?;
672696

673697
let RequestedSendConnection {
674698
context,
@@ -771,22 +795,9 @@ async fn tcp_listener(
771795
mut reader: FramedRead<tokio::io::ReadHalf<tokio::net::TcpStream>, TwoPartCodec>,
772796
writer: FramedWrite<tokio::io::WriteHalf<tokio::net::TcpStream>, TwoPartCodec>,
773797
) -> Result<()> {
774-
let response_stream = {
775-
let mut guard = state.lock().await;
776-
let conn = guard
777-
.rx_subjects
778-
.remove(&subject)
779-
.ok_or(error!("Subject not found: {}; upstream publisher specified a subject unknown to the downsteam subscriber", subject))?;
780-
if let Some(key) = guard.subject_instance.remove(&subject)
781-
&& let Some(subjects) = guard.instance_subjects.get_mut(&key)
782-
{
783-
subjects.remove(&(StreamType::Response, subject.clone()));
784-
if subjects.is_empty() {
785-
guard.instance_subjects.remove(&key);
786-
}
787-
}
788-
conn
789-
};
798+
let response_stream = TcpStreamServer::take_response_stream(&state, &subject).ok_or_else(|| {
799+
error!("Subject not found: {}; upstream publisher specified a subject unknown to the downsteam subscriber", subject)
800+
})?;
790801

791802
// unwrap response_stream
792803
let RequestedRecvConnection {
@@ -1036,6 +1047,7 @@ mod tests {
10361047
use crate::engine::AsyncEngineContextProvider;
10371048
use crate::pipeline::Context;
10381049
use crate::pipeline::network::DEFAULT_SEND_BUFFER_COUNT;
1050+
use crate::pipeline::network::tcp::client::TcpClient;
10391051
use tokio::io::{AsyncWriteExt, ReadHalf, WriteHalf};
10401052
use tokio::net::TcpStream;
10411053

@@ -1138,7 +1150,7 @@ mod tests {
11381150

11391151
let _pending = server.register(options).await;
11401152

1141-
let state = server.state.lock().await;
1153+
let state = server.state.lock();
11421154
assert_eq!(state.tx_subjects.len(), 1, "one request stream registered");
11431155
assert_eq!(state.rx_subjects.len(), 1, "one response stream registered");
11441156
assert!(
@@ -1439,7 +1451,7 @@ mod tests {
14391451

14401452
// Verify it's in rx_subjects
14411453
{
1442-
let state = server.state.lock().await;
1454+
let state = server.state.lock();
14431455
assert!(state.rx_subjects.contains_key(&subject));
14441456
}
14451457

@@ -1451,7 +1463,7 @@ mod tests {
14511463

14521464
// Verify it's been removed from rx_subjects
14531465
{
1454-
let state = server.state.lock().await;
1466+
let state = server.state.lock();
14551467
assert!(
14561468
!state.rx_subjects.contains_key(&subject),
14571469
"RAII cleanup should have removed the rx_subjects entry"
@@ -1486,7 +1498,7 @@ mod tests {
14861498

14871499
// The entry should still be in rx_subjects (cleanup was disarmed)
14881500
{
1489-
let state = server.state.lock().await;
1501+
let state = server.state.lock();
14901502
assert!(
14911503
state.rx_subjects.contains_key(&subject),
14921504
"into_parts() should disarm the RAII cleanup"
@@ -1643,7 +1655,7 @@ mod tests {
16431655
// Tombstone the identity.
16441656
server.cancel_instance_streams(&id).await;
16451657
{
1646-
let state = server.state.lock().await;
1658+
let state = server.state.lock();
16471659
assert!(state.removed_instances.contains_key(&id));
16481660
}
16491661

@@ -1661,7 +1673,7 @@ mod tests {
16611673
// The expired tombstone must have been pruned (lazy pruning fires on
16621674
// every associate_instance/cancel_instance_streams call).
16631675
{
1664-
let state = server.state.lock().await;
1676+
let state = server.state.lock();
16651677
assert!(
16661678
!state.removed_instances.contains_key(&id),
16671679
"expired tombstone should be pruned, not retained"
@@ -1702,7 +1714,7 @@ mod tests {
17021714
tokio::time::advance(TOMBSTONE_TTL + Duration::from_secs(1)).await;
17031715
server.cancel_instance_streams(&id_new).await;
17041716

1705-
let state = server.state.lock().await;
1717+
let state = server.state.lock();
17061718
assert!(
17071719
!state.removed_instances.contains_key(&id_old),
17081720
"old tombstone should be pruned by the next cancel_instance_streams call"
@@ -1728,7 +1740,7 @@ mod tests {
17281740
server.cancel_instance_streams(&id_a).await;
17291741
server.clear_instance_tombstone(&id_b).await;
17301742

1731-
let state = server.state.lock().await;
1743+
let state = server.state.lock();
17321744
assert!(
17331745
state.removed_instances.contains_key(&id_a),
17341746
"clearing a different identity must not remove id_a's tombstone"
@@ -1957,6 +1969,63 @@ mod tests {
19571969
}
19581970
}
19591971

1972+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1973+
async fn test_concurrent_response_registration_and_call_home() {
1974+
const STREAMS: usize = 128;
1975+
1976+
let result = time::timeout(Duration::from_secs(20), async {
1977+
let server = test_server().await;
1978+
let mut pending_streams = Vec::with_capacity(STREAMS);
1979+
let mut client_tasks = Vec::with_capacity(STREAMS);
1980+
1981+
for idx in 0..STREAMS {
1982+
let context = Context::new(());
1983+
let options = StreamOptions::builder()
1984+
.context(context.context())
1985+
.enable_request_stream(false)
1986+
.enable_response_stream(true)
1987+
.build()
1988+
.unwrap();
1989+
1990+
let pending = server.register(options).await;
1991+
let registered_stream = pending.recv_stream.unwrap();
1992+
let (connection_info, stream_provider) = registered_stream.into_parts();
1993+
let client_context =
1994+
Context::with_id_and_metadata((), context.id().to_string(), Default::default());
1995+
let payload = Bytes::from(format!("payload-{idx}"));
1996+
1997+
pending_streams.push((idx, payload.clone(), stream_provider));
1998+
client_tasks.push(tokio::spawn(async move {
1999+
let mut sender = TcpClient::create_response_stream(
2000+
client_context.context(),
2001+
connection_info,
2002+
None,
2003+
)
2004+
.await
2005+
.unwrap();
2006+
sender.send_prologue(None).await.unwrap();
2007+
sender.send(payload).await.unwrap();
2008+
}));
2009+
}
2010+
2011+
for task in client_tasks {
2012+
task.await.unwrap();
2013+
}
2014+
2015+
for (idx, expected, stream_provider) in pending_streams {
2016+
let mut stream = stream_provider.await.unwrap().unwrap();
2017+
let actual = stream.rx.recv().await.unwrap();
2018+
assert_eq!(actual, expected, "payload mismatch for stream {idx}");
2019+
}
2020+
})
2021+
.await;
2022+
2023+
assert!(
2024+
result.is_ok(),
2025+
"concurrent response registration and call-home timed out"
2026+
);
2027+
}
2028+
19602029
// ==================== request_stream_send_handler integration tests ====================
19612030
//
19622031
// These exercise the closing-message contract of `request_stream_send_handler`

0 commit comments

Comments
 (0)