Skip to content

Commit da41f56

Browse files
committed
Make the heartbeat timer an option so it can be set to None when it's irrelevant
1 parent 11d327c commit da41f56

5 files changed

Lines changed: 112 additions & 31 deletions

File tree

crates/hotfix/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,5 @@ tracing = { workspace = true }
3939
uuid = { version = "1.5.0", features = ["v4"] }
4040

4141
[dev-dependencies]
42-
testcontainers = "^0.24"
42+
testcontainers = "^0.24"
43+
tokio = { version = "^1", features = ["test-util"] }

crates/hotfix/src/message.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ pub use hotfix_message::fix44;
55
pub(crate) use hotfix_message::message::{Config, Message};
66
pub use hotfix_message::{Part, RepeatingGroup};
77

8-
pub(crate) mod heartbeat;
9-
pub(crate) mod logon;
10-
pub(crate) mod logout;
11-
pub(crate) mod parser;
12-
pub(crate) mod resend_request;
13-
pub(crate) mod sequence_reset;
14-
pub(crate) mod test_request;
8+
pub mod heartbeat;
9+
pub mod logon;
10+
pub mod logout;
11+
pub mod parser;
12+
pub mod resend_request;
13+
pub mod sequence_reset;
14+
pub mod test_request;
1515

1616
pub use parser::RawFixMessage;
1717

@@ -23,7 +23,7 @@ pub trait FixMessage: Clone + Send + 'static {
2323
fn parse(message: &Message) -> Self;
2424
}
2525

