Skip to content

Commit 147515d

Browse files
committed
chore(socket): extrac ReqDriver connection manager
1 parent 7e704e7 commit 147515d

4 files changed

Lines changed: 249 additions & 163 deletions

File tree

msg-socket/src/req/conn_manager.rs

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
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

Comments
 (0)