Skip to content

Commit ed6e1a4

Browse files
committed
Give the reader an external kill signal
1 parent 706a898 commit ed6e1a4

3 files changed

Lines changed: 89 additions & 30 deletions

File tree

crates/hotfix/src/transport/reader.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,19 @@ use tracing::warn;
66
pub struct ReaderMessage;
77

88
pub struct ReaderRef {
9-
disconnect_signal: oneshot::Receiver<()>,
9+
pub(crate) disconnect_signal: oneshot::Receiver<()>,
10+
// Consumed by the watchdog in FixConnection::run_until_disconnect (follow-up commit).
11+
// Kept wired now so the reader's select! already accepts an external kill signal.
12+
#[allow(dead_code)]
13+
pub(crate) kill: oneshot::Sender<()>,
1014
}
1115

1216
impl ReaderRef {
13-
pub fn new(disconnect_signal: oneshot::Receiver<()>) -> Self {
14-
Self { disconnect_signal }
17+
pub fn new(disconnect_signal: oneshot::Receiver<()>, kill: oneshot::Sender<()>) -> Self {
18+
Self {
19+
disconnect_signal,
20+
kill,
21+
}
1522
}
1623

1724
pub async fn wait_for_disconnect(self) {

crates/hotfix/src/transport/socket/socket_reader.rs

Lines changed: 77 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ pub fn spawn_socket_reader(
1212
session_ref: InternalSessionRef<impl OutboundMessage>,
1313
) -> ReaderRef {
1414
let (dc_sender, dc_receiver) = oneshot::channel();
15+
let (kill_sender, kill_receiver) = oneshot::channel();
1516
let actor = ReaderActor::new(reader, session_ref, dc_sender);
16-
tokio::spawn(run_reader(actor));
17+
tokio::spawn(run_reader(actor, kill_receiver));
1718

18-
ReaderRef::new(dc_receiver)
19+
ReaderRef::new(dc_receiver, kill_sender)
1920
}
2021

2122
struct ReaderActor<M, R> {
@@ -38,40 +39,52 @@ impl<M, R: AsyncRead> ReaderActor<M, R> {
3839
}
3940
}
4041

41-
async fn run_reader<Outbound, R>(mut actor: ReaderActor<Outbound, R>)
42-
where
42+
async fn run_reader<Outbound, R>(
43+
mut actor: ReaderActor<Outbound, R>,
44+
mut kill_rx: oneshot::Receiver<()>,
45+
) where
4346
Outbound: OutboundMessage,
4447
R: AsyncRead,
4548
{
4649
let mut parser = Parser::default();
4750
loop {
4851
let mut buf = vec![];
4952

50-
match actor.reader.read_buf(&mut buf).await {
51-
Ok(0) => {
52-
let _ = actor
53-
.session_ref
54-
.disconnect("received EOF".to_string())
55-
.await;
56-
break;
57-
}
58-
Err(err) => {
59-
let _ = actor.session_ref.disconnect(err.to_string()).await;
60-
break;
61-
}
62-
Ok(_) => {
63-
let messages = parser.parse(&buf);
64-
65-
for msg in messages {
66-
if actor
53+
tokio::select! {
54+
result = actor.reader.read_buf(&mut buf) => match result {
55+
Ok(0) => {
56+
let _ = actor
6757
.session_ref
68-
.new_fix_message_received(msg)
69-
.await
70-
.is_err()
71-
{
72-
debug!("reader received message but session has been terminated");
58+
.disconnect("received EOF".to_string())
59+
.await;
60+
break;
61+
}
62+
Err(err) => {
63+
let _ = actor.session_ref.disconnect(err.to_string()).await;
64+
break;
65+
}
66+
Ok(_) => {
67+
let messages = parser.parse(&buf);
68+
69+
for msg in messages {
70+
if actor
71+
.session_ref
72+
.new_fix_message_received(msg)
73+
.await
74+
.is_err()
75+
{
76+
debug!("reader received message but session has been terminated");
77+
}
7378
}
7479
}
80+
},
81+
res = &mut kill_rx => {
82+
let reason = match res {
83+
Ok(()) => "forced close by watchdog",
84+
Err(_) => "reader handle dropped",
85+
};
86+
let _ = actor.session_ref.disconnect(reason.to_string()).await;
87+
break;
7588
}
7689
}
7790
}
@@ -228,4 +241,42 @@ mod tests {
228241
// wait for disconnect signal
229242
let _ = reader_ref.wait_for_disconnect().await;
230243
}
244+
245+
/// Kill signal terminates the reader even when the peer is silent, and
246+
/// the session observes the watchdog-sourced disconnect reason.
247+
#[tokio::test]
248+
async fn kill_signal_terminates_reader() {
249+
let (_writer, reader) = duplex(1024);
250+
let (reader_half, _writer_half) = tokio::io::split(reader);
251+
252+
let (session_ref, mut event_receiver) = create_test_session_ref();
253+
let reader_ref = spawn_socket_reader(reader_half, session_ref);
254+
255+
// Destructure so we can both fire the kill and later await the disconnect signal.
256+
let ReaderRef {
257+
disconnect_signal,
258+
kill,
259+
} = reader_ref;
260+
261+
kill.send(()).expect("kill receiver dropped");
262+
263+
// Reader should publish the watchdog reason to the session.
264+
match tokio::time::timeout(
265+
tokio::time::Duration::from_millis(100),
266+
event_receiver.recv(),
267+
)
268+
.await
269+
{
270+
Ok(Some(SessionEvent::Disconnected(reason))) => {
271+
assert_eq!(reason, "forced close by watchdog");
272+
}
273+
other => panic!("expected Disconnected(\"forced close by watchdog\"), got {other:?}"),
274+
}
275+
276+
// And the disconnect signal should fire shortly after.
277+
tokio::time::timeout(tokio::time::Duration::from_millis(100), disconnect_signal)
278+
.await
279+
.expect("disconnect signal not fired within timeout")
280+
.expect("disconnect sender dropped without signalling");
281+
}
231282
}

crates/hotfix/tests/session_test_cases/common/fakes/fake_counterparty.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ where
250250

251251
fn create_reader() -> (ReaderRef, oneshot::Sender<()>) {
252252
let (dc_sender, dc_receiver) = oneshot::channel();
253-
(ReaderRef::new(dc_receiver), dc_sender)
253+
let (kill_sender, _kill_receiver) = oneshot::channel();
254+
(ReaderRef::new(dc_receiver, kill_sender), dc_sender)
254255
}
255256
}

0 commit comments

Comments
 (0)