Skip to main content

easyfix_session/
io.rs

1use std::{
2    any::Any,
3    cell::{Cell, RefCell},
4    collections::{HashMap, hash_map::Entry},
5    future::Future,
6    panic::AssertUnwindSafe,
7    rc::Rc,
8    sync::Mutex,
9    time::Duration,
10};
11
12use easyfix_messages::{
13    fields::{FixString, SessionStatus},
14    messages::{FixtMessage, Message},
15};
16use futures_util::{FutureExt, Stream, pin_mut};
17use tokio::{
18    self,
19    io::{AsyncRead, AsyncWrite, AsyncWriteExt},
20    net::TcpStream,
21    sync::{mpsc, oneshot},
22};
23use tokio_stream::StreamExt;
24use tracing::{Instrument, Span, debug, error, info, info_span, warn};
25
26use crate::{
27    DisconnectReason, Error, NO_INBOUND_TIMEOUT_PADDING, Sender, SessionError,
28    TEST_REQUEST_THRESHOLD,
29    acceptor::{ActiveSessionsMap, SessionsMap},
30    application::{Emitter, FixEventInternal},
31    messages_storage::MessagesStorage,
32    session::Session,
33    session_id::SessionId,
34    session_state::State,
35    settings::{SessionSettings, Settings},
36};
37
38mod input_stream;
39pub use input_stream::{InputEvent, InputStream, input_stream};
40
41mod output_stream;
42use output_stream::{OutputEvent, output_stream};
43
44pub mod time;
45use time::{timeout, timeout_at, timeout_stream};
46
47static SENDERS: Mutex<Option<HashMap<SessionId, Sender>>> = Mutex::new(None);
48
49pub fn register_sender(session_id: SessionId, sender: Sender) {
50    if let Entry::Vacant(entry) = SENDERS
51        .lock()
52        .unwrap()
53        .get_or_insert_with(HashMap::new)
54        .entry(session_id)
55    {
56        entry.insert(sender);
57    }
58}
59
60pub fn unregister_sender(session_id: &SessionId) {
61    if SENDERS
62        .lock()
63        .unwrap()
64        .get_or_insert_with(HashMap::new)
65        .remove(session_id)
66        .is_none()
67    {
68        // TODO: ERROR?
69    }
70}
71
72pub fn sender(session_id: &SessionId) -> Option<Sender> {
73    SENDERS
74        .lock()
75        .unwrap()
76        .get_or_insert_with(HashMap::new)
77        .get(session_id)
78        .cloned()
79}
80
81// TODO: Remove?
82pub fn send(session_id: &SessionId, msg: Box<Message>) -> Result<(), Box<Message>> {
83    if let Some(sender) = sender(session_id) {
84        sender.send(msg).map_err(|msg| msg.body)
85    } else {
86        Err(msg)
87    }
88}
89
90pub fn send_raw(msg: Box<FixtMessage>) -> Result<(), Box<FixtMessage>> {
91    if let Some(sender) = sender(&SessionId::from_input_msg(&msg)) {
92        sender.send_raw(msg)
93    } else {
94        Err(msg)
95    }
96}
97
98async fn first_msg(
99    stream: &mut (impl Stream<Item = InputEvent> + Unpin),
100    logon_timeout: Duration,
101) -> Result<Box<FixtMessage>, Error> {
102    match timeout(logon_timeout, stream.next()).await {
103        Ok(Some(InputEvent::Message(msg))) => Ok(msg),
104        Ok(Some(InputEvent::IoError(error))) => Err(error.into()),
105        Ok(Some(InputEvent::DeserializeError(error))) => {
106            error!("failed to deserialize first message: {error}");
107            Err(Error::SessionError(SessionError::LogonNeverReceived))
108        }
109        _ => Err(Error::SessionError(SessionError::LogonNeverReceived)),
110    }
111}
112
113#[derive(Debug)]
114struct Connection<S> {
115    session: Rc<Session<S>>,
116}
117
118/// Logout event parked by [`SessionCleanupGuard`] when the connection future
119/// panics, delivered by [`supervise_connection`] which - unlike `Drop` - can
120/// await on the events channel.
121pub(crate) type PendingLogout = Rc<RefCell<Option<(SessionId, DisconnectReason)>>>;
122
123fn panic_message(panic: &(dyn Any + Send)) -> &str {
124    if let Some(message) = panic.downcast_ref::<&str>() {
125        message
126    } else if let Some(message) = panic.downcast_ref::<String>() {
127        message
128    } else {
129        "<non-string panic payload>"
130    }
131}
132
133/// Runs a connection future, containing its panics.
134///
135/// A panic is caught at the future's `poll` boundary, so it never reaches the
136/// executor: one panicking session can't kill the whole process (not every
137/// executor isolates task panics) and the application receives the
138/// Logout event parked by [`SessionCleanupGuard`], delivered here with
139/// a regular, backpressure-aware `send`.
140pub(crate) async fn supervise_connection(
141    connection: impl Future<Output = ()>,
142    pending_logout: PendingLogout,
143    emitter: &Emitter,
144) {
145    // UnwindSafe: all state shared with the connection future lives behind
146    // Rc<RefCell<...>> and is brought back to a consistent state by
147    // `SessionCleanupGuard` during unwind, the same way a process restart
148    // would leave it.
149    if let Err(panic) = AssertUnwindSafe(connection).catch_unwind().await {
150        error!(
151            "connection task panicked: {}",
152            panic_message(panic.as_ref())
153        );
154        let parked_logout = pending_logout.borrow_mut().take();
155        if let Some((session_id, reason)) = parked_logout {
156            emitter
157                .send(FixEventInternal::Logout(session_id, reason))
158                .await;
159        }
160    }
161}
162
163/// Removes global registrations of a connection's session when the connection
164/// task finishes, **including when it panics**. Without this, a panicking task
165/// leaves the session in `active_sessions`/`SENDERS` forever: every reconnect
166/// attempt is rejected with "Session already active" and a later logout
167/// request panics on the closed output channel.
168struct SessionCleanupGuard<S: MessagesStorage> {
169    session_id: SessionId,
170    state: Rc<RefCell<State<S>>>,
171    active_sessions: Rc<RefCell<ActiveSessionsMap<S>>>,
172    pending_logout: PendingLogout,
173    reset_on_disconnect: bool,
174}
175
176impl<S: MessagesStorage> Drop for SessionCleanupGuard<S> {
177    fn drop(&mut self) {
178        unregister_sender(&self.session_id);
179
180        // try_borrow_mut: this can run during unwind, never panic here
181        match self.active_sessions.try_borrow_mut() {
182            Ok(mut active_sessions) => {
183                active_sessions.remove(&self.session_id);
184            }
185            Err(_) => error!(
186                session_id = %self.session_id,
187                "session cleanup failed: active sessions map already borrowed"
188            ),
189        }
190
191        match self.state.try_borrow_mut() {
192            Ok(mut state) => {
193                // Logon flags still set mean `emit_logout` in the output loop
194                // never ran, i.e. the task died without a proper teardown.
195                // Stale flags would reject the next logon with "Invalid logon
196                // state", and without the Logout event the application would
197                // never learn that the connection is gone.
198                let logout_not_emitted = state.logon_received() || state.logon_sent();
199                state.set_logon_received(false);
200                state.set_logon_sent(false);
201                if !state.disconnected() {
202                    warn!(
203                        session_id = %self.session_id,
204                        "connection task finished without disconnecting, forcing disconnected state"
205                    );
206                    state.disconnect(self.reset_on_disconnect);
207                }
208                if logout_not_emitted {
209                    // Can't await in Drop - park the event for
210                    // `supervise_connection` to deliver.
211                    *self.pending_logout.borrow_mut() =
212                        Some((self.session_id.clone(), DisconnectReason::Disconnected));
213                }
214            }
215            Err(_) => error!(
216                session_id = %self.session_id,
217                "session cleanup failed: session state already borrowed"
218            ),
219        }
220    }
221}
222
223#[expect(clippy::too_many_arguments)]
224pub(crate) async fn acceptor_connection<S>(
225    reader: impl AsyncRead + Unpin,
226    writer: impl AsyncWrite + Unpin,
227    settings: Settings,
228    sessions: Rc<RefCell<SessionsMap<S>>>,
229    active_sessions: Rc<RefCell<ActiveSessionsMap<S>>>,
230    emitter: Emitter,
231    enabled: Rc<Cell<bool>>,
232    pending_logout: PendingLogout,
233) where
234    S: MessagesStorage,
235{
236    let stream = input_stream(reader);
237    let logon_timeout =
238        settings.auto_disconnect_after_no_logon_received + NO_INBOUND_TIMEOUT_PADDING;
239    pin_mut!(stream);
240    let msg = match first_msg(&mut stream, logon_timeout).await {
241        Ok(msg) => msg,
242        Err(err) => {
243            error!(%err, "failed to establish new session");
244            return;
245        }
246    };
247
248    let session_id = SessionId::from_input_msg(&msg);
249    debug!(first_msg = ?msg);
250
251    // XXX: there should be no await point between active_sessions.insert below
252    if !enabled.get() {
253        warn!("Acceptor is disabled, drop connection");
254        return;
255    }
256
257    let (sender, receiver) = mpsc::unbounded_channel();
258    let sender = Sender::new(sender);
259
260    let Some((session_settings, session_state)) = sessions.borrow().get_session(&session_id) else {
261        error!(%session_id, "failed to establish new session: unknown session id");
262        return;
263    };
264    if !session_state.borrow_mut().disconnected()
265        || active_sessions.borrow().contains_key(&session_id)
266    {
267        error!(%session_id, "Session already active");
268        return;
269    }
270    session_state.borrow_mut().set_disconnected(false);
271    register_sender(session_id.clone(), sender.clone());
272
273    let _cleanup_guard = SessionCleanupGuard {
274        session_id: session_id.clone(),
275        state: session_state.clone(),
276        active_sessions: active_sessions.clone(),
277        pending_logout,
278        reset_on_disconnect: session_settings.reset_on_disconnect,
279    };
280
281    let (disconnect_tx, disconnect_rx) = oneshot::channel();
282
283    let session = Rc::new(Session::new(
284        settings,
285        session_settings,
286        session_state,
287        sender,
288        emitter.clone(),
289        disconnect_tx,
290    ));
291
292    active_sessions
293        .borrow_mut()
294        .insert(session_id.clone(), session.clone());
295
296    let session_span = info_span!(
297        parent: None,
298        "session",
299        id = %session_id
300    );
301    session_span.follows_from(Span::current());
302
303    let input_loop_span = info_span!(parent: &session_span, "in");
304    let output_loop_span = info_span!(parent: &session_span, "out");
305
306    let force_disconnection_with_reason = session
307        .on_message_in(msg)
308        .instrument(input_loop_span.clone())
309        .await;
310
311    // TODO: Not here!, send this event when SessionState is created!
312    emitter
313        .send(FixEventInternal::Created(session_id.clone()))
314        .await;
315
316    let input_timeout_duration = session.heartbeat_interval().mul_f32(TEST_REQUEST_THRESHOLD);
317    let input_stream = timeout_stream(input_timeout_duration, stream)
318        .map(|res| res.unwrap_or(InputEvent::Timeout));
319    pin_mut!(input_stream);
320
321    let output_stream = output_stream(session.clone(), session.heartbeat_interval(), receiver);
322    pin_mut!(output_stream);
323
324    let connection = Connection::new(session);
325    let (input_closed_tx, input_closed_rx) = oneshot::channel();
326
327    tokio::join!(
328        connection
329            .input_loop(
330                input_stream,
331                input_closed_tx,
332                force_disconnection_with_reason,
333                disconnect_rx,
334            )
335            .instrument(input_loop_span),
336        connection
337            .output_loop(writer, output_stream, input_closed_rx)
338            .instrument(output_loop_span),
339    );
340    session_span.in_scope(|| {
341        info!("connection closed");
342    });
343}
344
345pub(crate) async fn initiator_connection<S>(
346    tcp_stream: TcpStream,
347    settings: Settings,
348    session_settings: SessionSettings,
349    state: Rc<RefCell<State<S>>>,
350    active_sessions: Rc<RefCell<ActiveSessionsMap<S>>>,
351    emitter: Emitter,
352    pending_logout: PendingLogout,
353) where
354    S: MessagesStorage,
355{
356    let (source, sink) = tcp_stream.into_split();
357    state.borrow_mut().set_disconnected(false);
358    let session_id = session_settings.session_id.clone();
359
360    let (sender, receiver) = mpsc::unbounded_channel();
361    let sender = Sender::new(sender);
362
363    let (disconnect_tx, disconnect_rx) = oneshot::channel();
364
365    register_sender(session_id.clone(), sender.clone());
366
367    let _cleanup_guard = SessionCleanupGuard {
368        session_id: session_id.clone(),
369        state: state.clone(),
370        active_sessions: active_sessions.clone(),
371        pending_logout,
372        reset_on_disconnect: session_settings.reset_on_disconnect,
373    };
374
375    let session = Rc::new(Session::new(
376        settings,
377        session_settings,
378        state,
379        sender,
380        emitter.clone(),
381        disconnect_tx,
382    ));
383    active_sessions
384        .borrow_mut()
385        .insert(session_id.clone(), session.clone());
386
387    let session_span = info_span!(
388        "session",
389        id = %session_id
390    );
391
392    let input_loop_span = info_span!(parent: &session_span, "in");
393    let output_loop_span = info_span!(parent: &session_span, "out");
394
395    // TODO: Not here!, send this event when SessionState is created!
396    emitter
397        .send(FixEventInternal::Created(session_id.clone()))
398        .await;
399
400    let input_timeout_duration = session.heartbeat_interval().mul_f32(TEST_REQUEST_THRESHOLD);
401    let input_stream = timeout_stream(input_timeout_duration, input_stream(source))
402        .map(|res| res.unwrap_or(InputEvent::Timeout));
403    pin_mut!(input_stream);
404
405    let output_stream = output_stream(session.clone(), session.heartbeat_interval(), receiver);
406    pin_mut!(output_stream);
407
408    // TODO: It's not so simple, add check if session time is within range,
409    //       if not schedule timer to send logon at proper time
410    session.send_logon_request(&mut session.state().borrow_mut());
411
412    let connection = Connection::new(session);
413    let (input_closed_tx, input_closed_rx) = oneshot::channel();
414
415    tokio::join!(
416        connection
417            .input_loop(input_stream, input_closed_tx, None, disconnect_rx)
418            .instrument(input_loop_span),
419        connection
420            .output_loop(sink, output_stream, input_closed_rx)
421            .instrument(output_loop_span),
422    );
423    info!("connection closed");
424}
425
426impl<S: MessagesStorage> Connection<S> {
427    fn new(session: Rc<Session<S>>) -> Connection<S> {
428        Connection { session }
429    }
430
431    async fn input_loop(
432        &self,
433        mut input_stream: impl Stream<Item = InputEvent> + Unpin,
434        input_closed_tx: oneshot::Sender<()>,
435        force_disconnection_with_reason: Option<DisconnectReason>,
436        mut disconnect_rx: oneshot::Receiver<()>,
437    ) {
438        if let Some(disconnect_reason) = force_disconnection_with_reason {
439            self.session
440                .disconnect(&mut self.session.state().borrow_mut(), disconnect_reason);
441
442            // Notify output loop that all input is processed so output queue can
443            // be safely closed.
444            // See `fn send()` and `fn send_raw()` from session.rs.
445            input_closed_tx
446                .send(())
447                .expect("Failed to notify about closed inpuot");
448
449            return;
450        }
451
452        let mut disconnect_reason = DisconnectReason::Disconnected;
453        let mut logout_deadline = None;
454
455        let mut next_item = async || {
456            if logout_deadline.is_none() {
457                logout_deadline = self.session.logout_deadline();
458            }
459            if let Some(logout_deadline) = logout_deadline {
460                timeout_at(logout_deadline, input_stream.next())
461                    .await
462                    .unwrap_or(Some(InputEvent::LogoutTimeout))
463            } else {
464                input_stream.next().await
465            }
466        };
467
468        loop {
469            let event = tokio::select! {
470                // Wait for network input
471                event = next_item() => {
472                    if let Some(event) = event {
473                        // Don't process event here, it won't be cancel-safe
474                        event
475                    } else {
476                        break
477                    }
478                }
479
480                // Wait for disconnect signal from Session::disconnect()
481                _ = &mut disconnect_rx => {
482                    info!("Disconnect signaled, exiting input loop");
483                    disconnect_reason = DisconnectReason::ApplicationForcedDisconnect;
484                    break;
485                }
486            };
487
488            // Don't accept new messages if session is disconnected.
489            if self.session.state().borrow().disconnected() {
490                info!("session disconnected, exit input processing");
491                // Notify output loop that all input is processed so output queue can
492                // be safely closed.
493                // See `fn send()` and `fn send_raw()` from session.rs.
494                input_closed_tx
495                    .send(())
496                    .expect("Failed to notify about closed input");
497                return;
498            }
499
500            match event {
501                InputEvent::Message(msg) => {
502                    if let Some(reason) = self.session.on_message_in(msg).await {
503                        info!(?reason, "disconnect, exit input processing");
504                        disconnect_reason = reason;
505                        break;
506                    }
507                }
508                InputEvent::DeserializeError(error) => {
509                    if let Some(reason) = self.session.on_deserialize_error(error).await {
510                        info!(?reason, "disconnect, exit input processing");
511                        disconnect_reason = reason;
512                        break;
513                    }
514                }
515                InputEvent::IoError(error) => {
516                    error!(%error, "Input error");
517                    disconnect_reason = DisconnectReason::IoError;
518                    break;
519                }
520                InputEvent::Timeout => {
521                    if self.session.on_in_timeout().await {
522                        self.session.send_logout(
523                            &mut self.session.state().borrow_mut(),
524                            Some(SessionStatus::SessionLogoutComplete),
525                            Some(FixString::from_ascii_lossy(
526                                b"Grace period is over".to_vec(),
527                            )),
528                        );
529                        break;
530                    }
531                }
532                InputEvent::LogoutTimeout => {
533                    info!("Logout timeout");
534                    disconnect_reason = DisconnectReason::LogoutTimeout;
535                    break;
536                }
537            }
538        }
539        self.session
540            .disconnect(&mut self.session.state().borrow_mut(), disconnect_reason);
541
542        // Notify output loop that all input is processed so output queue can
543        // be safely closed.
544        // See `fn send()` and `fn send_raw()` from session.rs.
545        input_closed_tx
546            .send(())
547            .expect("Failed to notify about closed inpout");
548    }
549
550    async fn output_loop(
551        &self,
552        mut sink: impl AsyncWrite + Unpin,
553        mut output_stream: impl Stream<Item = OutputEvent> + Unpin,
554        input_closed_rx: oneshot::Receiver<()>,
555    ) {
556        let mut sink_closed = false;
557        let mut disconnect_reason = DisconnectReason::Disconnected;
558        while let Some(event) = output_stream.next().await {
559            match event {
560                OutputEvent::Message(msg) => {
561                    if sink_closed {
562                        // Sink is closed - ignore message, but do not break
563                        // the loop. Output stream has to process all enqueued
564                        // messages to made them available
565                        // for ResendRequest<2>.
566                        info!("Client disconnected, message will be stored for further resend");
567                    } else if let Err(error) = sink.write_all(&msg).await {
568                        sink_closed = true;
569                        error!(%error, "Output write error");
570                        // XXX: Don't disconnect now. If IO error happened
571                        //      here, it will aslo happen in input loop
572                        //      and input loop will trigger disconnection.
573                        //      Disonnection from here would lead to message
574                        //      loss when output queue would be closed
575                        //      and input handler would try to send something.
576                        //
577                        // self.session.disconnect(
578                        //     &mut self.session.state().borrow_mut(),
579                        //     DisconnectReason::IoError,
580                        // );
581                    }
582                }
583                OutputEvent::Timeout => self.session.on_out_timeout().await,
584                OutputEvent::Disconnect(reason) => {
585                    // Internal channel is closed in output stream
586                    // inplementation, at this point no new messages
587                    // can be send.
588                    info!("Client disconnected");
589                    if !sink_closed && let Err(error) = sink.flush().await {
590                        error!(%error, "final flush failed");
591                    }
592                    disconnect_reason = reason;
593                }
594            }
595        }
596        // XXX: Emit logout here instead of Session::disconnect, so `Logout`
597        //      event will be delivered after Logout message instead of
598        //      randomly before or after.
599        self.session.emit_logout(disconnect_reason).await;
600
601        // Don't wait for any specific value it's just notification that
602        // input_loop finished, so no more messages can be added to output
603        // queue.
604        let _ = input_closed_rx.await;
605        if let Err(error) = sink.shutdown().await {
606            error!(%error, "connection shutdown failed")
607        }
608        info!("disconnect, exit output processing");
609    }
610}