use bytes::Bytes;
use futures::{SinkExt, StreamExt, stream::SplitSink, stream::SplitStream};
use std::sync::{Arc, Mutex, PoisonError};
use std::task::{Context, Poll};
use std::time::Duration;
use tokio::{
net::TcpStream,
select,
sync::{mpsc, oneshot},
time::sleep,
};
use tokio_tungstenite::{
MaybeTlsStream, WebSocketStream,
tungstenite::{self, Message, Utf8Bytes, protocol::CloseFrame},
};
#[cfg(feature = "tracing")]
use tracing::{error, info, instrument, trace};
use crate::Error;
#[derive(Debug)]
pub(crate) struct TxChannelPayload {
pub(crate) message: Message,
pub(crate) response_tx: Option<oneshot::Sender<Result<(), Error>>>,
}
pub type WebSocketStreamType = WebSocketStream<MaybeTlsStream<TcpStream>>;
type SocketSink = SplitSink<WebSocketStreamType, Message>;
type SocketStream = SplitStream<WebSocketStreamType>;
enum LoopState {
Running,
Error(Error),
Closed,
}
pub(crate) type TerminalError = Arc<Mutex<Option<Error>>>;
pub(crate) fn take_terminal_error_opt(slot: &TerminalError) -> Option<Error> {
slot.lock().unwrap_or_else(PoisonError::into_inner).take()
}
pub(crate) fn take_terminal_error(slot: &TerminalError) -> Error {
take_terminal_error_opt(slot).unwrap_or(Error::WebsocketClosed)
}
pub(crate) async fn send_confirmed(
sender: &mpsc::Sender<TxChannelPayload>,
terminal_error: &TerminalError,
message: Message,
) -> Result<(), Error> {
let (tx, rx) = oneshot::channel::<Result<(), Error>>();
sender
.send(TxChannelPayload {
message,
response_tx: Some(tx),
})
.await
.map_err(|_| take_terminal_error(terminal_error))?;
match rx.await {
Ok(result) => result,
Err(_) => unreachable!("Socket loop always sends response before dropping one-shot"),
}
}
pub(crate) async fn send_unconfirmed(
sender: &mpsc::Sender<TxChannelPayload>,
terminal_error: &TerminalError,
message: Message,
) -> Result<(), Error> {
sender
.send(TxChannelPayload {
message,
response_tx: None,
})
.await
.map_err(|_| take_terminal_error(terminal_error))
}
pub(crate) async fn send_close(sender: &mpsc::Sender<TxChannelPayload>) -> Result<(), Error> {
let (tx, rx) = oneshot::channel::<Result<(), Error>>();
sender
.send(TxChannelPayload {
message: Message::Close(Some(CloseFrame {
code: tungstenite::protocol::frame::coding::CloseCode::Normal,
reason: Utf8Bytes::from_static("Closing Connection"),
})),
response_tx: Some(tx),
})
.await
.map_err(|_| Error::WebsocketClosed)?;
match rx.await {
Ok(result) => result,
Err(_) => unreachable!("Socket loop always sends response before dropping one-shot"),
}
}
pub(crate) async fn recv_raw(
receiver: &mut mpsc::Receiver<Message>,
terminal_error: &TerminalError,
) -> Result<Message, Error> {
match receiver.recv().await {
Some(message) => {
#[cfg(feature = "tracing")]
trace!("Received message: {:?}", message);
Ok(message)
}
None => Err(take_terminal_error(terminal_error)),
}
}
pub(crate) fn poll_recv_raw(
receiver: &mut mpsc::Receiver<Message>,
terminal_error: &TerminalError,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Message, Error>>> {
match receiver.poll_recv(cx) {
Poll::Ready(Some(message)) => Poll::Ready(Some(Ok(message))),
Poll::Ready(None) => match take_terminal_error_opt(terminal_error) {
Some(error) => Poll::Ready(Some(Err(error))),
None => Poll::Ready(None),
},
Poll::Pending => Poll::Pending,
}
}
#[cfg_attr(
feature = "tracing",
instrument(skip(keepalive_interval, keepalive_message, terminal_error))
)]
pub(crate) async fn socket_loop_split(
mut receiver: mpsc::Receiver<TxChannelPayload>,
mut sender: mpsc::Sender<Message>,
mut sink: SocketSink,
mut stream: SocketStream,
keepalive_interval: Option<Duration>,
keepalive_message: Option<Message>,
terminal_error: TerminalError,
) {
let mut state = LoopState::Running;
while matches!(state, LoopState::Running) {
state = if let Some(interval) = keepalive_interval {
select! {
outgoing_message = receiver.recv() => send_socket_message(outgoing_message, &mut sink).await,
incoming_message = stream.next() => match handle_incoming(incoming_message, &mut sink).await {
Incoming::State(s) => s,
Incoming::Data(frame) => deliver_with_backpressure(
frame,
&mut sender,
&mut receiver,
&mut sink,
keepalive_interval,
keepalive_message.as_ref(),
).await,
},
() = sleep(interval) => send_keepalive(&mut sink, keepalive_message.as_ref()).await,
}
} else {
select! {
outgoing_message = receiver.recv() => send_socket_message(outgoing_message, &mut sink).await,
incoming_message = stream.next() => match handle_incoming(incoming_message, &mut sink).await {
Incoming::State(s) => s,
Incoming::Data(frame) => deliver_with_backpressure(
frame,
&mut sender,
&mut receiver,
&mut sink,
keepalive_interval,
keepalive_message.as_ref(),
).await,
},
}
};
}
match state {
LoopState::Error(e) => {
*terminal_error
.lock()
.unwrap_or_else(PoisonError::into_inner) = Some(e);
}
LoopState::Closed => {}
LoopState::Running => unreachable!("We only exit when closed or errored"),
}
}
#[cfg_attr(feature = "tracing", instrument)]
async fn send_socket_message(
message: Option<TxChannelPayload>,
sink: &mut SocketSink,
) -> LoopState {
if let Some(message) = message {
#[cfg(feature = "tracing")]
trace!("Sending message: {:?}", message);
let send_result = sink.send(message.message).await.map_err(Error::from);
let socket_error = send_result.is_err();
if let Some(response_tx) = message.response_tx
&& response_tx.send(send_result).is_err()
{
return LoopState::Error(Error::SocketeerDroppedWithoutClosing);
}
if socket_error {
LoopState::Error(Error::WebsocketClosed)
} else {
LoopState::Running
}
} else {
#[cfg(feature = "tracing")]
error!("Socketeer dropped without closing connection");
LoopState::Error(Error::SocketeerDroppedWithoutClosing)
}
}
enum Incoming {
State(LoopState),
Data(Message),
}
#[cfg_attr(feature = "tracing", instrument)]
async fn handle_incoming(
message: Option<Result<Message, tungstenite::Error>>,
sink: &mut SocketSink,
) -> Incoming {
const PONG_BYTES: Bytes = Bytes::from_static(b"pong");
match message {
Some(Ok(message)) => match message {
Message::Ping(_) => {
let send_result = sink
.send(Message::Pong(PONG_BYTES))
.await
.map_err(Error::from);
Incoming::State(match send_result {
Ok(()) => LoopState::Running,
Err(e) => {
#[cfg(feature = "tracing")]
error!("Error sending Pong: {:?}", e);
LoopState::Error(e)
}
})
}
Message::Close(_) => {
let close_result = sink.close().await;
Incoming::State(match close_result {
Ok(()) => LoopState::Closed,
Err(e) => {
#[cfg(feature = "tracing")]
error!("Error sending Close: {:?}", e);
LoopState::Error(Error::from(e))
}
})
}
Message::Text(_) | Message::Binary(_) => Incoming::Data(message),
_ => Incoming::State(LoopState::Running),
},
Some(Err(e)) => {
#[cfg(feature = "tracing")]
error!("Error receiving message: {:?}", e);
Incoming::State(LoopState::Error(Error::WebsocketError(e)))
}
None => {
#[cfg(feature = "tracing")]
info!("Websocket Closed, closing rx channel");
Incoming::State(LoopState::Error(Error::WebsocketClosed))
}
}
}
async fn deliver_with_backpressure(
frame: Message,
sender: &mut mpsc::Sender<Message>,
receiver: &mut mpsc::Receiver<TxChannelPayload>,
sink: &mut SocketSink,
keepalive_interval: Option<Duration>,
keepalive_message: Option<&Message>,
) -> LoopState {
let frame = match sender.try_send(frame) {
Ok(()) => return LoopState::Running,
Err(mpsc::error::TrySendError::Closed(_)) => {
return LoopState::Error(Error::SocketeerDroppedWithoutClosing);
}
Err(mpsc::error::TrySendError::Full(frame)) => frame,
};
#[cfg(feature = "tracing")]
trace!("Receive buffer full; holding frame and applying backpressure");
loop {
if let Some(interval) = keepalive_interval {
select! {
permit = sender.reserve() => match permit {
Ok(permit) => {
permit.send(frame);
return LoopState::Running;
}
Err(_) => return LoopState::Error(Error::SocketeerDroppedWithoutClosing),
},
outgoing = receiver.recv() => match send_socket_message(outgoing, sink).await {
LoopState::Running => {}
other => return other,
},
() = sleep(interval) => match send_keepalive(sink, keepalive_message).await {
LoopState::Running => {}
other => return other,
},
}
} else {
select! {
permit = sender.reserve() => match permit {
Ok(permit) => {
permit.send(frame);
return LoopState::Running;
}
Err(_) => return LoopState::Error(Error::SocketeerDroppedWithoutClosing),
},
outgoing = receiver.recv() => match send_socket_message(outgoing, sink).await {
LoopState::Running => {}
other => return other,
},
}
}
}
}
#[cfg_attr(feature = "tracing", instrument)]
async fn send_keepalive(sink: &mut SocketSink, custom_message: Option<&Message>) -> LoopState {
let message = if let Some(custom) = custom_message {
#[cfg(feature = "tracing")]
trace!("Timeout waiting for message, sending custom keepalive");
custom.clone()
} else {
#[cfg(feature = "tracing")]
trace!("Timeout waiting for message, sending Ping");
Message::Ping(Bytes::new())
};
let result = sink.send(message).await.map_err(Error::from);
match result {
Ok(()) => LoopState::Running,
Err(e) => {
#[cfg(feature = "tracing")]
error!("Error sending keepalive: {:?}", e);
LoopState::Error(e)
}
}
}