Initial version of the reconnecting tcp client

This commit is contained in:
Jedrzej Stuczynski
2020-02-25 15:54:50 +00:00
parent f599502eab
commit 9b81c2eb68
5 changed files with 323 additions and 161 deletions
@@ -0,0 +1,95 @@
use crate::connection_manager::reconnector::ConnectionReconnector;
use crate::connection_manager::writer::ConnectionWriter;
use futures::task::Poll;
use futures::AsyncWriteExt;
use log::*;
use std::io;
use std::net::SocketAddr;
use std::time::Duration;
mod reconnector;
mod writer;
enum ConnectionState {
Writing(ConnectionWriter),
Reconnecting(ConnectionReconnector),
}
pub(crate) struct ConnectionManager {
address: SocketAddr,
maximum_reconnection_backoff: Duration,
reconnection_backoff: Duration,
state: ConnectionState,
}
impl ConnectionManager {
pub(crate) async fn new(
address: SocketAddr,
reconnection_backoff: Duration,
maximum_reconnection_backoff: Duration,
) -> Self {
// based on initial connection we will either have a writer or a reconnector
let state = match tokio::net::TcpStream::connect(address).await {
Ok(conn) => ConnectionState::Writing(ConnectionWriter::new(conn)),
Err(e) => {
warn!(
"failed to establish initial connection to {} ({}). Going into reconnection mode",
address, e
);
ConnectionState::Reconnecting(ConnectionReconnector::new(
address,
reconnection_backoff,
maximum_reconnection_backoff,
))
}
};
ConnectionManager {
address,
maximum_reconnection_backoff,
reconnection_backoff,
state,
}
}
pub(crate) async fn send(&mut self, msg: &[u8]) -> io::Result<()> {
if let ConnectionState::Reconnecting(conn_reconnector) = &mut self.state {
// do a single poll rather than await for future to completely resolve
// TODO: if we call poll ourselves here, will the Waker still call it itself later on?
let new_connection = match futures::poll!(conn_reconnector) {
Poll::Pending => {
return Err(io::Error::new(
io::ErrorKind::BrokenPipe,
"connection is broken - reconnection is in progress",
))
}
Poll::Ready(conn) => conn,
};
println!("We managed to reconnect!");
self.state = ConnectionState::Writing(ConnectionWriter::new(new_connection));
}
// we must be in writing state if we are here, either by being here from beginning or just
// transitioning from reconnecting
if let ConnectionState::Writing(conn_writer) = &mut self.state {
return match conn_writer.write_all(msg).await {
// if we failed to write to connection we should reconnect
// TODO: is this true? can we fail to write to a connection while it still remains open and valid?
Ok(res) => Ok(res),
Err(e) => {
println!("Creating connection reconnector!");
self.state = ConnectionState::Reconnecting(ConnectionReconnector::new(
self.address,
self.reconnection_backoff,
self.maximum_reconnection_backoff,
));
Err(e)
}
};
};
unreachable!()
}
}
@@ -0,0 +1,76 @@
use log::*;
use std::future::Future;
use std::io;
use std::net::SocketAddr;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Duration;
pub(crate) struct ConnectionReconnector {
address: SocketAddr,
connection: Pin<Box<dyn Future<Output = io::Result<tokio::net::TcpStream>>>>,
current_retry_attempt: u32,
current_backoff_delay: tokio::time::Delay,
maximum_reconnection_backoff: Duration,
reconnection_backoff: Duration,
}
impl ConnectionReconnector {
pub(crate) fn new(
address: SocketAddr,
reconnection_backoff: Duration,
maximum_reconnection_backoff: Duration,
) -> Self {
ConnectionReconnector {
address,
connection: Box::pin(tokio::net::TcpStream::connect(address)),
current_backoff_delay: tokio::time::delay_for(Duration::new(0, 0)), // if we can re-establish connection on first try without any backoff that's perfect
current_retry_attempt: 0,
maximum_reconnection_backoff,
reconnection_backoff,
}
}
}
impl Future for ConnectionReconnector {
type Output = tokio::net::TcpStream;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
// see if we are still in exponential backoff
if Pin::new(&mut self.current_backoff_delay)
.poll(cx)
.is_pending()
{
return Poll::Pending;
};
// see if we managed to resolve the connection yet
match Pin::new(&mut self.connection).poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(Err(e)) => {
warn!(
"we failed to re-establish connection to {} - {:?} (attempt {})",
self.address, e, self.current_retry_attempt
);
self.current_retry_attempt += 1;
// we failed to re-establish connection - continue exponential backoff
let next_delay = std::cmp::min(
self.maximum_reconnection_backoff,
2_u32.pow(self.current_retry_attempt) * self.reconnection_backoff,
);
self.current_backoff_delay
.reset(tokio::time::Instant::now() + next_delay);
self.connection = Box::pin(tokio::net::TcpStream::connect(self.address));
Poll::Pending
}
Poll::Ready(Ok(conn)) => Poll::Ready(conn),
}
}
}
@@ -0,0 +1,46 @@
use futures::task::{Context, Poll};
use futures::AsyncWrite;
use std::io;
use std::pin::Pin;
use tokio::prelude::*;
pub(crate) struct ConnectionWriter {
connection: tokio::net::TcpStream,
}
impl ConnectionWriter {
pub(crate) fn new(connection: tokio::net::TcpStream) -> Self {
ConnectionWriter { connection }
}
}
impl AsyncWrite for ConnectionWriter {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
use tokio::io::AsyncWrite;
let mut read_buf = [0; 1];
match Pin::new(&mut self.connection).poll_read(cx, &mut read_buf) {
// at least try the obvious check for if connection is definitely down
// TODO: can we do anything else?
Poll::Ready(Ok(n)) if n == 0 => Poll::Ready(Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"trying to write to closed connection",
))),
_ => Pin::new(&mut self.connection).poll_write(cx, buf),
}
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
use tokio::io::AsyncWrite;
Pin::new(&mut self.connection).poll_flush(cx)
}
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
use tokio::io::AsyncWrite;
Pin::new(&mut self.connection).poll_shutdown(cx)
}
}
+86 -161
View File
@@ -1,222 +1,83 @@
use futures::task::{Context, Poll};
use futures::{AsyncWrite, AsyncWriteExt};
use crate::connection_manager::ConnectionManager;
use std::collections::HashMap;
use std::io;
use std::net::SocketAddr;
use std::pin::Pin;
use std::str;
use std::time::Duration;
use tokio::prelude::*;
use tokio::prelude::*; // this import is actually required for socket.read() call
struct ConnectionWriter {
connection: tokio::net::TcpStream,
reconnection_backoff: Duration,
maximum_reconnection_backoff: Duration,
current_reconnection_backoff: Duration,
}
impl ConnectionWriter {
fn new(
connection: tokio::net::TcpStream,
initial_reconnection_backoff: Duration,
maximum_reconnection_backoff: Duration,
) -> Self {
ConnectionWriter {
connection,
reconnection_backoff: initial_reconnection_backoff,
struct ConnectionReconnector {
address: SocketAddr,
connection: Pin<Box<dyn Future<Output = io::Result<tokio::net::TcpStream>>>>,
current_retry_attempt: u32,
maximum_retry_attempts: u32,
current_backoff_delay: tokio::time::Delay,
maximum_reconnection_backoff: Duration,
reconnection_backoff: Duration,
}
impl ConnectionReconnector {
fn new(
address: SocketAddr,
maximum_retry_attempts: u32,
reconnection_backoff: Duration,
maximum_reconnection_backoff: Duration,
) -> Self {
ConnectionReconnector {
address,
connection: Box::pin(tokio::net::TcpStream::connect(address)),
current_backoff_delay: tokio::time::delay_for(Duration::new(0, 0)), // if we can re-establish connection on first try without any backoff that's perfect
current_retry_attempt: 0,
maximum_reconnection_backoff,
maximum_retry_attempts,
reconnection_backoff,
}
}
}
impl Drop for ConnectionWriter {
fn drop(&mut self) {
// try to cleanly shutdown connection on going out of scope
if let Err(e) = self.connection.shutdown(std::net::Shutdown::Both) {
eprintln!("Failed to cleanly shutdown the connection - {:?}", e);
impl Future for ConnectionReconnector {
type Output = io::Result<tokio::net::TcpStream>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
// see if we are still in exponential backoff
if Pin::new(&mut self.current_backoff_delay)
.poll(cx)
.is_pending()
{
return Poll::Pending;
};
// see if we managed to resolve the connection yet
match Pin::new(&mut self.connection).poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(Err(e)) => {
warn!(
"we failed to re-establish connection to {} - {:?}",
self.address, e
);
self.current_retry_attempt += 1;
// check if we reached our limit
if self.current_retry_attempt == self.maximum_retry_attempts {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::Other,
"Reached maximum number of retry attempts",
)));
}
// we failed to re-establish connection - continue exponential backoff
let next_delay = std::cmp::min(
self.maximum_reconnection_backoff,
2_u32.pow(self.current_retry_attempt) * self.reconnection_backoff,
);
self.current_backoff_delay
.reset(tokio::time::Instant::now() + next_delay);
Poll::Pending
}
Poll::Ready(Ok(conn)) => Poll::Ready(Ok(conn)),
}
}
}
impl AsyncWrite for ConnectionWriter {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
use tokio::io::AsyncWrite;
let mut read_buf = [0; 1];
match Pin::new(&mut self.connection).poll_read(cx, &mut read_buf) {
// at least try the obvious check if connection is definitely down
// can't do more than that
Poll::Ready(Ok(n)) if n == 0 => Poll::Ready(Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"trying to write to closed connection",
))),
_ => Pin::new(&mut self.connection).poll_write(cx, buf),
}
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
use tokio::io::AsyncWrite;
Pin::new(&mut self.connection).poll_flush(cx)
}
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
use tokio::io::AsyncWrite;
Pin::new(&mut self.connection).poll_shutdown(cx)
}
}
mod connection_manager;
pub struct Config {
initial_endpoints: Vec<SocketAddr>,
initial_reconnection_backoff: Duration,
reconnection_backoff: Duration,
maximum_reconnection_backoff: Duration,
}
impl Config {
pub fn new(
initial_endpoints: Vec<SocketAddr>,
initial_reconnection_backoff: Duration,
reconnection_backoff: Duration,
maximum_reconnection_backoff: Duration,
) -> Self {
Config {
initial_endpoints,
initial_reconnection_backoff,
reconnection_backoff,
maximum_reconnection_backoff,
}
}
}
pub struct Client {
connections_writers: HashMap<SocketAddr, ConnectionWriter>,
connections_managers: HashMap<SocketAddr, ConnectionManager>,
}
impl Client {
pub async fn new(config: Config) -> Client {
let mut connections_writers = HashMap::new();
let mut connections_managers = HashMap::new();
for endpoint in config.initial_endpoints {
connections_writers.insert(
connections_managers.insert(
endpoint,
ConnectionWriter::new(
tokio::net::TcpStream::connect(endpoint).await.unwrap(),
config.initial_reconnection_backoff,
ConnectionManager::new(
endpoint,
config.reconnection_backoff,
config.maximum_reconnection_backoff,
),
)
.await,
);
}
Client {
connections_writers,
connections_managers,
}
}
pub async fn send(&mut self, address: SocketAddr, message: &[u8]) -> io::Result<()> {
println!("sending {:?}", str::from_utf8(message));
if !self.connections_writers.contains_key(&address) {
if !self.connections_managers.contains_key(&address) {
return Err(io::Error::new(
io::ErrorKind::AddrNotAvailable,
"address not in the list",
"address not in the list - dynamic connections not yet supported",
));
}
// let (tx, rx) = oneshot::channel();
// to optimize later by using channels and separate tokio tasks for each connection handler
// because right now say we want to write to addresses A and B -
// We have to wait until we're done dealing with A before we can do anything with B
if let Err(e) = self
.connections_writers
self.connections_managers
.get_mut(&address)
.unwrap()
.write_all(&message)
.send(&message)
.await
{
println!(
"Failed to write to socket - {:?}. Presumably we need to reconnect!",
e
);
// TODO: reconnection
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time;
use std::{env, time};
const CLOSE_MESSAGE: [u8; 3] = [0, 0, 0];
@@ -267,8 +128,72 @@ mod tests {
}
}
#[test]
fn client_reconnects_to_server_after_it_went_down() {
let num_test_threads = env::var("RUST_TEST_THREADS").expect("Number of test threads must be set to 1 to prevent tests interacting with themselves! (RUST_TEST_THREADS=1)");
assert_eq!(num_test_threads, "1", "Number of test threads must be set to 1 to prevent tests interacting with themselves! (RUST_TEST_THREADS=1)");
let mut rt = tokio::runtime::Runtime::new().unwrap();
let addr = "127.0.0.1:5000".parse().unwrap();
let reconnection_backoff = Duration::from_secs(1);
let client_config =
Config::new(vec![addr], reconnection_backoff, 10 * reconnection_backoff);
let messages_to_send = vec![b"foomp1", b"foomp2"];
let finished_dummy_server_future =
rt.spawn(DummyServer::new().listen_until(addr, CLOSE_MESSAGE.as_ref()));
let mut c = rt.block_on(Client::new(client_config));
for msg in &messages_to_send {
rt.block_on(c.send(addr, *msg)).unwrap();
rt.block_on(
async move { tokio::time::delay_for(time::Duration::from_millis(50)).await },
);
}
// kill server
rt.block_on(c.send(addr, CLOSE_MESSAGE.as_ref())).unwrap();
rt.block_on(async move { tokio::time::delay_for(time::Duration::from_millis(50)).await });
// try to send - go into reconnection
let post_kill_message = b"new foomp";
// we are trying to send to killed server
assert!(rt.block_on(c.send(addr, post_kill_message)).is_err());
// the server future should have already been resolved
let received_messages = rt
.block_on(finished_dummy_server_future)
.unwrap()
.get_received();
assert_eq!(received_messages, messages_to_send);
let new_server_future =
rt.spawn(DummyServer::new().listen_until(addr, CLOSE_MESSAGE.as_ref()));
// keep sending after we leave reconnection backoff and reconnect
loop {
if rt.block_on(c.send(addr, post_kill_message)).is_ok() {
break;
}
rt.block_on(
async move { tokio::time::delay_for(time::Duration::from_millis(50)).await },
);
}
rt.block_on(async move { tokio::time::delay_for(time::Duration::from_millis(50)).await });
// kill the server to ensure it actually got the message
rt.block_on(c.send(addr, CLOSE_MESSAGE.as_ref())).unwrap();
let new_received_messages = rt.block_on(new_server_future).unwrap().get_received();
assert_eq!(post_kill_message.to_vec(), new_received_messages[0]);
}
#[test]
fn server_receives_all_sent_messages_when_up() {
let num_test_threads = env::var("RUST_TEST_THREADS").expect("Number of test threads must be set to 1 to prevent tests interacting with themselves! (RUST_TEST_THREADS=1)");
assert_eq!(num_test_threads, "1", "Number of test threads must be set to 1 to prevent tests interacting with themselves! (RUST_TEST_THREADS=1)");
let mut rt = tokio::runtime::Runtime::new().unwrap();
let addr = "127.0.0.1:5000".parse().unwrap();
let reconnection_backoff = Duration::from_secs(2);
@@ -0,0 +1,20 @@
use multi_tcp_client::{Client, Config};
use std::time;
use std::time::Duration;
use tokio::prelude::*;
use tokio::runtime::Runtime;
fn main() {
let mut rt = Runtime::new().unwrap();
let addr = "127.0.0.1:5000".parse().unwrap();
let reconnection_backoff = Duration::from_secs(1);
let client_config = Config::new(vec![addr], reconnection_backoff, 10 * reconnection_backoff);
let mut c = rt.block_on(Client::new(client_config));
for _ in 0..50 {
rt.block_on(c.send(addr, b"foomp\n"));
rt.block_on(async move { tokio::time::delay_for(time::Duration::from_millis(250)).await });
}
}