Skip to main content

ironfix_session/
heartbeat.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 27/1/26
5******************************************************************************/
6
7//! Heartbeat and TestRequest management.
8//!
9//! This module handles FIX session heartbeat logic including:
10//! - Sending heartbeats at configured intervals
11//! - Sending TestRequest when no messages received
12//! - Detecting heartbeat timeouts
13
14use std::time::{Duration, Instant};
15
16/// Manages heartbeat timing for a FIX session.
17#[derive(Debug)]
18pub struct HeartbeatManager {
19    /// Heartbeat interval.
20    interval: Duration,
21    /// Time of last message sent.
22    last_sent: Instant,
23    /// Time of last message received.
24    last_received: Instant,
25    /// Pending TestRequest ID, if any.
26    test_request_pending: Option<String>,
27    /// Time when TestRequest was sent.
28    test_request_sent_at: Option<Instant>,
29}
30
31impl HeartbeatManager {
32    /// Creates a new heartbeat manager with the specified interval.
33    ///
34    /// # Arguments
35    /// * `interval` - The heartbeat interval
36    #[must_use]
37    pub fn new(interval: Duration) -> Self {
38        let now = Instant::now();
39        Self {
40            interval,
41            last_sent: now,
42            last_received: now,
43            test_request_pending: None,
44            test_request_sent_at: None,
45        }
46    }
47
48    /// Records that a message was sent.
49    #[inline]
50    pub fn on_message_sent(&mut self) {
51        self.last_sent = Instant::now();
52    }
53
54    /// Records that a message was received.
55    ///
56    /// If a TestRequest was pending and a Heartbeat with matching ID is received,
57    /// the pending request is cleared.
58    ///
59    /// # Arguments
60    /// * `is_heartbeat` - Whether the received message is a Heartbeat
61    /// * `test_req_id` - The TestReqID from the Heartbeat, if present
62    pub fn on_message_received(&mut self, is_heartbeat: bool, test_req_id: Option<&str>) {
63        self.last_received = Instant::now();
64
65        if is_heartbeat
66            && let (Some(pending), Some(received)) = (&self.test_request_pending, test_req_id)
67            && pending == received
68        {
69            self.test_request_pending = None;
70            self.test_request_sent_at = None;
71        }
72    }
73
74    /// Checks if a heartbeat should be sent.
75    ///
76    /// A heartbeat should be sent if no message has been sent within the interval.
77    #[must_use]
78    pub fn should_send_heartbeat(&self) -> bool {
79        self.last_sent.elapsed() >= self.interval
80    }
81
82    /// Checks if a TestRequest should be sent.
83    ///
84    /// A TestRequest should be sent if no message has been received within
85    /// the interval plus a grace period, and no TestRequest is already pending.
86    #[must_use]
87    pub fn should_send_test_request(&self) -> bool {
88        if self.test_request_pending.is_some() {
89            return false;
90        }
91
92        let grace = Duration::from_secs(1);
93        self.last_received.elapsed() >= self.interval + grace
94    }
95
96    /// Checks if the session has timed out.
97    ///
98    /// A timeout occurs if a TestRequest was sent but no response was received
99    /// within the interval.
100    #[must_use]
101    pub fn is_timed_out(&self) -> bool {
102        if let Some(sent_at) = self.test_request_sent_at {
103            sent_at.elapsed() >= self.interval
104        } else {
105            false
106        }
107    }
108
109    /// Records that a TestRequest was sent.
110    ///
111    /// # Arguments
112    /// * `test_req_id` - The TestReqID that was sent
113    pub fn on_test_request_sent(&mut self, test_req_id: String) {
114        self.test_request_pending = Some(test_req_id);
115        self.test_request_sent_at = Some(Instant::now());
116        self.last_sent = Instant::now();
117    }
118
119    /// Returns the pending TestRequest ID, if any.
120    #[must_use]
121    pub fn pending_test_request(&self) -> Option<&str> {
122        self.test_request_pending.as_deref()
123    }
124
125    /// Returns the time since the last message was received.
126    #[must_use]
127    pub fn time_since_last_received(&self) -> Duration {
128        self.last_received.elapsed()
129    }
130
131    /// Returns the time since the last message was sent.
132    #[must_use]
133    pub fn time_since_last_sent(&self) -> Duration {
134        self.last_sent.elapsed()
135    }
136
137    /// Returns the heartbeat interval.
138    #[must_use]
139    pub const fn interval(&self) -> Duration {
140        self.interval
141    }
142
143    /// Resets the manager state.
144    pub fn reset(&mut self) {
145        let now = Instant::now();
146        self.last_sent = now;
147        self.last_received = now;
148        self.test_request_pending = None;
149        self.test_request_sent_at = None;
150    }
151}
152
153/// Generates a unique TestReqID.
154///
155/// Uses the current timestamp in nanoseconds.
156#[must_use]
157pub fn generate_test_req_id() -> String {
158    use std::time::{SystemTime, UNIX_EPOCH};
159
160    let nanos = SystemTime::now()
161        .duration_since(UNIX_EPOCH)
162        .unwrap_or_default()
163        .as_nanos();
164
165    format!("TEST{}", nanos)
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use std::thread::sleep;
172
173    #[test]
174    fn test_heartbeat_manager_new() {
175        let mgr = HeartbeatManager::new(Duration::from_secs(30));
176        assert_eq!(mgr.interval(), Duration::from_secs(30));
177        assert!(mgr.pending_test_request().is_none());
178    }
179
180    #[test]
181    fn test_should_send_heartbeat() {
182        let mgr = HeartbeatManager::new(Duration::from_millis(10));
183        assert!(!mgr.should_send_heartbeat());
184
185        sleep(Duration::from_millis(15));
186        assert!(mgr.should_send_heartbeat());
187    }
188
189    #[test]
190    fn test_on_message_sent() {
191        let mut mgr = HeartbeatManager::new(Duration::from_millis(10));
192        sleep(Duration::from_millis(15));
193        assert!(mgr.should_send_heartbeat());
194
195        mgr.on_message_sent();
196        assert!(!mgr.should_send_heartbeat());
197    }
198
199    #[test]
200    fn test_test_request_pending() {
201        let mut mgr = HeartbeatManager::new(Duration::from_secs(30));
202
203        mgr.on_test_request_sent("TEST123".to_string());
204        assert_eq!(mgr.pending_test_request(), Some("TEST123"));
205
206        mgr.on_message_received(true, Some("TEST123"));
207        assert!(mgr.pending_test_request().is_none());
208    }
209
210    #[test]
211    fn test_generate_test_req_id() {
212        let id1 = generate_test_req_id();
213        std::thread::sleep(std::time::Duration::from_nanos(1));
214        let id2 = generate_test_req_id();
215
216        assert!(id1.starts_with("TEST"));
217        assert!(id2.starts_with("TEST"));
218        // IDs may be equal if generated within the same nanosecond on fast systems
219        // The important thing is that they have the correct format
220        assert!(id1.len() > 4);
221        assert!(id2.len() > 4);
222    }
223}