Skip to main content

quickfix_tokio/
application.rs

1//! The application callback interface — the async equivalent of the seven
2//! classic QuickFIX callbacks.
3
4use async_trait::async_trait;
5use tokio::sync::mpsc;
6
7use crate::error::RejectError;
8use crate::message::Message;
9use crate::session_id::SessionId;
10
11/// Returned by `from_admin` / `from_app` to influence session behavior.
12#[derive(Debug)]
13pub enum ApplicationError {
14    /// Veto a counterparty Logon: the session logs out and disconnects.
15    RejectLogon(String),
16    /// Emit a session-level Reject (35=3) for this message.
17    Reject(RejectError),
18    /// Emit a BusinessMessageReject (35=j) with BusinessRejectReason(380)=3.
19    UnsupportedMessageType,
20}
21
22/// Returned by `to_app` to veto an outgoing (or resent) message.
23#[derive(Debug)]
24pub struct DoNotSend;
25
26/// Implemented by users of the engine. All callbacks run on the session's
27/// task: a slow callback delays that session (only), exactly like the
28/// single-threaded session model of the reference engines.
29///
30/// Because callbacks run on the session task, do not `await` a
31/// [`crate::SessionHandle`] operation for the *same* session inside a
32/// callback — forward work to another task instead (see the executor
33/// example).
34#[async_trait]
35#[allow(clippy::wrong_self_convention)] // from_admin/from_app are the canonical QuickFIX names
36pub trait Application: Send + Sync + 'static {
37    /// A session was created at engine start.
38    async fn on_create(&self, _session_id: &SessionId) {}
39
40    /// The session completed a logon exchange.
41    async fn on_logon(&self, _session_id: &SessionId) {}
42
43    /// The session went offline (logout or disconnect).
44    async fn on_logout(&self, _session_id: &SessionId) {}
45
46    /// An admin message is about to be sent; last chance to mutate it
47    /// (e.g. add credentials to a Logon).
48    async fn to_admin(&self, _msg: &mut Message, _session_id: &SessionId) {}
49
50    /// An application message is about to be sent (also called for resends,
51    /// with PossDupFlag=Y). Return `Err(DoNotSend)` to suppress it — a resend
52    /// is then gap-filled.
53    async fn to_app(
54        &self,
55        _msg: &mut Message,
56        _session_id: &SessionId,
57    ) -> Result<(), DoNotSend> {
58        Ok(())
59    }
60
61    /// An admin message was received and verified.
62    async fn from_admin(
63        &self,
64        _msg: &Message,
65        _session_id: &SessionId,
66    ) -> Result<(), ApplicationError> {
67        Ok(())
68    }
69
70    /// An application message was received and verified. This is the main
71    /// inbound entry point for business messages.
72    async fn from_app(
73        &self,
74        _msg: &Message,
75        _session_id: &SessionId,
76    ) -> Result<(), ApplicationError> {
77        Ok(())
78    }
79}
80
81// ----- channel adapter -----
82
83/// A protocol event surfaced by [`event_channel`]. Every variant carries the
84/// [`SessionId`] it happened on, so one receiver can serve many sessions —
85/// match on the id to demultiplex.
86#[derive(Debug)]
87pub enum SessionEvent {
88    /// A session was created at engine start ([`Application::on_create`]).
89    Created(SessionId),
90    /// A logon exchange completed ([`Application::on_logon`]).
91    LoggedOn(SessionId),
92    /// The session went offline ([`Application::on_logout`]).
93    LoggedOut(SessionId),
94    /// An application (business) message arrived ([`Application::from_app`]).
95    App(Message, SessionId),
96    /// An admin message arrived ([`Application::from_admin`]) — Heartbeat,
97    /// TestRequest, Reject, etc. Usually ignored; here for observability.
98    Admin(Message, SessionId),
99}
100
101/// The [`Application`] half of [`event_channel`]: it forwards the
102/// *notification* callbacks onto the channel and accepts everything else with
103/// the trait defaults. Construct it only via [`event_channel`].
104pub struct ChannelApplication {
105    tx: mpsc::UnboundedSender<SessionEvent>,
106}
107
108/// Bridge the callback interface to a tokio channel: returns an
109/// [`Application`] to hand to [`crate::Engine::start`], plus a receiver you
110/// drain from your own task or `select!` loop. This is the tokio-native
111/// alternative to implementing [`Application`] by hand — inbound events and
112/// your outbound [`crate::SessionHandle`] sends live in one place, with no
113/// callback reentrancy hazard.
114///
115/// The channel is **unbounded on purpose**: pushing an event never blocks the
116/// session task, so a slow consumer never stalls the protocol (heartbeats,
117/// gap-fills). The cost is that a consumer which stops draining entirely will
118/// grow memory — that's a consumer bug, since inbound rate is paced by one
119/// socket.
120///
121/// This adapter is **notify-only**. It cannot carry the *decision* hooks —
122/// [`Application::to_app`]/[`DoNotSend`], [`Application::from_admin`] ->
123/// [`ApplicationError::RejectLogon`], or [`Application::to_admin`] mutation —
124/// because those need a synchronous verdict the engine waits on, which a
125/// fire-and-forward channel has nowhere to return. Implement [`Application`]
126/// directly if you need to veto messages, reject logons, or stamp
127/// credentials.
128pub fn event_channel() -> (ChannelApplication, mpsc::UnboundedReceiver<SessionEvent>) {
129    let (tx, rx) = mpsc::unbounded_channel();
130    (ChannelApplication { tx }, rx)
131}
132
133#[async_trait]
134impl Application for ChannelApplication {
135    async fn on_create(&self, session_id: &SessionId) {
136        let _ = self.tx.send(SessionEvent::Created(session_id.clone()));
137    }
138    async fn on_logon(&self, session_id: &SessionId) {
139        let _ = self.tx.send(SessionEvent::LoggedOn(session_id.clone()));
140    }
141    async fn on_logout(&self, session_id: &SessionId) {
142        let _ = self.tx.send(SessionEvent::LoggedOut(session_id.clone()));
143    }
144    async fn from_admin(
145        &self,
146        msg: &Message,
147        session_id: &SessionId,
148    ) -> Result<(), ApplicationError> {
149        let _ = self.tx.send(SessionEvent::Admin(msg.clone(), session_id.clone()));
150        Ok(())
151    }
152    async fn from_app(
153        &self,
154        msg: &Message,
155        session_id: &SessionId,
156    ) -> Result<(), ApplicationError> {
157        let _ = self.tx.send(SessionEvent::App(msg.clone(), session_id.clone()));
158        Ok(())
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[tokio::test]
167    async fn event_channel_forwards_notifications() {
168        let (app, mut rx) = event_channel();
169        let sid = SessionId::new("FIX.4.4", "ME", "YOU");
170
171        app.on_logon(&sid).await;
172        let mut msg = Message::default();
173        msg.set(35, "D"); // NewOrderSingle
174        app.from_app(&msg, &sid).await.unwrap();
175        app.on_logout(&sid).await;
176
177        assert!(matches!(rx.recv().await, Some(SessionEvent::LoggedOn(id)) if id == sid));
178        assert!(matches!(rx.recv().await, Some(SessionEvent::App(_, id)) if id == sid));
179        assert!(matches!(rx.recv().await, Some(SessionEvent::LoggedOut(id)) if id == sid));
180    }
181
182    #[tokio::test]
183    async fn send_never_blocks_and_survives_dropped_receiver() {
184        let (app, rx) = event_channel();
185        let sid = SessionId::new("FIX.4.4", "ME", "YOU");
186        drop(rx); // consumer went away
187        // Forwarding must not panic or block even with no receiver.
188        app.on_logon(&sid).await;
189        app.from_app(&Message::default(), &sid).await.unwrap();
190    }
191}