diff --git a/crates/hotfix/src/config.rs b/crates/hotfix/src/config.rs index 04e1662..64097a5 100644 --- a/crates/hotfix/src/config.rs +++ b/crates/hotfix/src/config.rs @@ -52,6 +52,10 @@ fn default_logon_timeout() -> u64 { 10 } +fn default_logout_timeout() -> u64 { + 2 +} + /// The configuration of a single FIX session. #[derive(Clone, Debug, Deserialize)] pub struct SessionConfig { @@ -87,6 +91,10 @@ pub struct SessionConfig { #[serde(default = "default_logon_timeout")] pub logon_timeout: u64, + /// The time we wait in seconds for Logon responses before timing out. + #[serde(default = "default_logout_timeout")] + pub logout_timeout: u64, + /// The interval we should attempt to reconnect at in seconds. #[serde(default = "default_reconnect_interval")] pub reconnect_interval: u64, diff --git a/crates/hotfix/src/message/verification.rs b/crates/hotfix/src/message/verification.rs index 5e8ccdd..d2b0ce1 100644 --- a/crates/hotfix/src/message/verification.rs +++ b/crates/hotfix/src/message/verification.rs @@ -191,6 +191,7 @@ mod tests { tls_config: None, heartbeat_interval: 0, logon_timeout: 0, + logout_timeout: 0, reconnect_interval: 0, reset_on_logon: false, schedule: None, diff --git a/crates/hotfix/src/session.rs b/crates/hotfix/src/session.rs index 18589c1..ea24bef 100644 --- a/crates/hotfix/src/session.rs +++ b/crates/hotfix/src/session.rs @@ -293,11 +293,8 @@ impl, M: FixMessage, S: MessageStore> Session { SessionState::Disconnected { .. } => { warn!("disconnect message was received, but the session is already disconnected") } - SessionState::AwaitingLogout { .. } => { - // this is unexpected because the other side should send a logout before disconnecting, - // which would move this session out of the ShuttingDown state - // TODO: is this actually true? need to review the spec carefully - warn!("disconnect message was received, but the session is still shutting down") + SessionState::AwaitingLogout { reconnect, .. } => { + self.state = SessionState::new_disconnected(reconnect, &reason); } } } @@ -782,8 +779,11 @@ impl, M: FixMessage, S: MessageStore> Session { self.state.disconnect_writer().await; } - async fn initiate_graceful_logout(&mut self, reason: &str) { - if self.state.try_transition_to_awaiting_logout() { + async fn initiate_graceful_logout(&mut self, reason: &str, reconnect: bool) { + if self.state.try_transition_to_awaiting_logout( + Duration::from_secs(self.config.logout_timeout), + reconnect, + ) { self.send_logout(reason).await; } } @@ -826,8 +826,8 @@ impl, M: FixMessage, S: MessageStore> Session { match request { AdminRequest::InitiateGracefulShutdown { reconnect } => { warn!("initiating shutdown on request from admin.."); - self.logout_and_terminate("shutdown requested").await; - self.state = SessionState::new_disconnected(reconnect, "shutdown requested"); + self.initiate_graceful_logout("explicitly requested", reconnect) + .await; } AdminRequest::RequestSessionInfo(responder) => { info!("session info requested"); @@ -853,6 +853,9 @@ impl, M: FixMessage, S: MessageStore> Session { } else if self.state.is_awaiting_logon() { warn!("peer didn't respond to our Logon, disconnecting.."); self.state.disconnect_writer().await; + } else if self.state.is_awaiting_logout() { + warn!("peer didn't respond to our Logout, disconnecting.."); + self.state.disconnect_writer().await; } else { let req_id = format!("TEST_{}", self.store.next_target_seq_number()); info!("sending TestRequest due to peer timer expiring"); @@ -892,7 +895,8 @@ impl, M: FixMessage, S: MessageStore> Session { } } else if self.state.is_connected() { // we are currently outside scheduled session time - self.initiate_graceful_logout("End of session time").await; + self.initiate_graceful_logout("End of session time", true) + .await; } // we always need to reschedule the check, otherwise we won't be able to resume an inactive session diff --git a/crates/hotfix/src/session/state.rs b/crates/hotfix/src/session/state.rs index 9ec4b16..414b8d0 100644 --- a/crates/hotfix/src/session/state.rs +++ b/crates/hotfix/src/session/state.rs @@ -24,7 +24,11 @@ pub enum SessionState { /// We are awaiting the target to resend the gap we have. AwaitingResend(AwaitingResendState), /// We are in the process of gracefully logging out - AwaitingLogout { writer: WriterRef }, // we need the writer so we can disconnect it on successful logout + AwaitingLogout { + writer: WriterRef, // we need the writer so we can disconnect it on successful logout + logout_timeout: Instant, + reconnect: bool, // we carry this forward for the subsequent disconnected state + }, /// The session is active, we have connected and mutually logged on. Active(ActiveState), /// The TCP connection has been dropped. @@ -87,7 +91,7 @@ impl SessionState { _ => error!("invalid outgoing message for AwaitingLogon state"), } } - Self::AwaitingLogout { writer } => { + Self::AwaitingLogout { writer, .. } => { // Logout messages are allowed because we first transition into AwaitingLogout // and only then send the logout message if message_type == b"5" { @@ -102,7 +106,7 @@ impl SessionState { match self { Self::Active(ActiveState { writer, .. }) | Self::AwaitingLogon { writer, .. } - | Self::AwaitingLogout { writer } + | Self::AwaitingLogout { writer, .. } | Self::AwaitingResend(AwaitingResendState { writer, .. }) => writer.disconnect().await, _ => debug!("disconnecting an already disconnected session"), } @@ -112,13 +116,17 @@ impl SessionState { match self { Self::Active(ActiveState { writer, .. }) | Self::AwaitingLogon { writer, .. } - | Self::AwaitingLogout { writer } + | Self::AwaitingLogout { writer, .. } | Self::AwaitingResend(AwaitingResendState { writer, .. }) => Some(writer), _ => None, } } - pub fn try_transition_to_awaiting_logout(&mut self) -> bool { + pub fn try_transition_to_awaiting_logout( + &mut self, + logout_timeout: Duration, + reconnect: bool, + ) -> bool { if matches!(self, SessionState::AwaitingLogout { .. }) { debug!("already in awaiting logout state"); return false; @@ -127,6 +135,8 @@ impl SessionState { if let Some(writer) = self.get_writer() { *self = SessionState::AwaitingLogout { writer: writer.clone(), + logout_timeout: Instant::now() + logout_timeout, + reconnect, }; true } else { @@ -220,6 +230,7 @@ impl SessionState { match self { Self::Active(ActiveState { peer_deadline, .. }) => Some(peer_deadline), Self::AwaitingLogon { logon_timeout, .. } => Some(logon_timeout), + Self::AwaitingLogout { logout_timeout, .. } => Some(logout_timeout), _ => None, } } @@ -268,6 +279,10 @@ impl SessionState { matches!(self, SessionState::AwaitingLogon { .. }) } + pub fn is_awaiting_logout(&self) -> bool { + matches!(self, SessionState::AwaitingLogout { .. }) + } + pub fn as_status(&self) -> SessionInfoStatus { match self { SessionState::AwaitingLogon { .. } => SessionInfoStatus::AwaitingLogon, @@ -427,6 +442,8 @@ mod tests { fn test_awaiting_resend_transition_when_awaiting_logout_is_prevented() { let mut state = SessionState::AwaitingLogout { writer: create_writer_ref(), + logout_timeout: Instant::now(), + reconnect: false, }; let result = state.try_transition_to_awaiting_resend(1, 5); diff --git a/crates/hotfix/tests/common/cleanup.rs b/crates/hotfix/tests/common/cleanup.rs new file mode 100644 index 0000000..809d401 --- /dev/null +++ b/crates/hotfix/tests/common/cleanup.rs @@ -0,0 +1,40 @@ +use crate::common::assertions::{DEFAULT_TIMEOUT, assert_msg_type}; +use crate::common::fakes::{FakeCounterparty, SessionSpy}; +use crate::common::test_messages::TestMessage; +use hotfix::message::logout::Logout; +use hotfix_message::fix44::MsgType; + +pub struct Finally<'a> { + session: &'a SessionSpy, + counterparty: &'a mut FakeCounterparty, +} + +pub fn finally<'a>( + session: &'a SessionSpy, + counterparty: &'a mut FakeCounterparty, +) -> Finally<'a> { + Finally { + session, + counterparty, + } +} + +impl<'a> Finally<'a> { + pub async fn disconnect(self) { + // initiate disconnect from our side + self.session.session_handle().shutdown(false).await.unwrap(); + + // counterparty receives our logout message + self.counterparty + .assert_next_with_timeout(|msg| assert_msg_type(msg, MsgType::Logout), DEFAULT_TIMEOUT) + .await; + + // counterparty responds with logout acknowledgement + self.counterparty.send_message(Logout::default()).await; + + // verify disconnection occurs + self.counterparty + .assert_disconnected_with_timeout(DEFAULT_TIMEOUT) + .await; + } +} diff --git a/crates/hotfix/tests/common/mod.rs b/crates/hotfix/tests/common/mod.rs index a786f35..4725e1b 100644 --- a/crates/hotfix/tests/common/mod.rs +++ b/crates/hotfix/tests/common/mod.rs @@ -1,5 +1,6 @@ pub mod actions; pub mod assertions; +pub mod cleanup; pub mod fakes; pub mod setup; pub mod test_messages; diff --git a/crates/hotfix/tests/common/setup.rs b/crates/hotfix/tests/common/setup.rs index c669ebe..2392f3e 100644 --- a/crates/hotfix/tests/common/setup.rs +++ b/crates/hotfix/tests/common/setup.rs @@ -11,6 +11,7 @@ use hotfix_message::fix44::MSG_TYPE; pub const HEARTBEAT_INTERVAL: u64 = 30; pub const LOGON_TIMEOUT: u64 = 10; +pub const LOGOUT_TIMEOUT: u64 = 2; pub const COUNTERPARTY_COMP_ID: &str = "dummy-acceptor"; pub const OUR_COMP_ID: &str = "dummy-initiator"; @@ -58,6 +59,7 @@ pub fn create_session_config() -> SessionConfig { tls_config: None, heartbeat_interval: HEARTBEAT_INTERVAL, logon_timeout: LOGON_TIMEOUT, + logout_timeout: LOGOUT_TIMEOUT, reconnect_interval: 30, reset_on_logon: false, schedule: None, diff --git a/crates/hotfix/tests/session_test_cases/admin_request_tests.rs b/crates/hotfix/tests/session_test_cases/admin_request_tests.rs index ad31fd2..d8e709d 100644 --- a/crates/hotfix/tests/session_test_cases/admin_request_tests.rs +++ b/crates/hotfix/tests/session_test_cases/admin_request_tests.rs @@ -1,5 +1,6 @@ use crate::common::actions::when; use crate::common::assertions::{assert_msg_type, then}; +use crate::common::cleanup::finally; use crate::common::setup::given_an_active_session; use hotfix::session::Status; use hotfix_message::Part; @@ -40,8 +41,7 @@ async fn test_reset_sequence_numbers_once() { .expect("reset request to succeed"); // the counterparty is disconnected - when(&session).requests_disconnect().await; - then(&mut counterparty).gets_disconnected().await; + finally(&session, &mut counterparty).disconnect().await; // a new connection is established to the counterparty when(&mut counterparty).gets_reconnected(true).await; @@ -70,6 +70,5 @@ async fn test_reset_sequence_numbers_once() { "target sequence number should be 2 (after receiving logon)" ); - when(&session).requests_disconnect().await; - then(&mut counterparty).gets_disconnected().await; + finally(&session, &mut counterparty).disconnect().await; } diff --git a/crates/hotfix/tests/session_test_cases/business_tests.rs b/crates/hotfix/tests/session_test_cases/business_tests.rs index e69db6b..d4f785b 100644 --- a/crates/hotfix/tests/session_test_cases/business_tests.rs +++ b/crates/hotfix/tests/session_test_cases/business_tests.rs @@ -1,5 +1,6 @@ use crate::common::actions::when; use crate::common::assertions::then; +use crate::common::cleanup::finally; use crate::common::setup::given_an_active_session; use crate::common::test_messages::TestMessage; use hotfix::message::FixMessage; @@ -27,6 +28,5 @@ async fn test_new_order_single() { .receives(|msg| assert_eq!(msg.message_type(), MsgType::ExecutionReport.to_string())) .await; - when(&session).requests_disconnect().await; - then(&mut counterparty).gets_disconnected().await; + finally(&session, &mut counterparty).disconnect().await; } diff --git a/crates/hotfix/tests/session_test_cases/heartbeat_tests.rs b/crates/hotfix/tests/session_test_cases/heartbeat_tests.rs index 2b8a5b7..2a60ffa 100644 --- a/crates/hotfix/tests/session_test_cases/heartbeat_tests.rs +++ b/crates/hotfix/tests/session_test_cases/heartbeat_tests.rs @@ -1,5 +1,6 @@ use crate::common::actions::when; use crate::common::assertions::{assert_msg_type, then}; +use crate::common::cleanup::finally; use crate::common::setup::{HEARTBEAT_INTERVAL, given_an_active_session}; use hotfix::message::test_request::TestRequest; use hotfix_message::Part; @@ -27,8 +28,7 @@ async fn test_heartbeats() { .receives(|msg| assert_msg_type(msg, MsgType::Heartbeat)) .await; - when(&session).requests_disconnect().await; - then(&mut counterparty).gets_disconnected().await; + finally(&session, &mut counterparty).disconnect().await; } /// Tests the peer timeout and disconnection mechanism: @@ -83,6 +83,5 @@ async fn test_heartbeat_in_response_to_test_request() { }) .await; - when(&session).requests_disconnect().await; - then(&mut counterparty).gets_disconnected().await; + finally(&session, &mut counterparty).disconnect().await; } diff --git a/crates/hotfix/tests/session_test_cases/invalid_message_tests.rs b/crates/hotfix/tests/session_test_cases/invalid_message_tests.rs index 9d354c6..d4a1fb4 100644 --- a/crates/hotfix/tests/session_test_cases/invalid_message_tests.rs +++ b/crates/hotfix/tests/session_test_cases/invalid_message_tests.rs @@ -1,5 +1,6 @@ use crate::common::actions::when; use crate::common::assertions::{assert_msg_type, then}; +use crate::common::cleanup::finally; use crate::common::setup::{COUNTERPARTY_COMP_ID, OUR_COMP_ID, given_an_active_session}; use crate::common::test_messages::{ ExecutionReportWithInvalidField, TestMessage, build_execution_report_with_comp_id, @@ -28,8 +29,7 @@ async fn test_message_with_invalid_field_gets_rejected() { .receives(|msg| assert_msg_type(msg, MsgType::Reject)) .await; - when(&session).requests_disconnect().await; - then(&mut counterparty).gets_disconnected().await; + finally(&session, &mut counterparty).disconnect().await; } /// Tests that when a counterparty sends a garbled message with an invalid body length, @@ -63,8 +63,7 @@ async fn test_garbled_message_with_invalid_target_comp_id_gets_ignored() { }) .await; - when(&session).requests_disconnect().await; - then(&mut counterparty).gets_disconnected().await; + finally(&session, &mut counterparty).disconnect().await; } /// Tests that when a counterparty sends a message with an invalid BeginString, @@ -165,8 +164,7 @@ async fn test_message_with_invalid_msg_type() { .target_sequence_number_reaches(sequence_number) .await; - when(&session).requests_disconnect().await; - then(&mut counterparty).gets_disconnected().await; + finally(&session, &mut counterparty).disconnect().await; } /// Tests that a message with a sequence number lower than the expected one @@ -226,8 +224,7 @@ async fn test_message_with_sequence_number_too_low_possdup_ignored() { .target_sequence_number_reaches(second_seq) .await; - when(&session).requests_disconnect().await; - then(&mut counterparty).gets_disconnected().await; + finally(&session, &mut counterparty).disconnect().await; } /// Tests that a message with `OrigSendingTime` after `SendingTime` is rejected @@ -262,8 +259,7 @@ async fn test_message_with_incorrect_orig_sending_time_is_rejected() { }) .await; - when(&session).requests_disconnect().await; - then(&mut counterparty).gets_disconnected().await; + finally(&session, &mut counterparty).disconnect().await; } /// Tests that a message with missing `OrigSendingTime` is rejected. @@ -299,8 +295,7 @@ async fn test_message_with_missing_orig_sending_time_is_rejected() { }) .await; - when(&session).requests_disconnect().await; - then(&mut counterparty).gets_disconnected().await; + finally(&session, &mut counterparty).disconnect().await; } /// Tests that a message with missing `SendingTime` is rejected. @@ -333,8 +328,7 @@ async fn test_message_with_missing_sending_time_is_rejected() { .target_sequence_number_reaches(seq_number) .await; - when(&session).requests_disconnect().await; - then(&mut counterparty).gets_disconnected().await; + finally(&session, &mut counterparty).disconnect().await; } /// Tests that a message with `SendingTime` too far in the past is rejected. @@ -367,8 +361,7 @@ async fn test_message_with_sending_time_too_old_is_rejected() { .target_sequence_number_reaches(seq_number) .await; - when(&session).requests_disconnect().await; - then(&mut counterparty).gets_disconnected().await; + finally(&session, &mut counterparty).disconnect().await; } /// Tests that a message with PossDupFlag=Y but missing OrigSendingTime is rejected. @@ -407,6 +400,5 @@ async fn test_scenario_2g_possdup_without_orig_sending_time() { }) .await; - when(&session).requests_disconnect().await; - then(&mut counterparty).gets_disconnected().await; + finally(&session, &mut counterparty).disconnect().await; } diff --git a/crates/hotfix/tests/session_test_cases/logon_tests.rs b/crates/hotfix/tests/session_test_cases/logon_tests.rs index 9452960..496cca4 100644 --- a/crates/hotfix/tests/session_test_cases/logon_tests.rs +++ b/crates/hotfix/tests/session_test_cases/logon_tests.rs @@ -1,5 +1,6 @@ use crate::common::actions::when; use crate::common::assertions::{assert_msg_type, then}; +use crate::common::cleanup::finally; use crate::common::setup::{ LOGON_TIMEOUT, given_a_connected_session, given_a_connected_session_with_store, }; @@ -29,8 +30,7 @@ async fn test_happy_logon() { when(&mut mock_counterparty).sends_logon().await; then(&mut session).status_changes_to(Status::Active).await; - when(&session).requests_disconnect().await; - then(&mut mock_counterparty).gets_disconnected().await; + finally(&session, &mut mock_counterparty).disconnect().await; } /// Tests that sending a non-logon message (execution report) in response to a logon @@ -129,8 +129,7 @@ async fn test_logon_response_with_sequence_number_too_high() { when(&mut counterparty).sends_gap_fill(2, 3).await; // the logon is gap filled then(&mut session).status_changes_to(Status::Active).await; - when(&session).requests_disconnect().await; - then(&mut counterparty).gets_disconnected().await; + finally(&session, &mut counterparty).disconnect().await; } /// Tests the scenario where the counterparty does not respond to our logon message diff --git a/crates/hotfix/tests/session_test_cases/logout_tests.rs b/crates/hotfix/tests/session_test_cases/logout_tests.rs new file mode 100644 index 0000000..57dada8 --- /dev/null +++ b/crates/hotfix/tests/session_test_cases/logout_tests.rs @@ -0,0 +1,85 @@ +use crate::common::actions::when; +use crate::common::assertions::{assert_msg_type, then}; +use crate::common::setup::{LOGOUT_TIMEOUT, given_an_active_session}; +use hotfix::message::logout::Logout; +use hotfix_message::fix44::MsgType; +use std::time::Duration; + +/// Test a successful logout flow where we initiate the logout: +/// 1. Establish an active session +/// 2. We send a logout message +/// 3. Counterparty responds with a logout acknowledgement +/// 4. Verify that the connection is cleanly disconnected +/// +/// This test ensures the proper FIX protocol logout sequence where +/// the session initiates the logout. +#[tokio::test] +async fn test_happy_logout_initiated_by_us() { + let (session, mut counterparty) = given_an_active_session().await; + + // when we send a logout message + when(&session).requests_disconnect().await; + + // then the counterparty receives a logout message + then(&mut counterparty) + .receives(|msg| assert_msg_type(msg, MsgType::Logout)) + .await; + + // and when the counterparty acknowledges the logout + when(&mut counterparty) + .sends_message(Logout::default()) + .await; + + // then disconnection occurs + then(&mut counterparty).gets_disconnected().await; +} + +/// Test a successful logout flow where the counterparty initiates the logout: +/// 1. Establish an active session +/// 2. Counterparty sends a logout message +/// 3. Verify that the session responds with a logout acknowledgement +/// 4. Verify that the connection is cleanly disconnected +/// +/// This test ensures the proper FIX protocol logout sequence where +/// the session responds to a counterparty-initiated logout. +#[tokio::test] +async fn test_happy_logout_initiated_by_counterparty() { + let (_session, mut counterparty) = given_an_active_session().await; + + // when the counterparty initiates logout + when(&mut counterparty) + .sends_message(Logout::default()) + .await; + + // then our session responds with logout acknowledgement + then(&mut counterparty) + .receives(|msg| assert_msg_type(msg, MsgType::Logout)) + .await; + + // then disconnection occurs + then(&mut counterparty).gets_disconnected().await; +} + +/// Test a logout flow where we initiate the logout and the counterparty does not respond: +/// 1. Establish an active session +/// 2. We send a logout message +/// 3. Counterparty does not respond within the logout timeout period +/// 4. Verify that the connection is cleanly disconnected regardless +#[tokio::test(start_paused = true)] +async fn test_logout_timeout_is_handled() { + let (session, mut counterparty) = given_an_active_session().await; + + // when we send a logout message + when(&session).requests_disconnect().await; + + // then the counterparty receives a logout message + then(&mut counterparty) + .receives(|msg| assert_msg_type(msg, MsgType::Logout)) + .await; + + // when enough time elapses to exceed the allowed logout timeout + when(Duration::from_secs(LOGOUT_TIMEOUT)).elapses().await; + + // then disconnection occurs + then(&mut counterparty).gets_disconnected().await; +} diff --git a/crates/hotfix/tests/session_test_cases/mod.rs b/crates/hotfix/tests/session_test_cases/mod.rs index 0d7eee1..7162044 100644 --- a/crates/hotfix/tests/session_test_cases/mod.rs +++ b/crates/hotfix/tests/session_test_cases/mod.rs @@ -3,4 +3,5 @@ mod business_tests; mod heartbeat_tests; mod invalid_message_tests; mod logon_tests; +mod logout_tests; mod resend_tests; diff --git a/crates/hotfix/tests/session_test_cases/resend_tests.rs b/crates/hotfix/tests/session_test_cases/resend_tests.rs index ba2d0ec..865652c 100644 --- a/crates/hotfix/tests/session_test_cases/resend_tests.rs +++ b/crates/hotfix/tests/session_test_cases/resend_tests.rs @@ -1,5 +1,6 @@ use crate::common::actions::when; use crate::common::assertions::{assert_msg_type, then}; +use crate::common::cleanup::finally; use crate::common::setup::{HEARTBEAT_INTERVAL, given_an_active_session}; use crate::common::test_messages::{ TestMessage, build_execution_report_with_incorrect_body_length, build_invalid_resend_request, @@ -41,8 +42,7 @@ async fn test_message_sequence_number_too_high() { when(&mut counterparty).resends_message(3).await; // the second message is resent then(&mut session).status_changes_to(Status::Active).await; - when(&session).requests_disconnect().await; - then(&mut counterparty).gets_disconnected().await; + finally(&session, &mut counterparty).disconnect().await; } /// Tests that when a counterparty repeatedly resends garbled messages that cannot be processed, @@ -140,8 +140,7 @@ async fn test_resent_message_previously_received_is_ignored() { }) .await; - when(&session).requests_disconnect().await; - then(&mut counterparty).gets_disconnected().await; + finally(&session, &mut counterparty).disconnect().await; } /// Tests that when a counterparty sends a resend request without the required field, @@ -165,8 +164,7 @@ async fn test_invalid_resend_request_gets_rejected() { .receives(|msg| assert_msg_type(msg, MsgType::Reject)) .await; - when(&session).requests_disconnect().await; - then(&mut counterparty).gets_disconnected().await; + finally(&session, &mut counterparty).disconnect().await; } } @@ -218,6 +216,5 @@ async fn test_resend_request_with_gap_fill_for_admin_messages() { .receives(|msg| assert_msg_type(msg, MsgType::ExecutionReport)) .await; - when(&session).requests_disconnect().await; - then(&mut counterparty).gets_disconnected().await; + finally(&session, &mut counterparty).disconnect().await; } diff --git a/examples/load-testing/src/main.rs b/examples/load-testing/src/main.rs index 9bfaf52..958a366 100644 --- a/examples/load-testing/src/main.rs +++ b/examples/load-testing/src/main.rs @@ -163,7 +163,8 @@ fn get_config() -> SessionConfig { connection_port: 9880, tls_config: None, heartbeat_interval: 30, - logon_timeout: 30, + logon_timeout: 10, + logout_timeout: 2, reconnect_interval: 30, reset_on_logon: true, schedule: None,