26-
pub(crate) fn generate_message(
26+
pub fn generate_message(
2727
sender_comp_id: &str,
2828
target_comp_id: &str,
2929
msg_seq_num: usize,

crates/hotfix/src/session.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ struct Session<M, S> {
120120
store: S,
121121
awaiting_test_response: Option<TestRequestId>,
122122

123-
heartbeat_timer: Pin<Box<Sleep>>,
123+
heartbeat_timer: Option<Pin<Box<Sleep>>>,
124124
peer_timer: Pin<Box<Sleep>>,
125125
schedule_check_timer: Pin<Box<Sleep>>,
126126
}
@@ -132,7 +132,6 @@ impl<M: FixMessage, S: MessageStore> Session<M, S> {
132132
application: ApplicationRef<M>,
133133
store: S,
134134
) -> Session<M, S> {
135-
let heartbeat_timer = sleep(Duration::from_secs(config.heartbeat_interval));
136135
let peer_timer = sleep(Duration::from_secs(
137136
(config.heartbeat_interval as f64 * TEST_REQUEST_THRESHOLD).round() as u64,
138137
));
@@ -151,7 +150,7 @@ impl<M: FixMessage, S: MessageStore> Session<M, S> {
151150
application,
152151
store,
153152
awaiting_test_response: None,
154-
heartbeat_timer: Box::pin(heartbeat_timer),
153+
heartbeat_timer: None,
155154
peer_timer: Box::pin(peer_timer),
156155
schedule_check_timer: Box::pin(schedule_check_timer),
157156
}
@@ -545,7 +544,12 @@ impl<M: FixMessage, S: MessageStore> Session<M, S> {
545544

546545
fn reset_heartbeat_timer(&mut self) {
547546
let deadline = Instant::now() + Duration::from_secs(self.config.heartbeat_interval);
548-
self.heartbeat_timer.as_mut().reset(deadline);
547+
if let Some(heartbeat_timer) = &mut self.heartbeat_timer {
548+
heartbeat_timer.as_mut().reset(deadline);
549+
} else {
550+
let timer = sleep(Duration::from_secs(self.config.heartbeat_interval));
551+
self.heartbeat_timer = Some(Box::pin(timer));
552+
}
549553
}
550554

551555
fn reset_peer_timer(&mut self, awaiting_req_id: Option<TestRequestId>) {
@@ -742,7 +746,13 @@ where
742746
None => break,
743747
}
744748
}
745-
() = &mut session.heartbeat_timer.as_mut() => {
749+
() = async {
750+
if let Some(ref mut timer) = session.heartbeat_timer {
751+
timer.as_mut().await
752+
} else {
753+
std::future::pending().await
754+
}
755+
} => {
746756
session.handle_heartbeat_timeout().await;
747757
}
748758
() = &mut session.peer_timer.as_mut() => {

crates/hotfix/tests/common/mock_counterparty.rs

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use hotfix::message::{FixMessage, RawFixMessage};
1+
use hotfix::config::SessionConfig;
2+
use hotfix::message::logon::{Logon, ResetSeqNumConfig};
3+
use hotfix::message::{FixMessage, RawFixMessage, generate_message};
24
use hotfix::session::SessionRef;
35
use hotfix::transport::FixConnection;
46
use hotfix::transport::reader::ReaderRef;
@@ -12,18 +14,23 @@ use tokio::sync::{mpsc, oneshot};
1214

1315
const DEFAULT_TIMEOUT: Duration = Duration::from_millis(10);
1416

15-
pub struct MockCounterparty {
17+
pub struct MockCounterparty<M> {
1618
receiver: Receiver<WriterMessage>,
17-
// History of received messages from the session
18-
messages: Vec<Message>,
19+
received_messages: Vec<Message>,
20+
sent_messages: Vec<Vec<u8>>,
21+
session_ref: SessionRef<M>,
22+
session_config: SessionConfig,
1923
dictionary: Dictionary,
2024
message_config: MessageConfig,
2125
_connection: FixConnection,
2226
_dc_sender: oneshot::Sender<()>,
2327
}
2428

25-
impl MockCounterparty {
26-
pub async fn start(session_ref: SessionRef<impl FixMessage>) -> Self {
29+
impl<M> MockCounterparty<M>
30+
where
31+
M: FixMessage,
32+
{
33+
pub async fn start(session_ref: SessionRef<M>, session_config: SessionConfig) -> Self {
2734
let (writer_ref, receiver) = Self::create_writer();
2835
let (reader_ref, dc_sender) = Self::create_reader();
2936
let connection = FixConnection::new(writer_ref, reader_ref);
@@ -32,14 +39,39 @@ impl MockCounterparty {
3239

3340
Self {
3441
receiver,
35-
messages: vec![],
42+
received_messages: vec![],
43+
sent_messages: vec![],
44+
session_ref,
45+
session_config,
3646
dictionary: Dictionary::fix44(),
3747
message_config: MessageConfig::default(),
3848
_connection: connection,
3949
_dc_sender: dc_sender,
4050
}
4151
}
4252

53+
pub async fn send_logon(&mut self) {
54+
let logon = Logon::new(
55+
self.session_config.heartbeat_interval,
56+
ResetSeqNumConfig::NoReset(None),
57+
);
58+
self.send_message(logon).await;
59+
}
60+
61+
pub async fn send_message(&mut self, message: impl FixMessage) {
62+
let raw_message = generate_message(
63+
&self.session_config.sender_comp_id,
64+
&self.session_config.target_comp_id,
65+
self.sent_messages.len() + 1,
66+
message,
67+
)
68+
.expect("failed to generate message");
69+
self.sent_messages.push(raw_message.clone());
70+
self.session_ref
71+
.new_fix_message_received(RawFixMessage::new(raw_message))
72+
.await;
73+
}
74+
4375
/// Waits for and returns the next message received from the session.
4476
///
4577
/// A `None` response indicates we have been disconnected, either through the channel
@@ -51,8 +83,8 @@ impl MockCounterparty {
5183
.and_then(|writer_message| match writer_message {
5284
WriterMessage::SendMessage(raw_message) => {
5385
let message = self.parse_message(&raw_message);
54-
self.messages.push(message);
55-
self.messages.last()
86+
self.received_messages.push(message);
87+
self.received_messages.last()
5688
}
5789
WriterMessage::Disconnect => None,
5890
})
@@ -73,15 +105,15 @@ impl MockCounterparty {
73105

74106
pub async fn assert_next<F>(&mut self, assertion: F)
75107
where
76-
F: FnOnce(&Message) -> bool,
108+
F: FnOnce(&Message),
77109
{
78110
self.assert_next_with_timeout(assertion, DEFAULT_TIMEOUT)
79111
.await;
80112
}
81113

82114
pub async fn assert_next_with_timeout<F>(&mut self, assertion: F, timeout: Duration)
83115
where
84-
F: FnOnce(&Message) -> bool,
116+
F: FnOnce(&Message),
85117
{
86118
match tokio::time::timeout(timeout, self.get_next()).await {
87119
Ok(Some(message)) => {

crates/hotfix/tests/session_tests.rs

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ use hotfix_message::fix44::MSG_TYPE;
1010

1111
mod common;
1212

13+
const HEARTBEAT_INTERVAL: u64 = 30;
14+
1315
#[tokio::test]
1416
async fn test_happy_login_flow() {
15-
let session = create_session();
16-
let mut mock_counterparty = MockCounterparty::start(session.clone()).await;
17+
let (session, mut mock_counterparty) = setup().await;
1718

1819
// assert that a logon message is received (type 'A')
1920
mock_counterparty
20-
.assert_next(|msg| msg.header().get::<&str>(MSG_TYPE).unwrap() == "A")
21+
.assert_next(|msg| assert_eq!(msg.header().get::<&str>(MSG_TYPE).unwrap(), "A"))
2122
.await;
2223

2324
session
@@ -26,12 +27,40 @@ async fn test_happy_login_flow() {
2627
mock_counterparty.assert_disconnected().await;
2728
}
2829

29-
fn create_session() -> SessionRef<TestMessage> {
30+
#[tokio::test(start_paused = true)]
31+
async fn test_heartbeats() {
32+
let (session, mut mock_counterparty) = setup().await;
33+
34+
// assert that a logon message is received (type 'A')
35+
mock_counterparty
36+
.assert_next(|msg| assert_eq!(msg.header().get::<&str>(MSG_TYPE).unwrap(), "A"))
37+
.await;
38+
// counterparty responds with a logon to establish a happy session
39+
mock_counterparty.send_logon().await;
40+
41+
// let's wait enough time for a heartbeat and assert that the heartbeat was sent
42+
tokio::time::advance(std::time::Duration::from_secs(HEARTBEAT_INTERVAL + 1)).await;
43+
mock_counterparty
44+
.assert_next(|msg| assert_eq!(msg.header().get::<&str>(MSG_TYPE).unwrap(), "0"))
45+
.await;
46+
47+
session
48+
.disconnect("Test Session Finished".to_string())
49+
.await;
50+
mock_counterparty.assert_disconnected().await;
51+
}
52+
53+
async fn setup() -> (SessionRef<TestMessage>, MockCounterparty<TestMessage>) {
3054
let config = create_session_config();
55+
let counterparty_config = create_counterparty_session_config(config.clone());
56+
3157
let application_ref = ApplicationRef::new(MockApplication {});
3258
let message_store = InMemoryMessageStore::default();
3359

34-
SessionRef::new(config, application_ref, message_store)
60+
let session = SessionRef::new(config, application_ref, message_store);
61+
let mock_counterparty = MockCounterparty::start(session.clone(), counterparty_config).await;
62+
63+
(session, mock_counterparty)
3564
}
3665

3766
fn create_session_config() -> SessionConfig {
@@ -43,9 +72,18 @@ fn create_session_config() -> SessionConfig {
4372
connection_host: "".to_string(),
4473
connection_port: 0,
4574
tls_config: None,
46-
heartbeat_interval: 30,
75+
heartbeat_interval: HEARTBEAT_INTERVAL,
4776
reconnect_interval: 30,
4877
reset_on_logon: false,
4978
schedule: None,
5079
}
5180
}
81+
82+
/// Create a session configuration for the counterparty from our configuration.
83+
fn create_counterparty_session_config(session_config: SessionConfig) -> SessionConfig {
84+
SessionConfig {
85+
sender_comp_id: session_config.target_comp_id.clone(),
86+
target_comp_id: session_config.sender_comp_id.clone(),
87+
..session_config
88+
}
89+
}

0 commit comments

Comments
 (0)