use std::time::Duration;
use async_trait::async_trait;
use super::deadline::{SetupDeadline, SetupStep};
use super::messages::SessionInfo;
use super::protocol::Credentials;
use crate::error::TransportError;
const MUST_CONNECT: &str = "Must connect before authenticating";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ConnectionState {
Disconnected,
Connected,
Authenticated,
Closed,
Terminated,
}
#[derive(Debug)]
pub(crate) struct ConnectionLifecycle {
state: ConnectionState,
deadline: Option<SetupDeadline>,
}
impl ConnectionLifecycle {
pub(crate) fn new() -> Self {
Self {
state: ConnectionState::Disconnected,
deadline: None,
}
}
#[cfg(test)]
pub(crate) fn in_state(state: ConnectionState) -> Self {
Self {
state,
deadline: None,
}
}
pub(crate) fn state(&self) -> ConnectionState {
self.state
}
pub(crate) fn is_open(&self) -> bool {
matches!(
self.state,
ConnectionState::Connected | ConnectionState::Authenticated
)
}
pub(crate) fn require(
&self,
expected: ConnectionState,
message: &str,
) -> Result<(), TransportError> {
if self.state == expected {
Ok(())
} else if self.state == ConnectionState::Terminated {
Err(terminated_error())
} else {
Err(TransportError::ProtocolError(message.to_string()))
}
}
pub(crate) fn begin_connect(&self, timeout_ms: u64) -> Result<SetupDeadline, TransportError> {
self.require(ConnectionState::Disconnected, "Already connected")?;
Ok(SetupDeadline::start(
"Connection",
Duration::from_millis(timeout_ms),
))
}
pub(crate) fn connected(&mut self, deadline: SetupDeadline) {
self.state = ConnectionState::Connected;
self.deadline = Some(deadline);
}
}
fn terminated_error() -> TransportError {
TransportError::ProtocolError(
"Transport was terminated after an export gave up on an in-flight response; \
reconnect before the next operation"
.to_string(),
)
}
#[async_trait]
pub(crate) trait LifecycleSteps: Send {
fn lifecycle_mut(&mut self) -> &mut ConnectionLifecycle;
async fn login_exchange(
&mut self,
credentials: &Credentials,
) -> Result<SessionInfo, TransportError>;
async fn send_disconnect(&mut self);
fn release_connection(&mut self);
}
pub(crate) fn terminate(transport: &mut impl LifecycleSteps) {
transport.release_connection();
transport.lifecycle_mut().state = ConnectionState::Terminated;
}
pub(crate) async fn authenticate_within_deadline(
transport: &mut impl LifecycleSteps,
credentials: &Credentials,
) -> Result<SessionInfo, TransportError> {
let lifecycle = transport.lifecycle_mut();
lifecycle.require(ConnectionState::Connected, MUST_CONNECT)?;
let deadline = lifecycle
.deadline
.ok_or_else(|| TransportError::ProtocolError(MUST_CONNECT.to_string()))?;
match deadline
.run(SetupStep::Login, transport.login_exchange(credentials))
.await
{
Ok(session) => {
transport.lifecycle_mut().state = ConnectionState::Authenticated;
Ok(session)
}
Err(error) => {
if deadline.has_elapsed() {
terminate(transport);
transport.lifecycle_mut().state = ConnectionState::Closed;
}
Err(error)
}
}
}
pub(crate) async fn close_gracefully(
transport: &mut impl LifecycleSteps,
) -> Result<(), TransportError> {
if !transport.lifecycle_mut().is_open() {
return Ok(());
}
transport.send_disconnect().await;
terminate(transport);
transport.lifecycle_mut().state = ConnectionState::Closed;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::transport::test_support::{
assert_names_the_termination, test_credentials, transport_session_info,
};
use std::collections::VecDeque;
use std::time::Duration;
use tokio::time::Instant;
const MUST_AUTHENTICATE: &str = "Must authenticate before executing queries";
enum Login {
NeverFinishes,
FailsWithWrongPassword,
Succeeds,
}
struct ScriptedSteps {
lifecycle: ConnectionLifecycle,
logins: VecDeque<Login>,
disconnects: usize,
releases: usize,
}
impl ScriptedSteps {
fn new(lifecycle: ConnectionLifecycle, logins: impl IntoIterator<Item = Login>) -> Self {
Self {
lifecycle,
logins: logins.into_iter().collect(),
disconnects: 0,
releases: 0,
}
}
fn connected(timeout_ms: u64, logins: impl IntoIterator<Item = Login>) -> Self {
let mut lifecycle = ConnectionLifecycle::new();
let deadline = lifecycle
.begin_connect(timeout_ms)
.expect("a new lifecycle may connect");
lifecycle.connected(deadline);
Self::new(lifecycle, logins)
}
}
#[async_trait]
impl LifecycleSteps for ScriptedSteps {
fn lifecycle_mut(&mut self) -> &mut ConnectionLifecycle {
&mut self.lifecycle
}
async fn login_exchange(
&mut self,
_credentials: &Credentials,
) -> Result<SessionInfo, TransportError> {
match self.logins.pop_front().expect("every login is scripted") {
Login::NeverFinishes => std::future::pending().await,
Login::FailsWithWrongPassword => Err(TransportError::ProtocolError(
"Authentication failed: wrong password".to_string(),
)),
Login::Succeeds => Ok(transport_session_info()),
}
}
async fn send_disconnect(&mut self) {
self.disconnects += 1;
}
fn release_connection(&mut self) {
self.releases += 1;
}
}
#[tokio::test(start_paused = true)]
async fn a_login_that_runs_out_of_time_releases_the_connection_and_records_closed() {
let mut steps = ScriptedSteps::connected(300, [Login::NeverFinishes]);
let error = authenticate_within_deadline(&mut steps, &test_credentials())
.await
.expect_err("a login that never finishes must run out of time");
assert!(
error
.to_string()
.contains("Connection timeout after 300ms (login)"),
"{error}"
);
assert_eq!(steps.releases, 1);
assert_eq!(steps.lifecycle.state(), ConnectionState::Closed);
assert!(!steps.lifecycle.is_open());
}
#[tokio::test(start_paused = true)]
async fn a_login_gets_only_the_time_left_after_connect() {
let started = Instant::now();
let mut steps = ScriptedSteps::connected(1000, [Login::NeverFinishes]);
tokio::time::sleep(Duration::from_millis(800)).await;
let error = authenticate_within_deadline(&mut steps, &test_credentials())
.await
.expect_err("a login that never finishes must run out of time");
assert_eq!(started.elapsed(), Duration::from_millis(1000));
assert!(error.to_string().contains("(login)"), "{error}");
}
#[tokio::test(start_paused = true)]
async fn a_login_error_before_the_deadline_is_returned_and_keeps_the_transport_connected() {
let started = Instant::now();
let mut steps =
ScriptedSteps::connected(1000, [Login::FailsWithWrongPassword, Login::NeverFinishes]);
let error = authenticate_within_deadline(&mut steps, &test_credentials())
.await
.expect_err("the scripted login fails");
assert!(
matches!(&error, TransportError::ProtocolError(message)
if message == "Authentication failed: wrong password"),
"{error:?}"
);
assert_eq!(steps.releases, 0);
assert_eq!(steps.lifecycle.state(), ConnectionState::Connected);
tokio::time::sleep(Duration::from_millis(500)).await;
let retry_error = authenticate_within_deadline(&mut steps, &test_credentials())
.await
.expect_err("a retried login that never finishes must run out of time");
assert_eq!(started.elapsed(), Duration::from_millis(1000));
assert!(
retry_error
.to_string()
.contains("Connection timeout after 1000ms (login)"),
"{retry_error}"
);
}
#[tokio::test(start_paused = true)]
async fn authenticate_requires_connected_and_records_authenticated_on_success() {
for lifecycle in [
ConnectionLifecycle::new(),
ConnectionLifecycle::in_state(ConnectionState::Connected),
] {
let mut unconnected = ScriptedSteps::new(lifecycle, []);
let error = authenticate_within_deadline(&mut unconnected, &test_credentials())
.await
.expect_err("a login needs a connection and its deadline");
assert!(
matches!(&error, TransportError::ProtocolError(message)
if message == "Must connect before authenticating"),
"{error:?}"
);
}
let mut steps = ScriptedSteps::connected(1000, [Login::Succeeds]);
let session = authenticate_within_deadline(&mut steps, &test_credentials())
.await
.expect("the scripted login succeeds");
assert_eq!(session.session_id, transport_session_info().session_id);
assert_eq!(steps.lifecycle.state(), ConnectionState::Authenticated);
}
#[tokio::test(start_paused = true)]
async fn every_guard_names_the_termination_on_a_terminated_transport() {
let mut steps = ScriptedSteps::new(
ConnectionLifecycle::in_state(ConnectionState::Authenticated),
[],
);
terminate(&mut steps);
let errors = [
steps
.lifecycle
.require(ConnectionState::Disconnected, "Already connected"),
steps.lifecycle.begin_connect(1000).map(|_| ()),
steps.lifecycle.require(
ConnectionState::Connected,
"Must connect before authenticating",
),
steps
.lifecycle
.require(ConnectionState::Authenticated, MUST_AUTHENTICATE),
authenticate_within_deadline(&mut steps, &test_credentials())
.await
.map(|_| ()),
];
for outcome in errors {
let error = outcome.expect_err("a terminated transport refuses every operation");
assert_names_the_termination(&error);
}
}
#[tokio::test(start_paused = true)]
async fn terminate_releases_the_connection_without_io_and_records_terminated() {
let mut steps = ScriptedSteps::new(
ConnectionLifecycle::in_state(ConnectionState::Authenticated),
[],
);
terminate(&mut steps);
assert_eq!(steps.releases, 1);
assert_eq!(steps.disconnects, 0);
assert_eq!(steps.lifecycle.state(), ConnectionState::Terminated);
assert!(!steps.lifecycle.is_open());
}
#[tokio::test(start_paused = true)]
async fn close_after_terminate_succeeds_without_io_and_keeps_reporting_termination() {
let mut steps = ScriptedSteps::new(
ConnectionLifecycle::in_state(ConnectionState::Authenticated),
[],
);
terminate(&mut steps);
close_gracefully(&mut steps)
.await
.expect("closing a terminated transport succeeds");
assert_eq!(steps.disconnects, 0);
assert_eq!(steps.releases, 1);
let error = steps
.lifecycle
.require(ConnectionState::Authenticated, MUST_AUTHENTICATE)
.expect_err("a closed terminated transport still refuses queries");
assert_names_the_termination(&error);
}
#[tokio::test(start_paused = true)]
async fn close_disconnects_an_open_transport_and_records_closed_not_terminated() {
let mut open = ScriptedSteps::new(
ConnectionLifecycle::in_state(ConnectionState::Authenticated),
[],
);
close_gracefully(&mut open)
.await
.expect("closing an open transport succeeds");
assert_eq!(open.disconnects, 1);
assert_eq!(open.releases, 1);
assert_eq!(open.lifecycle.state(), ConnectionState::Closed);
let error = open
.lifecycle
.require(ConnectionState::Authenticated, MUST_AUTHENTICATE)
.expect_err("a closed transport refuses queries");
assert!(
matches!(&error, TransportError::ProtocolError(message) if message == MUST_AUTHENTICATE),
"{error:?}"
);
let mut never_connected = ScriptedSteps::new(ConnectionLifecycle::new(), []);
close_gracefully(&mut never_connected)
.await
.expect("closing a transport that never connected succeeds");
assert_eq!(never_connected.disconnects, 0);
assert_eq!(never_connected.releases, 0);
}
}