|
| 1 | +use std::{ |
| 2 | + io, |
| 3 | + pin::Pin, |
| 4 | + sync::Arc, |
| 5 | + task::{Context, Poll}, |
| 6 | + time::Duration, |
| 7 | +}; |
| 8 | + |
| 9 | +use bytes::Bytes; |
| 10 | +use futures::{Future, FutureExt, SinkExt, StreamExt}; |
| 11 | +use msg_common::span::{EnterSpan as _, WithSpan}; |
| 12 | +use tokio_util::codec::Framed; |
| 13 | +use tracing::Instrument; |
| 14 | + |
| 15 | +use crate::{ConnectionState, ExponentialBackoff}; |
| 16 | + |
| 17 | +use msg_transport::{Address, MeteredIo, Transport}; |
| 18 | +use msg_wire::{auth, reqrep}; |
| 19 | + |
| 20 | +/// A connection task that connects to a server and returns the underlying IO object. |
| 21 | +type ConnTask<Io, Err> = Pin<Box<dyn Future<Output = Result<Io, Err>> + Send>>; |
| 22 | + |
| 23 | +/// A connection from the transport to a server. |
| 24 | +/// |
| 25 | +/// # Usage of Framed |
| 26 | +/// [`Framed`] is used for encoding and decoding messages ("frames"). |
| 27 | +/// Usually, [`Framed`] has its own internal buffering mechanism, that's respected |
| 28 | +/// when calling `poll_ready` and configured by [`Framed::set_backpressure_boundary`]. |
| 29 | +/// |
| 30 | +/// However, we don't use `poll_ready` here, and instead we flush every time we write a message to |
| 31 | +/// the framed buffer. |
| 32 | +pub(crate) type Conn<Io, S, A> = Framed<MeteredIo<Io, S, A>, reqrep::Codec>; |
| 33 | + |
| 34 | +/// A connection controller that manages the connection to a server with an exponential backoff. |
| 35 | +pub(crate) type ConnCtl<Io, S, A> = ConnectionState<Conn<Io, S, A>, ExponentialBackoff, A>; |
| 36 | + |
| 37 | +/// Manages the connection lifecycle: connecting, reconnecting, and maintaining the connection. |
| 38 | +pub(crate) struct ConnManager<T: Transport<A>, A: Address> { |
| 39 | + /// The connection task which handles the connection to the server. |
| 40 | + conn_task: Option<WithSpan<ConnTask<T::Io, T::Error>>>, |
| 41 | + /// The transport controller, wrapped in a [`ConnectionState`] for backoff. |
| 42 | + /// The [`Framed`] object can send and receive messages from the socket. |
| 43 | + conn_ctl: ConnCtl<T::Io, T::Stats, A>, |
| 44 | + /// The transport for this socket. |
| 45 | + transport: T, |
| 46 | + /// The address of the server. |
| 47 | + addr: A, |
| 48 | + /// Transport stats for metering IO. |
| 49 | + transport_stats: Arc<arc_swap::ArcSwap<T::Stats>>, |
| 50 | + /// Authentication token for the connection. |
| 51 | + auth_token: Option<Bytes>, |
| 52 | + |
| 53 | + /// A span to use for connection-related logging. |
| 54 | + span: tracing::Span, |
| 55 | +} |
| 56 | + |
| 57 | +/// Perform the authentication handshake with the server. |
| 58 | +#[tracing::instrument(skip_all, "auth", fields(token = ?token))] |
| 59 | +async fn authentication_handshake<T, A>(mut io: T::Io, token: Bytes) -> Result<T::Io, T::Error> |
| 60 | +where |
| 61 | + T: Transport<A>, |
| 62 | + A: Address, |
| 63 | +{ |
| 64 | + let mut conn = Framed::new(&mut io, auth::Codec::new_client()); |
| 65 | + |
| 66 | + conn.send(auth::Message::Auth(token)).await?; |
| 67 | + tracing::debug!("sent auth, waiting ack from server"); |
| 68 | + |
| 69 | + // Wait for the response |
| 70 | + let Some(res) = conn.next().await else { |
| 71 | + return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "connection closed").into()); |
| 72 | + }; |
| 73 | + |
| 74 | + match res { |
| 75 | + Ok(auth::Message::Ack) => { |
| 76 | + tracing::debug!("received ack"); |
| 77 | + Ok(io) |
| 78 | + } |
| 79 | + Ok(msg) => { |
| 80 | + tracing::error!(?msg, "unexpected ack result"); |
| 81 | + Err(io::Error::new(io::ErrorKind::PermissionDenied, "rejected").into()) |
| 82 | + } |
| 83 | + Err(e) => Err(io::Error::new(io::ErrorKind::PermissionDenied, e).into()), |
| 84 | + } |
| 85 | +} |
| 86 | + |
| 87 | +impl<T, A> ConnManager<T, A> |
| 88 | +where |
| 89 | + T: Transport<A>, |
| 90 | + A: Address, |
| 91 | +{ |
| 92 | + pub(crate) fn new( |
| 93 | + transport: T, |
| 94 | + addr: A, |
| 95 | + conn_ctl: ConnCtl<T::Io, T::Stats, A>, |
| 96 | + transport_stats: Arc<arc_swap::ArcSwap<T::Stats>>, |
| 97 | + auth_token: Option<Bytes>, |
| 98 | + span: tracing::Span, |
| 99 | + ) -> Self { |
| 100 | + Self { conn_task: None, conn_ctl, transport, addr, transport_stats, auth_token, span } |
| 101 | + } |
| 102 | + |
| 103 | + /// Start the connection task to the server, handling authentication if necessary. |
| 104 | + /// The result will be polled by the driver and re-tried according to the backoff policy. |
| 105 | + fn try_connect(&mut self) { |
| 106 | + let connect = self.transport.connect(self.addr.clone()); |
| 107 | + let token = self.auth_token.clone(); |
| 108 | + |
| 109 | + let task = async move { |
| 110 | + let io = connect.await?; |
| 111 | + |
| 112 | + let Some(token) = token else { |
| 113 | + return Ok(io); |
| 114 | + }; |
| 115 | + |
| 116 | + authentication_handshake::<T, A>(io, token).await |
| 117 | + } |
| 118 | + .in_current_span(); |
| 119 | + |
| 120 | + // FIX: coercion to BoxFuture for [`SpanExt::with_current_span`] |
| 121 | + self.conn_task = Some(WithSpan::current(Box::pin(task))); |
| 122 | + } |
| 123 | + |
| 124 | + /// Reset the connection state to inactive, so that it will be re-tried. |
| 125 | + /// This is done when the connection is closed or an error occurs. |
| 126 | + #[inline] |
| 127 | + pub(crate) fn reset_connection(&mut self) { |
| 128 | + self.conn_ctl = ConnectionState::Inactive { |
| 129 | + addr: self.addr.clone(), |
| 130 | + backoff: ExponentialBackoff::new(Duration::from_millis(20), 16), |
| 131 | + }; |
| 132 | + } |
| 133 | + |
| 134 | + /// Returns a mutable reference to the connection channel if it is active. |
| 135 | + #[inline] |
| 136 | + pub(crate) fn active_connection(&mut self) -> Option<&mut Conn<T::Io, T::Stats, A>> { |
| 137 | + if let ConnectionState::Active { ref mut channel } = self.conn_ctl { |
| 138 | + Some(channel) |
| 139 | + } else { |
| 140 | + None |
| 141 | + } |
| 142 | + } |
| 143 | + |
| 144 | + /// Poll connection management logic: connection task, backoff, and retry logic. |
| 145 | + /// Loops until the connection is active, then returns a mutable reference to the channel. |
| 146 | + /// |
| 147 | + /// Note: this is not a `Future` impl because we want to return a reference; doing it in |
| 148 | + /// a `Future` would require lifetime headaches or unsafe code. |
| 149 | + /// |
| 150 | + /// Returns: |
| 151 | + /// * `Poll::Ready(Some(&mut channel))` if the connection is active |
| 152 | + /// * `Poll::Ready(None)` if we should terminate (max retries exceeded) |
| 153 | + /// * `Poll::Pending` if we need to wait for backoff |
| 154 | + #[allow(clippy::type_complexity)] |
| 155 | + pub(crate) fn poll( |
| 156 | + &mut self, |
| 157 | + cx: &mut Context<'_>, |
| 158 | + ) -> Poll<Option<&mut Conn<T::Io, T::Stats, A>>> { |
| 159 | + loop { |
| 160 | + // Poll the active connection task, if any |
| 161 | + if let Some(ref mut conn_task) = self.conn_task { |
| 162 | + if let Poll::Ready(result) = conn_task.poll_unpin(cx).enter() { |
| 163 | + // As soon as the connection task finishes, set it to `None`. |
| 164 | + // - If it was successful, set the connection to active |
| 165 | + // - If it failed, it will be re-tried until the backoff limit is reached. |
| 166 | + self.conn_task = None; |
| 167 | + |
| 168 | + match result.inner { |
| 169 | + Ok(io) => { |
| 170 | + tracing::info!("connected"); |
| 171 | + |
| 172 | + let metered = MeteredIo::new(io, self.transport_stats.clone()); |
| 173 | + let framed = Framed::new(metered, reqrep::Codec::new()); |
| 174 | + self.conn_ctl = ConnectionState::Active { channel: framed }; |
| 175 | + } |
| 176 | + Err(e) => { |
| 177 | + tracing::error!(?e, "failed to connect"); |
| 178 | + } |
| 179 | + } |
| 180 | + } |
| 181 | + } |
| 182 | + |
| 183 | + // If the connection is inactive, try to connect to the server or poll the backoff |
| 184 | + // timer if we're already trying to connect. |
| 185 | + if let ConnectionState::Inactive { backoff, .. } = &mut self.conn_ctl { |
| 186 | + let Poll::Ready(item) = backoff.poll_next_unpin(cx) else { |
| 187 | + return Poll::Pending; |
| 188 | + }; |
| 189 | + |
| 190 | + let _span = tracing::info_span!(parent: &self.span, "connect").entered(); |
| 191 | + |
| 192 | + if let Some(duration) = item { |
| 193 | + if self.conn_task.is_none() { |
| 194 | + tracing::debug!(backoff = ?duration, "trying connection"); |
| 195 | + self.try_connect(); |
| 196 | + } else { |
| 197 | + tracing::debug!( |
| 198 | + backoff = ?duration, |
| 199 | + "not retrying as there is already a connection task" |
| 200 | + ); |
| 201 | + } |
| 202 | + } else { |
| 203 | + tracing::error!("exceeded maximum number of retries, terminating connection"); |
| 204 | + return Poll::Ready(None); |
| 205 | + } |
| 206 | + } |
| 207 | + |
| 208 | + if let ConnectionState::Active { ref mut channel } = self.conn_ctl { |
| 209 | + return Poll::Ready(Some(channel)); |
| 210 | + } |
| 211 | + } |
| 212 | + } |
| 213 | +} |
0 commit comments