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