Skip to main content

quickfix_tokio/
session.rs

1//! The FIX session layer: logon negotiation, heartbeats, sequence number
2//! tracking, resend/gap-fill recovery, and the per-session run loop.
3//!
4//! Protocol behavior follows QuickFIX C++ (`Session.cpp`); the concurrency
5//! model follows quickfix-go: one task owns all session state, connected to
6//! the socket tasks and API handles purely by channels.
7
8use std::collections::BTreeMap;
9use std::sync::Arc;
10use std::time::Duration;
11
12use bytes::Bytes;
13use tokio::sync::{mpsc, oneshot};
14use tokio::time::Instant;
15
16use crate::application::{Application, ApplicationError};
17use crate::datadictionary::DataDictionary;
18use crate::error::{Error, RejectError, Result, SessionRejectReason};
19use crate::log::Log;
20use crate::message::Message;
21use crate::session_id::SessionId;
22use crate::settings::{ConnectionType, SessionConfig};
23use crate::store::MessageStore;
24use crate::tags::{self, msg_type};
25use crate::value::UtcTimestamp;
26
27/// Commands sent to a session task through its handle.
28pub(crate) enum Command {
29    /// Queue an application (or custom admin) message for sending.
30    Send(Message, oneshot::Sender<Result<()>>),
31    /// A transport connection is ready for this session.
32    Attach(Connection),
33    /// Initiate a graceful logout (stays attached until peer replies/timeout).
34    Logout,
35    /// Detach and stop the session task.
36    Stop(oneshot::Sender<()>),
37    Status(oneshot::Sender<SessionStatus>),
38    /// Operationally set the next sender/target sequence numbers (each
39    /// `Some` is applied). Reply resolves once applied.
40    SetSeqNums {
41        sender: Option<u64>,
42        target: Option<u64>,
43        reply: oneshot::Sender<Result<()>>,
44    },
45}
46
47pub(crate) struct Connection {
48    pub inbound: mpsc::Receiver<Bytes>,
49    pub outbound: mpsc::Sender<Bytes>,
50    /// Dropped (closing the channel) when the session detaches; the
51    /// initiator's connect loop uses this to schedule a reconnect.
52    pub disconnected: Option<oneshot::Sender<()>>,
53}
54
55#[derive(Debug, Clone)]
56pub struct SessionStatus {
57    pub connected: bool,
58    pub logged_on: bool,
59    pub next_sender_seq_num: u64,
60    pub next_target_seq_num: u64,
61}
62
63/// Cheap, cloneable handle to a running session task.
64#[derive(Clone)]
65pub struct SessionHandle {
66    pub id: SessionId,
67    pub(crate) cmd_tx: mpsc::Sender<Command>,
68}
69
70impl SessionHandle {
71    /// Send an application message on this session. Resolves once the
72    /// session has accepted (sequenced and persisted) it.
73    pub async fn send(&self, msg: Message) -> Result<()> {
74        let (tx, rx) = oneshot::channel();
75        self.cmd_tx
76            .send(Command::Send(msg, tx))
77            .await
78            .map_err(|_| Error::UnknownSession(self.id.to_string()))?;
79        rx.await.map_err(|_| Error::UnknownSession(self.id.to_string()))?
80    }
81
82    pub async fn logout(&self) -> Result<()> {
83        self.cmd_tx
84            .send(Command::Logout)
85            .await
86            .map_err(|_| Error::UnknownSession(self.id.to_string()))
87    }
88
89    pub async fn status(&self) -> Result<SessionStatus> {
90        let (tx, rx) = oneshot::channel();
91        self.cmd_tx
92            .send(Command::Status(tx))
93            .await
94            .map_err(|_| Error::UnknownSession(self.id.to_string()))?;
95        rx.await.map_err(|_| Error::UnknownSession(self.id.to_string()))
96    }
97
98    pub async fn is_logged_on(&self) -> bool {
99        self.status().await.map(|s| s.logged_on).unwrap_or(false)
100    }
101
102    /// Set the next outbound sequence number (operational reset).
103    pub async fn set_next_sender_seq_num(&self, n: u64) -> Result<()> {
104        self.set_seq_nums(Some(n), None).await
105    }
106
107    /// Set the next expected inbound sequence number (operational reset).
108    pub async fn set_next_target_seq_num(&self, n: u64) -> Result<()> {
109        self.set_seq_nums(None, Some(n)).await
110    }
111
112    async fn set_seq_nums(&self, sender: Option<u64>, target: Option<u64>) -> Result<()> {
113        let (reply, rx) = oneshot::channel();
114        self.cmd_tx
115            .send(Command::SetSeqNums { sender, target, reply })
116            .await
117            .map_err(|_| Error::UnknownSession(self.id.to_string()))?;
118        rx.await.map_err(|_| Error::UnknownSession(self.id.to_string()))?
119    }
120}
121
122/// Outcome of the verify pipeline for the calling handler.
123enum Flow {
124    /// Checks passed; the handler should do its work and increment the
125    /// target seqnum.
126    Continue,
127    /// The message was consumed abnormally (rejected, queued, or ignored);
128    /// the handler must not process it further.
129    Stop,
130}
131
132/// A condition that ends the connection immediately.
133struct Disconnect(String);
134
135type Handling = std::result::Result<Flow, Disconnect>;
136
137pub(crate) struct Session {
138    cfg: SessionConfig,
139    store: Box<dyn MessageStore>,
140    log: Box<dyn Log>,
141    app: Arc<dyn Application>,
142    /// Dictionary for app-message validation (transport+app merged on FIXT).
143    dictionary: Option<Arc<DataDictionary>>,
144    /// Dictionary for admin-message validation (transport-only on FIXT —
145    /// app-level fields are unknown tags in admin messages).
146    admin_dictionary: Option<Arc<DataDictionary>>,
147    cmd_rx: mpsc::Receiver<Command>,
148
149    inbound: Option<mpsc::Receiver<Bytes>>,
150    outbound: Option<mpsc::Sender<Bytes>>,
151    disconnected_tx: Option<oneshot::Sender<()>>,
152
153    // Session state flags (QuickFIX C++ SessionState).
154    received_logon: bool,
155    sent_logon: bool,
156    sent_logout: bool,
157    sent_reset: bool,
158    received_reset: bool,
159    /// Negotiated heartbeat interval (acceptors adopt the initiator's).
160    heart_bt_int: Duration,
161    last_sent: Instant,
162    last_received: Instant,
163    test_request_counter: u32,
164    /// Outstanding resend range we asked the peer for:
165    /// (begin, full_end, current_chunk_end). With MaxMessagesInResendRequest
166    /// the range is requested in chunks; the next chunk goes out when the
167    /// current one completes.
168    resend_range: Option<(u64, u64, u64)>,
169    /// MsgSeqNum of the most recently received message (even if stashed or
170    /// rejected) — the value stamped into LastMsgSeqNumProcessed(369).
171    last_received_seq: u64,
172    /// Raw messages received with too-high seqnums, replayed once the gap
173    /// fills.
174    stash: BTreeMap<u64, Bytes>,
175    /// Whether the most recently processed inbound message was replayed
176    /// from the stash — a too-low SequenceReset-GapFill right after a
177    /// stash replay is obeyed anyway (QuickFIX/n issue #309).
178    last_processed_was_queued: bool,
179    /// Negotiate recovery via NextExpectedMsgSeqNum(789) on logon.
180    send_next_expected: bool,
181    /// When the session is active/resets, and when logons are allowed.
182    schedule: crate::schedule::Schedule,
183    logon_schedule: crate::schedule::Schedule,
184    /// Initiator connected but the logon window was closed; send the logon
185    /// once it opens.
186    pending_logon: bool,
187}
188
189impl Session {
190    pub(crate) fn spawn(
191        cfg: SessionConfig,
192        store: Box<dyn MessageStore>,
193        log: Box<dyn Log>,
194        app: Arc<dyn Application>,
195        dictionary: Option<Arc<DataDictionary>>,
196        admin_dictionary: Option<Arc<DataDictionary>>,
197    ) -> SessionHandle {
198        let (cmd_tx, cmd_rx) = mpsc::channel(64);
199        let handle = SessionHandle { id: cfg.session_id.clone(), cmd_tx };
200        let heart_bt_int = cfg.heart_bt_int;
201        let send_next_expected = cfg.send_next_expected_msg_seq_num;
202        let schedule = cfg.schedule.clone();
203        let logon_schedule = cfg.logon_schedule.clone();
204        let session = Session {
205            cfg,
206            store,
207            log,
208            app,
209            dictionary,
210            admin_dictionary,
211            cmd_rx,
212            inbound: None,
213            outbound: None,
214            disconnected_tx: None,
215            received_logon: false,
216            sent_logon: false,
217            sent_logout: false,
218            sent_reset: false,
219            received_reset: false,
220            heart_bt_int,
221            last_sent: Instant::now(),
222            last_received: Instant::now(),
223            test_request_counter: 0,
224            resend_range: None,
225            last_received_seq: 0,
226            stash: BTreeMap::new(),
227            last_processed_was_queued: false,
228            send_next_expected,
229            schedule,
230            logon_schedule,
231            pending_logon: false,
232        };
233        tokio::spawn(session.run());
234        handle
235    }
236
237    fn is_initiator(&self) -> bool {
238        self.cfg.connection_type == ConnectionType::Initiator
239    }
240
241    fn is_connected(&self) -> bool {
242        self.outbound.is_some()
243    }
244
245    fn is_logged_on(&self) -> bool {
246        self.received_logon && self.sent_logon
247    }
248
249    fn event(&mut self, text: &str) {
250        self.log.on_event(text);
251    }
252
253    // ----- run loop -----
254
255    async fn run(mut self) {
256        self.app.on_create(&self.cfg.session_id).await;
257        // Two timers, like quickfix-go: heartbeats/timeouts fire at their
258        // exact deadline (recomputed each iteration from last-sent/received),
259        // while a coarse 1 s tick drives only the session schedule.
260        let mut schedule_tick = tokio::time::interval(Duration::from_secs(1));
261        schedule_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
262
263        loop {
264            let proto_deadline = self.next_protocol_deadline();
265            let proto_timer = async {
266                match proto_deadline {
267                    Some(at) => tokio::time::sleep_until(at).await,
268                    None => std::future::pending().await,
269                }
270            };
271            tokio::select! {
272                cmd = self.cmd_rx.recv() => {
273                    match cmd {
274                        Some(Command::Send(msg, reply)) => {
275                            let res = self.send_message(msg).await;
276                            let _ = reply.send(res);
277                        }
278                        Some(Command::Attach(conn)) => self.on_attach(conn).await,
279                        Some(Command::Logout) => {
280                            if self.is_logged_on() {
281                                let _ = self.initiate_logout("").await;
282                            }
283                        }
284                        Some(Command::Status(reply)) => {
285                            let _ = reply.send(SessionStatus {
286                                connected: self.is_connected(),
287                                logged_on: self.is_logged_on(),
288                                next_sender_seq_num: self.store.next_sender_seq_num(),
289                                next_target_seq_num: self.store.next_target_seq_num(),
290                            });
291                        }
292                        Some(Command::SetSeqNums { sender, target, reply }) => {
293                            let mut res = Ok(());
294                            if let Some(n) = sender {
295                                res = res.and(self.store.set_next_sender_seq_num(n).await);
296                            }
297                            if let Some(n) = target {
298                                res = res.and(self.store.set_next_target_seq_num(n).await);
299                            }
300                            let _ = reply.send(res);
301                        }
302                        Some(Command::Stop(reply)) => {
303                            if self.is_logged_on() {
304                                let _ = self.initiate_logout("").await;
305                                // Give the peer a moment to reply.
306                                let deadline = Instant::now() + self.cfg.logout_timeout;
307                                while self.is_connected() && Instant::now() < deadline {
308                                    match tokio::time::timeout_at(
309                                        deadline,
310                                        recv_opt(&mut self.inbound),
311                                    )
312                                    .await
313                                    {
314                                        Ok(Some(raw)) => self.on_inbound(raw).await,
315                                        _ => break,
316                                    }
317                                }
318                            }
319                            self.disconnect("session stopped").await;
320                            let _ = reply.send(());
321                            return;
322                        }
323                        None => {
324                            self.disconnect("engine dropped").await;
325                            return;
326                        }
327                    }
328                }
329                maybe_raw = recv_opt(&mut self.inbound) => {
330                    match maybe_raw {
331                        Some(raw) => self.on_inbound(raw).await,
332                        None => self.disconnect("connection closed by peer").await,
333                    }
334                }
335                _ = schedule_tick.tick() => self.on_schedule_tick().await,
336                _ = proto_timer => self.on_protocol_deadline().await,
337            }
338        }
339    }
340
341    async fn on_attach(&mut self, conn: Connection) {
342        if self.is_connected() {
343            self.event("Rejecting connection attempt: session already connected");
344            // Dropping conn closes its channels and thus the new socket.
345            return;
346        }
347        self.inbound = Some(conn.inbound);
348        self.outbound = Some(conn.outbound);
349        self.disconnected_tx = conn.disconnected;
350        self.last_sent = Instant::now();
351        self.last_received = Instant::now();
352        self.test_request_counter = 0;
353        self.event("Connection established");
354
355        if self.is_initiator() {
356            if self.cfg.reset_on_logon {
357                let _ = self.store.reset().await;
358            }
359            if self.cfg.refresh_on_logon {
360                let _ = self.store.refresh().await;
361            }
362            // Only log on inside the logon window; otherwise defer to the
363            // timer, which sends it once the window opens.
364            if self.logon_schedule.is_in_range(chrono::Utc::now()) {
365                if let Err(Disconnect(reason)) = self.send_logon().await {
366                    self.disconnect(&reason).await;
367                }
368            } else {
369                self.pending_logon = true;
370                self.event("Connected outside logon time; deferring logon");
371            }
372        }
373    }
374
375    async fn disconnect(&mut self, reason: &str) {
376        if !self.is_connected() && self.inbound.is_none() {
377            return;
378        }
379        self.event(&format!("Disconnecting: {reason}"));
380        let was_logged_on = self.received_logon || self.sent_logon;
381        self.inbound = None;
382        self.outbound = None;
383        self.disconnected_tx = None; // dropping signals the connect loop
384        self.received_logon = false;
385        self.sent_logon = false;
386        self.sent_logout = false;
387        self.sent_reset = false;
388        self.received_reset = false;
389        self.test_request_counter = 0;
390        self.resend_range = None;
391        self.stash.clear();
392        if self.cfg.reset_on_disconnect {
393            let _ = self.store.reset().await;
394        }
395        if was_logged_on {
396            self.app.on_logout(&self.cfg.session_id).await;
397        }
398    }
399
400    // ----- timers -----
401    //
402    // Following quickfix-go: heartbeats and timeouts are event-driven — they
403    // fire at their exact deadline, computed from the last send/receive, so
404    // they are not limited by the (coarse) schedule tick's resolution. The
405    // schedule runs separately on a 1 s tick.
406
407    /// The earliest heartbeat/test-request/timeout deadline, or `None` when
408    /// nothing is pending (disconnected, or logged on with HeartBtInt=0). The
409    /// deadline is derived from `last_sent`/`last_received`, so an inbound or
410    /// outbound message on the next loop iteration effectively "resets" the
411    /// timer by recomputing a later deadline.
412    fn next_protocol_deadline(&self) -> Option<Instant> {
413        if !self.is_connected() {
414            return None;
415        }
416        if self.sent_logout {
417            return Some(self.last_sent + self.cfg.logout_timeout);
418        }
419        if !self.received_logon {
420            return Some(self.last_received + self.cfg.logon_timeout);
421        }
422        let hbi = self.heart_bt_int;
423        if hbi.is_zero() {
424            return None;
425        }
426        // Disconnect on peer silence, escalating test requests, and our own
427        // heartbeat when idle. Computed identically to on_protocol_deadline.
428        let mut deadline = (self.last_received + mul(hbi, 2.4))
429            .min(self.last_received + mul(hbi, 1.2 * (self.test_request_counter + 1) as f64));
430        if self.test_request_counter == 0 {
431            deadline = deadline.min(self.last_sent + hbi);
432        }
433        Some(deadline)
434    }
435
436    /// Act on whichever heartbeat/timeout deadline has come due.
437    async fn on_protocol_deadline(&mut self) {
438        if !self.is_connected() {
439            return;
440        }
441        let now = Instant::now();
442        let since_sent = now.duration_since(self.last_sent);
443        let since_recv = now.duration_since(self.last_received);
444
445        // A pending logout times out regardless of logon state (we may have
446        // sent Logout in response to a rejected logon attempt).
447        if self.sent_logout {
448            if since_sent >= self.cfg.logout_timeout {
449                self.disconnect("Timed out waiting for logout response").await;
450            }
451            return;
452        }
453        if !self.received_logon {
454            // Awaiting logon: initiators time out; acceptors give the peer
455            // LogonTimeout to identify themselves.
456            if since_recv >= self.cfg.logon_timeout {
457                self.disconnect("Timed out waiting for logon").await;
458            }
459            return;
460        }
461        let hbi = self.heart_bt_int;
462        if hbi.is_zero() {
463            return;
464        }
465        if since_recv >= mul(hbi, 2.4) {
466            if self.cfg.send_logout_before_disconnect_from_timeout {
467                let _ = self.initiate_logout("").await;
468            }
469            self.disconnect("Timed out waiting for heartbeat").await;
470        } else if since_recv >= mul(hbi, 1.2 * (self.test_request_counter + 1) as f64) {
471            self.test_request_counter += 1;
472            let mut tr = Message::with_type(msg_type::TEST_REQUEST);
473            tr.set(tags::TEST_REQ_ID, "TEST");
474            let _ = self.send_message(tr).await;
475        } else if since_sent >= hbi && self.test_request_counter == 0 {
476            let _ = self.send_message(Message::with_type(msg_type::HEARTBEAT)).await;
477        }
478    }
479
480    /// Session-schedule housekeeping on the 1 s tick. Runs even while
481    /// disconnected so re-entering the window can reset sequence numbers
482    /// before the next connection. A non-stop session does nothing here.
483    async fn on_schedule_tick(&mut self) {
484        if self.schedule.is_non_stop() {
485            return;
486        }
487        let utc_now = chrono::Utc::now();
488        if !self.schedule.is_in_range(utc_now) {
489            // Outside session time: don't operate. Log out and drop any
490            // connection; the reset happens when we re-enter the window.
491            if self.is_connected() {
492                if self.is_logged_on() && !self.sent_logout {
493                    let _ = self.initiate_logout("").await;
494                }
495                self.disconnect("Outside session time").await;
496            }
497            return;
498        }
499        // In session time. If this instant belongs to a *different* occurrence
500        // than the store's creation time, a new session has begun — reset
501        // sequence numbers (and restamp creation time).
502        if !self.schedule.is_in_same_range(utc_now, self.store.creation_time()) {
503            self.event("New session instance; resetting sequence numbers");
504            if self.is_connected() {
505                if self.is_logged_on() && !self.sent_logout {
506                    let _ = self.initiate_logout("").await;
507                }
508                self.disconnect("Session time boundary crossed").await;
509            }
510            let _ = self.store.reset().await;
511            return;
512        }
513        // Still in session time but the logon window closed: log out.
514        if self.is_connected()
515            && self.is_logged_on()
516            && !self.sent_logout
517            && !self.logon_schedule.is_in_range(utc_now)
518        {
519            self.event("Logon time expired, initiating logout");
520            let _ = self.initiate_logout("").await;
521        }
522        // Initiator: send a deferred logon once the window opens.
523        if self.pending_logon
524            && self.is_connected()
525            && !self.sent_logon
526            && self.logon_schedule.is_in_range(utc_now)
527        {
528            self.pending_logon = false;
529            if let Err(Disconnect(reason)) = self.send_logon().await {
530                self.disconnect(&reason).await;
531            }
532        }
533    }
534
535    // ----- outbound -----
536
537    /// Fill the standard header: BeginString, CompIDs, MsgSeqNum, SendingTime.
538    fn fill_header(&mut self, msg: &mut Message) {
539        let id = &self.cfg.session_id;
540        msg.header.set(tags::BEGIN_STRING, id.begin_string.as_str());
541        msg.header.set(tags::SENDER_COMP_ID, id.sender_comp_id.as_str());
542        msg.header.set(tags::TARGET_COMP_ID, id.target_comp_id.as_str());
543        if !id.sender_sub_id.is_empty() {
544            msg.header.set(tags::SENDER_SUB_ID, id.sender_sub_id.as_str());
545        }
546        if !id.sender_location_id.is_empty() {
547            msg.header.set(tags::SENDER_LOCATION_ID, id.sender_location_id.as_str());
548        }
549        if !id.target_sub_id.is_empty() {
550            msg.header.set(tags::TARGET_SUB_ID, id.target_sub_id.as_str());
551        }
552        if !id.target_location_id.is_empty() {
553            msg.header.set(tags::TARGET_LOCATION_ID, id.target_location_id.as_str());
554        }
555        msg.header.set(tags::MSG_SEQ_NUM, self.store.next_sender_seq_num());
556        if self.cfg.enable_last_msg_seq_num_processed {
557            msg.header.set(tags::LAST_MSG_SEQ_NUM_PROCESSED, self.last_received_seq);
558        }
559        msg.stamp_sending_time(UtcTimestamp::new(
560            chrono::Utc::now(),
561            self.cfg.timestamp_precision,
562        ));
563    }
564
565    /// The normal send path: assign seqnum, run callbacks, persist, transmit.
566    async fn send_message(&mut self, mut msg: Message) -> Result<()> {
567        // A fresh send is never a possible duplicate (C++ Session::send).
568        msg.header.remove(tags::POSS_DUP_FLAG);
569        msg.header.remove(tags::ORIG_SENDING_TIME);
570        self.fill_header(&mut msg);
571        let is_admin = msg.is_admin();
572        let mt = msg.msg_type().unwrap_or_default();
573        if is_admin {
574            self.app.to_admin(&mut msg, &self.cfg.session_id).await;
575        } else if self.app.to_app(&mut msg, &self.cfg.session_id).await.is_err() {
576            return Err(Error::DoNotSend);
577        }
578        if let Some(dd) = &self.dictionary {
579            dd.canonicalize_body(&mut msg);
580        }
581        let seq = msg.seq_num()?;
582        let raw = msg.to_bytes();
583        if self.cfg.persist_messages {
584            self.store.save_message_and_incr(seq, &raw).await?;
585        } else {
586            self.store.incr_next_sender_seq_num().await?;
587        }
588        // Session-control admin messages always go out; everything else only
589        // once logged on (unsent app messages are recovered via resend).
590        let always = matches!(
591            mt.as_str(),
592            msg_type::LOGON | msg_type::LOGOUT | msg_type::RESEND_REQUEST | msg_type::SEQUENCE_RESET
593        );
594        if self.is_logged_on() || always || self.sent_logon {
595            self.transmit(raw.into()).await;
596        }
597        Ok(())
598    }
599
600    /// Push raw bytes to the write task, bypassing sequencing (used for
601    /// resent messages and gap fills as well as normal sends).
602    async fn transmit(&mut self, raw: Bytes) {
603        self.log.on_outgoing(&raw);
604        if let Some(out) = &self.outbound {
605            if out.send(raw).await.is_err() {
606                self.disconnect("write side closed").await;
607                return;
608            }
609            self.last_sent = Instant::now();
610        }
611    }
612
613    async fn send_logon(&mut self) -> std::result::Result<(), Disconnect> {
614        let mut logon = Message::with_type(msg_type::LOGON);
615        logon.set(tags::ENCRYPT_METHOD, 0);
616        logon.set(tags::HEART_BT_INT, self.heart_bt_int.as_secs());
617        let should_reset = self.cfg.send_reset_seq_num_flag
618            || (self.cfg.session_id.begin_string.as_str() >= "FIX.4.1"
619                && self.cfg.reset_on_logon
620                && self.store.next_sender_seq_num() == 1
621                && self.store.next_target_seq_num() == 1);
622        if should_reset {
623            logon.set(tags::RESET_SEQ_NUM_FLAG, true);
624            self.sent_reset = true;
625        }
626        if let Some(v) = &self.cfg.default_appl_ver_id {
627            logon.set(tags::DEFAULT_APPL_VER_ID, appl_ver_id_enum(v));
628        }
629        // A fresh initiating logon reports the next seqnum we expect to
630        // receive (C++ generateLogon uses getExpectedTargetNum(), no +1 —
631        // we haven't received the peer's logon yet).
632        if self.send_next_expected {
633            logon.set(tags::NEXT_EXPECTED_MSG_SEQ_NUM, self.store.next_target_seq_num());
634        }
635        self.sent_logon = true;
636        self.event("Initiated logon request");
637        self.send_message(logon)
638            .await
639            .map_err(|e| Disconnect(format!("failed to send logon: {e}")))
640    }
641
642    async fn send_logon_reply(&mut self, peer_hbi: Option<u64>) -> Result<()> {
643        if let Some(secs) = peer_hbi {
644            self.heart_bt_int = self
645                .cfg
646                .heart_bt_int_override
647                .unwrap_or(Duration::from_secs(secs));
648        }
649        let mut logon = Message::with_type(msg_type::LOGON);
650        logon.set(tags::ENCRYPT_METHOD, 0);
651        logon.set(tags::HEART_BT_INT, self.heart_bt_int.as_secs());
652        // The reply echoes ResetSeqNumFlag(141) only in 789 mode: C++/go
653        // echo it, QuickFIX/n does not (whose SessionReset acceptance test
654        // expects no 141). Gating on send_next_expected keeps the default
655        // QuickFIX/n-compatible while matching C++ when 789 is enabled.
656        if self.received_reset {
657            if self.send_next_expected {
658                logon.set(tags::RESET_SEQ_NUM_FLAG, true);
659            }
660            self.sent_reset = true;
661        }
662        if let Some(v) = &self.cfg.default_appl_ver_id {
663            logon.set(tags::DEFAULT_APPL_VER_ID, appl_ver_id_enum(v));
664        }
665        // The reply reports next-target + 1: the incoming logon we're
666        // replying to has not incremented the target seqnum yet (C++
667        // generateLogon(reply)).
668        if self.send_next_expected {
669            logon.set(tags::NEXT_EXPECTED_MSG_SEQ_NUM, self.store.next_target_seq_num() + 1);
670        }
671        self.sent_logon = true;
672        self.event("Responding to logon request");
673        self.send_message(logon).await
674    }
675
676    async fn initiate_logout(&mut self, reason: &str) -> Result<()> {
677        let mut logout = Message::with_type(msg_type::LOGOUT);
678        if !reason.is_empty() {
679            logout.set(tags::TEXT, reason);
680            self.event(&format!("Initiated logout: {reason}"));
681        } else {
682            self.event("Initiated logout request");
683        }
684        self.sent_logout = true;
685        self.send_message(logout).await
686    }
687
688    /// Session-level Reject (35=3). Consumes the offending seqnum only when
689    /// the offender was in sequence and is not a Logon or SequenceReset
690    /// (C++ `generateReject`).
691    async fn send_reject(&mut self, offender: &Message, rej: &RejectError) -> Result<()> {
692        let mt = offender.msg_type().unwrap_or_default();
693        if mt != msg_type::LOGON
694            && mt != msg_type::SEQUENCE_RESET
695            && offender.seq_num().ok() == Some(self.store.next_target_seq_num())
696        {
697            self.store.incr_next_target_seq_num().await?;
698        }
699        let fix42_plus = self.cfg.session_id.begin_string.as_str() >= "FIX.4.2"
700            || self.cfg.session_id.is_fixt();
701        // Body fields in ascending tag order (canonical form).
702        let mut reject = Message::with_type(msg_type::REJECT);
703        reverse_route(offender, &mut reject, &self.cfg.session_id.begin_string);
704        if let Ok(seq) = offender.seq_num() {
705            reject.set(tags::REF_SEQ_NUM, seq);
706        }
707        // Pre-4.2 has no RefTagID(371): the offending tag rides in Text(58)
708        // as "reason (tag)" instead.
709        let text = match rej.ref_tag {
710            Some(tag) if !fix42_plus && rej.text.is_none() => format!("{rej} ({tag})"),
711            _ => rej.to_string(),
712        };
713        reject.set(tags::TEXT, text.as_str());
714        if fix42_plus {
715            if let Some(tag) = rej.ref_tag {
716                reject.set(tags::REF_TAG_ID, tag);
717            }
718            if let Ok(mt) = offender.msg_type() {
719                reject.set(tags::REF_MSG_TYPE, mt.as_str());
720            }
721            // FIX.4.2's SessionRejectReason enum stops at 11; higher codes
722            // exist only from FIX.4.3 on and are omitted before that.
723            let code = rej.reason.code();
724            if code <= 11 || self.cfg.session_id.begin_string.as_str() > "FIX.4.2" {
725                reject.set(tags::SESSION_REJECT_REASON, code);
726            }
727        }
728        self.event(&format!("Message rejected: {rej}"));
729        self.send_message(reject).await
730    }
731
732    async fn send_business_reject(&mut self, offender: &Message) -> Result<()> {
733        self.store.incr_next_target_seq_num().await?;
734        let fix42_plus = self.cfg.session_id.begin_string.as_str() >= "FIX.4.2"
735            || self.cfg.session_id.is_fixt();
736        // Body fields in ascending tag order (canonical form). The casing
737        // difference is QuickFIX/n-faithful: BusinessMessageReject says
738        // "Unsupported Message Type", the pre-4.2 Reject fallback says
739        // "Unsupported message type".
740        let (mut reject, text) = if fix42_plus {
741            (Message::with_type(msg_type::BUSINESS_MESSAGE_REJECT), "Unsupported Message Type")
742        } else {
743            (Message::with_type(msg_type::REJECT), "Unsupported message type")
744        };
745        reverse_route(offender, &mut reject, &self.cfg.session_id.begin_string);
746        if let Ok(seq) = offender.seq_num() {
747            reject.set(tags::REF_SEQ_NUM, seq);
748        }
749        reject.set(tags::TEXT, text);
750        if fix42_plus {
751            if let Ok(mt) = offender.msg_type() {
752                reject.set(tags::REF_MSG_TYPE, mt.as_str());
753            }
754            reject.set(tags::BUSINESS_REJECT_REASON, 3u32); // unsupported message type
755        }
756        self.send_message(reject).await
757    }
758
759    async fn send_resend_request(&mut self, begin: u64, received: u64) -> Result<()> {
760        let full_end = received - 1;
761        let mut rr = Message::with_type(msg_type::RESEND_REQUEST);
762        rr.set(tags::BEGIN_SEQ_NO, begin);
763        let chunk_end = match self.cfg.max_messages_in_resend_request {
764            // "To infinity": 0 for FIX >= 4.2, 999999 before.
765            0 => {
766                let open_ended = self.cfg.session_id.begin_string.as_str() >= "FIX.4.2"
767                    || self.cfg.session_id.is_fixt();
768                rr.set(tags::END_SEQ_NO, if open_ended { 0 } else { 999999u64 });
769                full_end
770            }
771            max => {
772                let chunk_end = full_end.min(begin + max - 1);
773                rr.set(tags::END_SEQ_NO, chunk_end);
774                chunk_end
775            }
776        };
777        self.resend_range = Some((begin, full_end, chunk_end));
778        self.event(&format!("Sent ResendRequest FROM: {begin} TO: {chunk_end}"));
779        self.send_message(rr).await
780    }
781
782    /// After inbound processing: request the next chunk of an outstanding
783    /// resend range, or note that it has been satisfied.
784    async fn check_resend_chunks(&mut self) {
785        let Some((_, full_end, chunk_end)) = self.resend_range else { return };
786        let expected = self.store.next_target_seq_num();
787        if expected <= chunk_end {
788            return;
789        }
790        if expected <= full_end {
791            let _ = self.send_resend_request(expected, full_end + 1).await;
792        } else {
793            self.event(&format!("ResendRequest for messages FROM ... TO {full_end} has been satisfied"));
794            self.resend_range = None;
795        }
796    }
797
798    /// SequenceReset-GapFill covering `[begin, new_seq)`. Bypasses normal
799    /// sequencing: MsgSeqNum is the start of the gap being filled.
800    async fn send_gap_fill(&mut self, begin: u64, new_seq: u64) {
801        let mut m = Message::with_type(msg_type::SEQUENCE_RESET);
802        self.fill_header(&mut m);
803        m.header.set(tags::MSG_SEQ_NUM, begin);
804        m.header.set(tags::POSS_DUP_FLAG, true);
805        let now = UtcTimestamp::new(chrono::Utc::now(), self.cfg.timestamp_precision);
806        m.header.set(tags::ORIG_SENDING_TIME, now);
807        m.set(tags::NEW_SEQ_NO, new_seq);
808        m.set(tags::GAP_FILL_FLAG, true);
809        self.app.to_admin(&mut m, &self.cfg.session_id).await;
810        self.event(&format!("Sent SequenceReset (GapFill) {begin} -> {new_seq}"));
811        let raw = m.to_bytes();
812        self.transmit(raw.into()).await;
813    }
814
815    // ----- inbound -----
816
817    async fn on_inbound(&mut self, raw: Bytes) {
818        self.log.on_incoming(&raw);
819        let msg = match Message::parse(&raw, self.cfg.validate_length_checksum) {
820            Ok(m) => m,
821            Err(e) => {
822                // Garbled: ignore, do not increment, do not reject.
823                self.event(&format!("Invalid message: {e}"));
824                // A garbled logon is fatal (mirrors the C++ engine).
825                if !self.received_logon && contains_field(&raw, b"35=A") {
826                    self.disconnect("garbled logon").await;
827                }
828                return;
829            }
830        };
831        if let Ok(seq) = msg.seq_num() {
832            self.last_received_seq = seq;
833        }
834        if let Err(Disconnect(reason)) = self.process(msg, raw).await {
835            self.disconnect(&reason).await;
836            return;
837        }
838        self.last_processed_was_queued = false;
839        if let Err(Disconnect(reason)) = self.drain_stash().await {
840            self.disconnect(&reason).await;
841            return;
842        }
843        self.check_resend_chunks().await;
844    }
845
846    /// Handle one parsed message (without stash draining).
847    async fn process(&mut self, msg: Message, raw: Bytes) -> std::result::Result<(), Disconnect> {
848        let mt = match msg.msg_type() {
849            Ok(mt) => mt,
850            Err(_) => {
851                self.event("Message without MsgType ignored");
852                return Ok(());
853            }
854        };
855        // BeginString mismatch: incorrect version is fatal.
856        if msg.header.get_raw(tags::BEGIN_STRING)
857            != Some(self.cfg.session_id.begin_string.as_bytes())
858        {
859            let got = msg
860                .header
861                .get_raw(tags::BEGIN_STRING)
862                .map(|v| String::from_utf8_lossy(v).into_owned())
863                .unwrap_or_default();
864            let _ = self.store.incr_next_target_seq_num().await;
865            let _ = self.initiate_logout(&format!("Incorrect BeginString ({got})")).await;
866            return Err(Disconnect("incorrect BeginString".into()));
867        }
868
869        // Dictionary validation of all messages, admin included, before
870        // dispatch (mirrors the C++ engine's ordering). Failures consume
871        // the seqnum. FIXT admin messages validate against the transport
872        // dictionary alone.
873        let dd = if msg.is_admin() { &self.admin_dictionary } else { &self.dictionary };
874        if let Some(dd) = dd.clone() {
875            if let Err(rej) = dd.validate(&msg, &self.cfg.validation) {
876                let _ = self.send_reject(&msg, &rej).await;
877                return Ok(());
878            }
879        }
880
881        match mt.as_str() {
882            msg_type::LOGON => self.handle_logon(&msg).await,
883            msg_type::HEARTBEAT | msg_type::REJECT => self.handle_plain_admin(&msg).await,
884            msg_type::TEST_REQUEST => self.handle_test_request(&msg).await,
885            msg_type::RESEND_REQUEST => self.handle_resend_request(&msg).await,
886            msg_type::SEQUENCE_RESET => self.handle_sequence_reset(&msg).await,
887            msg_type::LOGOUT => self.handle_logout(&msg).await,
888            _ => self.handle_app_message(&msg, raw).await,
889        }
890    }
891
892    // ----- verify pipeline (C++ Session::verify) -----
893
894    async fn verify(
895        &mut self,
896        msg: &Message,
897        check_too_high: bool,
898        check_too_low: bool,
899    ) -> Handling {
900        let mt = msg.msg_type().unwrap_or_default();
901
902        if !self.valid_logon_state(&mt) {
903            return Err(Disconnect(format!("logon state invalid for message type {mt}")));
904        }
905
906        // SendingTime present and within the latency window.
907        match msg.header.get_opt::<UtcTimestamp>(tags::SENDING_TIME) {
908            Ok(Some(st)) => {
909                if self.cfg.check_latency {
910                    let delta = (chrono::Utc::now() - st.time).abs();
911                    if delta.num_seconds().unsigned_abs()
912                        > self.cfg.max_latency.as_secs()
913                    {
914                        let _ = self
915                            .send_reject(
916                                msg,
917                                &RejectError::new(
918                                    SessionRejectReason::SendingTimeAccuracyProblem,
919                                ),
920                            )
921                            .await;
922                        let _ = self.initiate_logout("").await;
923                        return Ok(Flow::Stop);
924                    }
925                }
926            }
927            _ => {
928                let _ = self
929                    .send_reject(
930                        msg,
931                        &RejectError::with_tag(
932                            SessionRejectReason::RequiredTagMissing,
933                            tags::SENDING_TIME,
934                        ),
935                    )
936                    .await;
937                return Ok(Flow::Stop);
938            }
939        }
940
941        // CompIDs must be the mirror image of ours.
942        if self.cfg.check_comp_id {
943            let sender = msg.header.get_raw(tags::SENDER_COMP_ID).unwrap_or_default();
944            let target = msg.header.get_raw(tags::TARGET_COMP_ID).unwrap_or_default();
945            if sender != self.cfg.session_id.target_comp_id.as_bytes()
946                || target != self.cfg.session_id.sender_comp_id.as_bytes()
947            {
948                let _ = self
949                    .send_reject(msg, &RejectError::new(SessionRejectReason::CompIDProblem))
950                    .await;
951                let _ = self.initiate_logout("").await;
952                return Ok(Flow::Stop);
953            }
954        }
955
956        // Sequence number checks.
957        if check_too_high || check_too_low {
958            let Ok(seq) = msg.seq_num() else {
959                let _ = self
960                    .send_reject(
961                        msg,
962                        &RejectError::with_tag(
963                            SessionRejectReason::RequiredTagMissing,
964                            tags::MSG_SEQ_NUM,
965                        ),
966                    )
967                    .await;
968                return Ok(Flow::Stop);
969            };
970            let expected = self.store.next_target_seq_num();
971            if check_too_high && seq > expected {
972                self.on_target_too_high(msg, seq, expected).await?;
973                return Ok(Flow::Stop);
974            }
975            if check_too_low && seq < expected {
976                // A too-low SequenceReset-GapFill straight after a stash
977                // replay is obeyed anyway (QuickFIX/n issue #309).
978                let obey_anyway = self.last_processed_was_queued
979                    && mt == msg_type::SEQUENCE_RESET
980                    && msg.body.get_raw(tags::GAP_FILL_FLAG) == Some(b"Y");
981                if obey_anyway {
982                    self.event(&format!(
983                        "SequenceReset-GapFill {seq} is too low (expected {expected}), obeying it anyway"
984                    ));
985                } else {
986                    return self.on_target_too_low(msg, seq, expected).await;
987                }
988            }
989        }
990
991        self.last_received = Instant::now();
992        self.test_request_counter = 0;
993
994        // Application callbacks.
995        let cb = if msg.is_admin() {
996            self.app.from_admin(msg, &self.cfg.session_id).await
997        } else {
998            self.app.from_app(msg, &self.cfg.session_id).await
999        };
1000        match cb {
1001            Ok(()) => Ok(Flow::Continue),
1002            Err(ApplicationError::RejectLogon(reason)) => {
1003                let _ = self.store.incr_next_target_seq_num().await;
1004                let _ = self.initiate_logout(&reason).await;
1005                Err(Disconnect(format!("logon rejected by application: {reason}")))
1006            }
1007            Err(ApplicationError::Reject(rej)) => {
1008                let _ = self.send_reject(msg, &rej).await;
1009                Ok(Flow::Stop)
1010            }
1011            Err(ApplicationError::UnsupportedMessageType) => {
1012                let _ = self.send_business_reject(msg).await;
1013                Ok(Flow::Stop)
1014            }
1015        }
1016    }
1017
1018    /// Which message types are acceptable in the current logon state
1019    /// (C++ `validLogonState`).
1020    fn valid_logon_state(&self, mt: &str) -> bool {
1021        (mt == msg_type::LOGON && (self.sent_reset || self.received_reset))
1022            || (mt == msg_type::LOGON && !self.received_logon)
1023            || (mt != msg_type::LOGON && self.received_logon)
1024            || (mt == msg_type::LOGOUT && self.sent_logon)
1025            || (mt != msg_type::LOGOUT && self.sent_logout)
1026            || mt == msg_type::SEQUENCE_RESET
1027            || mt == msg_type::REJECT
1028    }
1029
1030    async fn on_target_too_high(
1031        &mut self,
1032        msg: &Message,
1033        seq: u64,
1034        expected: u64,
1035    ) -> std::result::Result<(), Disconnect> {
1036        self.event(&format!("MsgSeqNum too high, expecting {expected} but received {seq}"));
1037        // Stash for replay once the gap fills.
1038        self.stash.insert(seq, Bytes::from(msg.to_bytes()));
1039        let suppress = self.resend_range.is_some() && !self.cfg.send_redundant_resend_requests;
1040        if !suppress {
1041            self.send_resend_request(expected, seq)
1042                .await
1043                .map_err(|e| Disconnect(format!("failed to send ResendRequest: {e}")))?;
1044        }
1045        Ok(())
1046    }
1047
1048    async fn on_target_too_low(&mut self, msg: &Message, seq: u64, expected: u64) -> Handling {
1049        if !msg.poss_dup() {
1050            let reason =
1051                format!("MsgSeqNum too low, expecting {expected} but received {seq}");
1052            let _ = self.initiate_logout(&reason).await;
1053            return Err(Disconnect(reason));
1054        }
1055        // PossDup replay of something we already have: validate and drop.
1056        let mt = msg.msg_type().unwrap_or_default();
1057        if mt != msg_type::SEQUENCE_RESET && self.cfg.requires_orig_sending_time {
1058            match msg.header.get_opt::<UtcTimestamp>(tags::ORIG_SENDING_TIME) {
1059                Ok(Some(orig)) => {
1060                    let sending = msg.header.get_opt::<UtcTimestamp>(tags::SENDING_TIME).ok().flatten();
1061                    if let Some(st) = sending {
1062                        if orig.time > st.time {
1063                            let _ = self
1064                                .send_reject(
1065                                    msg,
1066                                    &RejectError::new(
1067                                        SessionRejectReason::SendingTimeAccuracyProblem,
1068                                    ),
1069                                )
1070                                .await;
1071                            let _ = self.initiate_logout("").await;
1072                            return Ok(Flow::Stop);
1073                        }
1074                    }
1075                }
1076                _ => {
1077                    let _ = self
1078                        .send_reject(
1079                            msg,
1080                            &RejectError::with_tag(
1081                                SessionRejectReason::RequiredTagMissing,
1082                                tags::ORIG_SENDING_TIME,
1083                            ),
1084                        )
1085                        .await;
1086                    return Ok(Flow::Stop);
1087                }
1088            }
1089        }
1090        self.event(&format!("Already received message {seq}, ignoring PossDup"));
1091        Ok(Flow::Stop)
1092    }
1093
1094    // ----- handlers -----
1095
1096    async fn handle_logon(&mut self, msg: &Message) -> std::result::Result<(), Disconnect> {
1097        // A logon with a bad SendingTime is dropped with a silent disconnect
1098        // (no Reject/Logout), per the reference engines.
1099        if self.cfg.check_latency {
1100            let good = msg
1101                .header
1102                .get_opt::<UtcTimestamp>(tags::SENDING_TIME)
1103                .ok()
1104                .flatten()
1105                .is_some_and(|st| {
1106                    (chrono::Utc::now() - st.time).abs().num_seconds().unsigned_abs()
1107                        <= self.cfg.max_latency.as_secs()
1108                });
1109            if !good {
1110                return Err(Disconnect("logon has bad sending time".into()));
1111            }
1112        }
1113        // ResetSeqNumFlag(141)=Y from the peer.
1114        if msg.body.get_raw(tags::RESET_SEQ_NUM_FLAG) == Some(b"Y") {
1115            self.received_reset = true;
1116            if !self.sent_reset {
1117                self.event("Logon contains ResetSeqNumFlag=Y, resetting sequence numbers to 1");
1118                self.store
1119                    .reset()
1120                    .await
1121                    .map_err(|e| Disconnect(format!("store reset failed: {e}")))?;
1122            }
1123        }
1124        // A logon response we never asked for.
1125        if self.is_initiator() && !self.sent_logon {
1126            return Err(Disconnect("received logon response before sending request".into()));
1127        }
1128        if !self.is_initiator() {
1129            if self.cfg.refresh_on_logon {
1130                let _ = self.store.refresh().await;
1131            }
1132            if self.cfg.reset_on_logon && !self.received_reset {
1133                let _ = self.store.reset().await;
1134            }
1135        }
1136
1137        match self.verify(msg, false, true).await? {
1138            Flow::Stop => return Ok(()),
1139            Flow::Continue => {}
1140        }
1141        self.received_logon = true;
1142
1143        // NextExpectedMsgSeqNum(789): the peer tells us the next seqnum it
1144        // expects from us. Read it before replying (C++ nextLogon).
1145        let mut retransmit_from: Option<u64> = None;
1146        if self.send_next_expected {
1147            if let Ok(Some(peer_789)) = msg.body.get_opt::<u64>(tags::NEXT_EXPECTED_MSG_SEQ_NUM) {
1148                let next_sender = self.store.next_sender_seq_num();
1149                if peer_789 > next_sender {
1150                    // The peer expects messages we never sent — unrecoverable.
1151                    let reason = format!(
1152                        "Tag 789 (NextExpectedMsgSeqNum) is higher than expected. Expected {next_sender}, Received {peer_789}"
1153                    );
1154                    let _ = self.initiate_logout(&reason).await;
1155                    return Err(Disconnect(reason));
1156                } else if peer_789 < next_sender {
1157                    // The peer is behind; retransmit the gap after logon.
1158                    retransmit_from = Some(peer_789);
1159                }
1160            }
1161        }
1162
1163        if !self.is_initiator() {
1164            let peer_hbi = msg.body.get_opt::<u64>(tags::HEART_BT_INT).ok().flatten();
1165            self.send_logon_reply(peer_hbi)
1166                .await
1167                .map_err(|e| Disconnect(format!("failed to send logon reply: {e}")))?;
1168        } else {
1169            self.event("Received logon response");
1170        }
1171        self.sent_reset = false;
1172        self.received_reset = false;
1173
1174        // Seqnum-too-high on the logon itself is handled after replying.
1175        let seq = msg.seq_num().map_err(|_| Disconnect("logon missing MsgSeqNum".into()))?;
1176        let expected = self.store.next_target_seq_num();
1177        let reset = msg.body.get_raw(tags::RESET_SEQ_NUM_FLAG) == Some(b"Y");
1178        if seq > expected {
1179            if self.send_next_expected && !reset {
1180                // In 789 mode we do not send a ResendRequest for the peer's
1181                // gap: we already told the peer (via our reply's 789) what we
1182                // expect, and rely on it to retransmit. Just stash and record
1183                // the range (C++ nextLogon).
1184                self.event(&format!(
1185                    "Expecting retransmits FROM: {expected} TO: {}",
1186                    seq - 1
1187                ));
1188                self.stash.insert(seq, Bytes::from(msg.to_bytes()));
1189                self.resend_range = Some((expected, seq - 1, seq - 1));
1190            } else {
1191                self.on_target_too_high(msg, seq, expected).await?;
1192            }
1193        } else {
1194            self.store
1195                .incr_next_target_seq_num()
1196                .await
1197                .map_err(|e| Disconnect(format!("store error: {e}")))?;
1198        }
1199
1200        if self.is_logged_on() {
1201            self.event("Logon successful");
1202            self.app.on_logon(&self.cfg.session_id).await;
1203        }
1204
1205        // Finally, replay the gap the peer is missing (its 789 was too low).
1206        if let Some(begin) = retransmit_from {
1207            let end = self.store.next_sender_seq_num() - 1;
1208            self.event(&format!(
1209                "Sending retransmits due to received NextExpectedMsgSeqNum too low. FROM: {begin} TO: {end}"
1210            ));
1211            self.answer_resend(begin, end).await;
1212        }
1213        Ok(())
1214    }
1215
1216    /// Heartbeat and Reject share the trivial handling: verify + increment.
1217    async fn handle_plain_admin(&mut self, msg: &Message) -> std::result::Result<(), Disconnect> {
1218        if let Flow::Continue = self.verify(msg, true, true).await? {
1219            self.incr_target().await?;
1220        }
1221        Ok(())
1222    }
1223
1224    async fn handle_test_request(&mut self, msg: &Message) -> std::result::Result<(), Disconnect> {
1225        if let Flow::Continue = self.verify(msg, true, true).await? {
1226            let mut hb = Message::with_type(msg_type::HEARTBEAT);
1227            if let Some(id) = msg.body.get_raw(tags::TEST_REQ_ID) {
1228                hb.body.set_raw(tags::TEST_REQ_ID, id.to_vec());
1229            }
1230            let _ = self.send_message(hb).await;
1231            self.incr_target().await?;
1232        }
1233        Ok(())
1234    }
1235
1236    async fn handle_resend_request(&mut self, msg: &Message) -> std::result::Result<(), Disconnect> {
1237        if let Flow::Stop = self.verify(msg, false, false).await? {
1238            return Ok(());
1239        }
1240        let begin: u64 = msg.body.get_opt(tags::BEGIN_SEQ_NO).ok().flatten().unwrap_or(1);
1241        let mut end: u64 = msg.body.get_opt(tags::END_SEQ_NO).ok().flatten().unwrap_or(0);
1242        let next_sender = self.store.next_sender_seq_num();
1243        if end == 0 || end == 999999 || end >= next_sender {
1244            end = next_sender - 1;
1245        }
1246        self.event(&format!("Received ResendRequest FROM: {begin} TO: {end}"));
1247        self.answer_resend(begin, end).await;
1248
1249        // The ResendRequest consumes its own seqnum only if it was in
1250        // sequence (it may itself arrive during a two-way gap).
1251        if let Ok(seq) = msg.seq_num() {
1252            if seq == self.store.next_target_seq_num() {
1253                self.incr_target().await?;
1254            }
1255        }
1256        Ok(())
1257    }
1258
1259    /// Answer a resend range `[begin, end]`: replay persisted messages, or
1260    /// gap-fill the whole range when persistence is off. Shared by the
1261    /// ResendRequest handler and the 789 retransmit-after-logon path.
1262    async fn answer_resend(&mut self, begin: u64, end: u64) {
1263        if begin > end {
1264            return;
1265        }
1266        if self.cfg.persist_messages {
1267            self.retransmit(begin, end).await;
1268        } else {
1269            self.send_gap_fill(begin, end + 1).await;
1270        }
1271    }
1272
1273    /// Replay stored messages `[begin, end]`: app messages re-sent with
1274    /// PossDupFlag=Y, admin messages and gaps folded into GapFills.
1275    async fn retransmit(&mut self, begin: u64, end: u64) {
1276        let stored: BTreeMap<u64, Vec<u8>> = match self.store.get_messages(begin, end).await {
1277            Ok(v) => v.into_iter().collect(),
1278            Err(e) => {
1279                self.event(&format!("Store error during resend: {e}"));
1280                return;
1281            }
1282        };
1283        let now = UtcTimestamp::new(chrono::Utc::now(), self.cfg.timestamp_precision);
1284        let mut gap_begin: Option<u64> = None;
1285        for seq in begin..=end {
1286            let resend = stored.get(&seq).and_then(|raw| Message::parse(raw, false).ok());
1287            match resend {
1288                Some(mut m) if !m.is_admin() => {
1289                    m.header.set(tags::POSS_DUP_FLAG, true);
1290                    if let Some(orig) = m.header.get_raw(tags::SENDING_TIME).map(|v| v.to_vec()) {
1291                        m.header.set_raw(tags::ORIG_SENDING_TIME, orig);
1292                    }
1293                    m.stamp_sending_time(now);
1294                    if self.app.to_app(&mut m, &self.cfg.session_id).await.is_err() {
1295                        gap_begin.get_or_insert(seq);
1296                        continue;
1297                    }
1298                    if let Some(gb) = gap_begin.take() {
1299                        self.send_gap_fill(gb, seq).await;
1300                    }
1301                    self.event(&format!("Resending message {seq}"));
1302                    let raw = m.to_bytes();
1303                    self.transmit(raw.into()).await;
1304                }
1305                // Admin messages are never resent; missing seqnums gap-fill.
1306                _ => {
1307                    gap_begin.get_or_insert(seq);
1308                }
1309            }
1310        }
1311        if let Some(gb) = gap_begin.take() {
1312            self.send_gap_fill(gb, end + 1).await;
1313        }
1314    }
1315
1316    async fn handle_sequence_reset(&mut self, msg: &Message) -> std::result::Result<(), Disconnect> {
1317        let gap_fill = msg.body.get_raw(tags::GAP_FILL_FLAG) == Some(b"Y");
1318        // Reset-Reset (GapFill=N) bypasses all seqnum checks.
1319        if let Flow::Stop = self.verify(msg, gap_fill, gap_fill).await? {
1320            return Ok(());
1321        }
1322        if let Ok(Some(new_seq)) = msg.body.get_opt::<u64>(tags::NEW_SEQ_NO) {
1323            let expected = self.store.next_target_seq_num();
1324            self.event(&format!("Received SequenceReset (GapFill={}) NewSeqNo={new_seq}",
1325                if gap_fill { "Y" } else { "N" }));
1326            if new_seq > expected {
1327                self.store
1328                    .set_next_target_seq_num(new_seq)
1329                    .await
1330                    .map_err(|e| Disconnect(format!("store error: {e}")))?;
1331                // Anything stashed below the new seqnum is superseded.
1332                self.stash.retain(|&k, _| k >= new_seq);
1333            } else if new_seq < expected {
1334                let _ = self
1335                    .send_reject(msg, &RejectError::new(SessionRejectReason::ValueIsIncorrect))
1336                    .await;
1337            }
1338        }
1339        Ok(())
1340    }
1341
1342    async fn handle_logout(&mut self, msg: &Message) -> std::result::Result<(), Disconnect> {
1343        if let Flow::Stop = self.verify(msg, false, false).await? {
1344            return Ok(());
1345        }
1346        if self.sent_logout {
1347            self.event("Received logout response");
1348        } else {
1349            self.event("Received logout request");
1350            let _ = self.initiate_logout("").await;
1351        }
1352        self.incr_target().await?;
1353        if self.cfg.reset_on_logout {
1354            let _ = self.store.reset().await;
1355        }
1356        Err(Disconnect("logout complete".into()))
1357    }
1358
1359    async fn handle_app_message(
1360        &mut self,
1361        msg: &Message,
1362        _raw: Bytes,
1363    ) -> std::result::Result<(), Disconnect> {
1364        if let Flow::Continue = self.verify(msg, true, true).await? {
1365            self.incr_target().await?;
1366        }
1367        Ok(())
1368    }
1369
1370    async fn incr_target(&mut self) -> std::result::Result<(), Disconnect> {
1371        self.store
1372            .incr_next_target_seq_num()
1373            .await
1374            .map_err(|e| Disconnect(format!("store error: {e}")))
1375    }
1376
1377    /// Replay stashed too-high messages that are now in sequence.
1378    async fn drain_stash(&mut self) -> std::result::Result<(), Disconnect> {
1379        loop {
1380            let expected = self.store.next_target_seq_num();
1381            let Some(raw) = self.stash.remove(&expected) else { return Ok(()) };
1382            self.event(&format!("Processing queued message: {expected}"));
1383            self.last_processed_was_queued = true;
1384            // Queued Logon/ResendRequest only advance the seqnum
1385            // (mirrors C++ nextQueued).
1386            if contains_field(&raw, b"35=A") || contains_field(&raw, b"35=2") {
1387                self.incr_target().await?;
1388                continue;
1389            }
1390            match Message::parse(&raw, false) {
1391                Ok(msg) => self.process(msg, raw).await?,
1392                Err(_) => self.incr_target().await?,
1393            }
1394        }
1395    }
1396}
1397
1398async fn recv_opt(rx: &mut Option<mpsc::Receiver<Bytes>>) -> Option<Bytes> {
1399    match rx {
1400        Some(rx) => rx.recv().await,
1401        None => std::future::pending().await,
1402    }
1403}
1404
1405fn mul(d: Duration, f: f64) -> Duration {
1406    Duration::from_secs_f64(d.as_secs_f64() * f)
1407}
1408
1409/// DefaultApplVerID(1137) rides on the wire as the ApplVerID enum, not the
1410/// BeginString-style name configured in settings.
1411fn appl_ver_id_enum(configured: &str) -> &str {
1412    match configured {
1413        "FIX.2.7" => "0",
1414        "FIX.3.0" => "1",
1415        "FIX.4.0" => "2",
1416        "FIX.4.1" => "3",
1417        "FIX.4.2" => "4",
1418        "FIX.4.3" => "5",
1419        "FIX.4.4" => "6",
1420        "FIX.5.0" => "7",
1421        "FIX.5.0SP1" => "8",
1422        "FIX.5.0SP2" => "9",
1423        other => other, // already an enum value
1424    }
1425}
1426
1427/// Copy routing fields from a received message into a reply, reversed:
1428/// OnBehalfOf(115/116/144) becomes DeliverTo(128/129/145) and vice versa.
1429/// Empty values are not propagated.
1430fn reverse_route(offender: &Message, reply: &mut Message, begin_string: &str) {
1431    const PAIRS: [(crate::message::Tag, crate::message::Tag); 6] = [
1432        (tags::ON_BEHALF_OF_COMP_ID, tags::DELIVER_TO_COMP_ID),
1433        (tags::ON_BEHALF_OF_SUB_ID, tags::DELIVER_TO_SUB_ID),
1434        (tags::ON_BEHALF_OF_LOCATION_ID, tags::DELIVER_TO_LOCATION_ID),
1435        (tags::DELIVER_TO_COMP_ID, tags::ON_BEHALF_OF_COMP_ID),
1436        (tags::DELIVER_TO_SUB_ID, tags::ON_BEHALF_OF_SUB_ID),
1437        (tags::DELIVER_TO_LOCATION_ID, tags::ON_BEHALF_OF_LOCATION_ID),
1438    ];
1439    // Location IDs (144/145) only exist from FIX 4.1 on.
1440    let has_location_ids = begin_string >= "FIX.4.1";
1441    for (src, dst) in PAIRS {
1442        if !has_location_ids
1443            && matches!(src, tags::ON_BEHALF_OF_LOCATION_ID | tags::DELIVER_TO_LOCATION_ID)
1444        {
1445            continue;
1446        }
1447        if let Some(v) = offender.header.get_raw(src) {
1448            if !v.is_empty() {
1449                reply.header.set_raw(dst, v.to_vec());
1450            }
1451        }
1452    }
1453}
1454
1455/// Whether `raw` contains `<SOH>field` (or starts with `field`).
1456fn contains_field(raw: &[u8], field: &[u8]) -> bool {
1457    if raw.starts_with(field) {
1458        return true;
1459    }
1460    let mut needle = Vec::with_capacity(field.len() + 1);
1461    needle.push(crate::message::SOH);
1462    needle.extend_from_slice(field);
1463    raw.windows(needle.len()).any(|w| w == needle)
1464}