use std::collections::HashMap;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::io::{split, AsyncRead, AsyncWrite, ReadHalf, WriteHalf};
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use crate::error::{BrainError, Result};
use crate::transport::{read_frame, write_frame};
use crate::wire::cbor::{from_cbor_bytes, to_cbor_bytes};
use crate::wire::frame::{Frame, FLAG_EOS, MAX_PAYLOAD_BYTES};
use crate::wire::opcode::Opcode;
use crate::wire::types::{
AuthOkPayload, AuthPayload, ByeRequest, ClientPongRequest, ErrorResponse, HelloPayload,
ServerPingResponse, SubscribeRequest, SubscriptionEvent, UnsubscribeRequest,
UnsubscribeResponse, WelcomePayload,
};
const HANDSHAKE_STREAM_ID: u32 = 0;
#[derive(Clone, Debug)]
pub struct HandshakeOutcome {
pub welcome: WelcomePayload,
pub auth_ok: AuthOkPayload,
}
fn now_unix_nanos() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| u64::try_from(d.as_nanos()).unwrap_or(u64::MAX))
}
struct RouteTable {
routes: HashMap<u32, mpsc::UnboundedSender<Frame>>,
closed: Option<String>,
}
struct Shared<S> {
writer: Arc<tokio::sync::Mutex<WriteHalf<S>>>,
table: Arc<Mutex<RouteTable>>,
next_stream_id: AtomicU32,
request_timeout: Option<Duration>,
max_payload_size: usize,
}
impl<S> Shared<S>
where
S: AsyncWrite,
{
fn alloc_stream_id(&self) -> u32 {
self.next_stream_id.fetch_add(2, Ordering::Relaxed)
}
fn register(&self, stream_id: u32) -> Result<mpsc::UnboundedReceiver<Frame>> {
let (tx, rx) = mpsc::unbounded_channel();
let mut table = self.table.lock().expect("route table mutex poisoned");
if let Some(reason) = &table.closed {
return Err(BrainError::Protocol(format!("connection closed: {reason}")));
}
table.routes.insert(stream_id, tx);
Ok(rx)
}
fn deregister(&self, stream_id: u32) {
self.table
.lock()
.expect("route table mutex poisoned")
.routes
.remove(&stream_id);
}
async fn write(&self, frame: &Frame) -> Result<()> {
if frame.payload.len() > self.max_payload_size {
return Err(BrainError::Protocol(format!(
"payload is {} bytes, over the {} the server advertised at WELCOME",
frame.payload.len(),
self.max_payload_size
)));
}
let mut w = self.writer.lock().await;
write_frame(&mut *w, frame).await
}
async fn request_on(
&self,
stream_id: u32,
mut rx: mpsc::UnboundedReceiver<Frame>,
opcode: Opcode,
payload: Vec<u8>,
) -> Result<Vec<Frame>> {
let frame = Frame::new(opcode.as_u16(), FLAG_EOS, stream_id, payload);
if let Err(e) = self.write(&frame).await {
self.deregister(stream_id);
return Err(e);
}
let mut frames = Vec::new();
loop {
let next = match self.request_timeout {
Some(timeout) => match tokio::time::timeout(timeout, rx.recv()).await {
Ok(v) => v,
Err(_) => {
self.deregister(stream_id);
return Err(BrainError::Timeout(timeout));
}
},
None => rx.recv().await,
};
match next {
Some(frame) if frame.opcode == Opcode::Error as u16 => {
self.deregister(stream_id);
let err: ErrorResponse = from_cbor_bytes(&frame.payload)?;
return Err(BrainError::from_server(err));
}
Some(frame) => {
let is_last = frame.flags & FLAG_EOS != 0;
frames.push(frame);
if is_last {
return Ok(frames);
}
}
None => return Err(self.closed_error()),
}
}
}
fn closed_error(&self) -> BrainError {
let table = self.table.lock().expect("route table mutex poisoned");
match &table.closed {
Some(reason) => BrainError::Protocol(format!("connection closed: {reason}")),
None => BrainError::Closed,
}
}
}
pub struct MuxConnection<S> {
shared: Arc<Shared<S>>,
reader: JoinHandle<()>,
}
impl<S> MuxConnection<S> {
#[must_use]
pub fn is_closed(&self) -> bool {
self.reader.is_finished()
|| self
.shared
.table
.lock()
.map_or(true, |t| t.closed.is_some())
}
}
impl<S> std::fmt::Debug for MuxConnection<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let live = self.shared.table.lock().ok().map(|t| t.routes.len());
f.debug_struct("MuxConnection")
.field("live_routes", &live)
.finish_non_exhaustive()
}
}
impl<S> Drop for MuxConnection<S> {
fn drop(&mut self) {
self.reader.abort();
}
}
impl<S> MuxConnection<S>
where
S: AsyncRead + AsyncWrite + Send + 'static,
{
pub async fn handshake(
stream: S,
hello: HelloPayload,
auth: AuthPayload,
request_timeout: Option<Duration>,
) -> Result<(Self, HandshakeOutcome)> {
let (mut read_half, mut write_half) = split(stream);
let mut buf = Vec::new();
let offered = hello.supported_versions.clone();
send(&mut write_half, Opcode::Hello, HANDSHAKE_STREAM_ID, &hello).await?;
let welcome_frame =
read_handshake_reply(&mut read_half, &mut buf, Opcode::Welcome, request_timeout)
.await?;
let welcome: WelcomePayload = from_cbor_bytes(&welcome_frame.payload)?;
if !offered.contains(&welcome.chosen_version) {
return Err(BrainError::VersionMismatch {
chosen: welcome.chosen_version,
supported: offered,
});
}
send(&mut write_half, Opcode::Auth, HANDSHAKE_STREAM_ID, &auth).await?;
let auth_ok_frame =
read_handshake_reply(&mut read_half, &mut buf, Opcode::AuthOk, request_timeout).await?;
let auth_ok: AuthOkPayload = from_cbor_bytes(&auth_ok_frame.payload)?;
let table = Arc::new(Mutex::new(RouteTable {
routes: HashMap::new(),
closed: None,
}));
let writer = Arc::new(tokio::sync::Mutex::new(write_half));
let reader = tokio::spawn(reader_loop(
read_half,
buf,
Arc::clone(&table),
Arc::clone(&writer),
));
let shared = Arc::new(Shared {
writer,
table,
next_stream_id: AtomicU32::new(1),
request_timeout,
max_payload_size: if welcome.server_features.max_payload_size == 0 {
MAX_PAYLOAD_BYTES
} else {
welcome.server_features.max_payload_size as usize
},
});
let conn = Self { shared, reader };
Ok((conn, HandshakeOutcome { welcome, auth_ok }))
}
pub async fn request(&self, opcode: Opcode, payload: Vec<u8>) -> Result<Vec<Frame>> {
let stream_id = self.shared.alloc_stream_id();
let rx = self.shared.register(stream_id)?;
self.shared.request_on(stream_id, rx, opcode, payload).await
}
pub async fn request_one(&self, opcode: Opcode, payload: Vec<u8>) -> Result<Frame> {
let mut frames = self.request(opcode, payload).await?;
if frames.len() != 1 {
return Err(BrainError::Protocol(format!(
"expected a single response frame, got {}",
frames.len()
)));
}
Ok(frames.remove(0))
}
pub async fn subscribe(&self, request: &SubscribeRequest) -> Result<Subscription<S>> {
let stream_id = self.shared.alloc_stream_id();
let rx = self.shared.register(stream_id)?;
let frame = Frame::new(
Opcode::SubscribeReq.as_u16(),
FLAG_EOS,
stream_id,
to_cbor_bytes(request),
);
if let Err(e) = self.shared.write(&frame).await {
self.shared.deregister(stream_id);
return Err(e);
}
Ok(Subscription {
shared: Arc::clone(&self.shared),
stream_id,
rx,
ended: false,
})
}
pub async fn send_bye(&self) -> Result<()> {
let frame = Frame::new(
Opcode::Bye.as_u16(),
FLAG_EOS,
HANDSHAKE_STREAM_ID,
to_cbor_bytes(&ByeRequest::default()),
);
self.shared.write(&frame).await
}
#[must_use]
pub fn route_count(&self) -> usize {
self.shared
.table
.lock()
.expect("route table mutex poisoned")
.routes
.len()
}
}
pub struct Subscription<S> {
shared: Arc<Shared<S>>,
stream_id: u32,
rx: mpsc::UnboundedReceiver<Frame>,
ended: bool,
}
impl<S> std::fmt::Debug for Subscription<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Subscription")
.field("stream_id", &self.stream_id)
.field("ended", &self.ended)
.finish_non_exhaustive()
}
}
impl<S> Subscription<S>
where
S: AsyncWrite,
{
#[must_use]
pub fn stream_id(&self) -> u32 {
self.stream_id
}
pub async fn next(&mut self) -> Option<Result<SubscriptionEvent>> {
if self.ended {
return None;
}
let frame = self.rx.recv().await?;
if frame.opcode == Opcode::Error as u16 {
self.ended = true;
return Some(match from_cbor_bytes::<ErrorResponse>(&frame.payload) {
Ok(err) => Err(BrainError::from_server(err)),
Err(e) => Err(e.into()),
});
}
if frame.flags & FLAG_EOS != 0 {
self.ended = true;
if frame.payload.is_empty() {
return None;
}
}
Some(from_cbor_bytes::<SubscriptionEvent>(&frame.payload).map_err(Into::into))
}
pub async fn unsubscribe(&self) -> Result<UnsubscribeResponse> {
let req = UnsubscribeRequest {
target_stream_id: self.stream_id,
};
let stream_id = self.shared.alloc_stream_id();
let rx = self.shared.register(stream_id)?;
let mut frames = self
.shared
.request_on(stream_id, rx, Opcode::UnsubscribeReq, to_cbor_bytes(&req))
.await?;
if frames.len() != 1 {
return Err(BrainError::Protocol(format!(
"expected a single UNSUBSCRIBE_RESP frame, got {}",
frames.len()
)));
}
let frame = frames.remove(0);
if frame.opcode != Opcode::UnsubscribeResp as u16 {
return Err(BrainError::Protocol(format!(
"expected UNSUBSCRIBE_RESP ({:#06x}), got {:#06x}",
Opcode::UnsubscribeResp as u16,
frame.opcode
)));
}
Ok(from_cbor_bytes(&frame.payload)?)
}
}
impl<S> Drop for Subscription<S> {
fn drop(&mut self) {
self.shared
.table
.lock()
.expect("route table mutex poisoned")
.routes
.remove(&self.stream_id);
}
}
async fn reader_loop<S>(
mut read_half: ReadHalf<S>,
mut buf: Vec<u8>,
table: Arc<Mutex<RouteTable>>,
writer: Arc<tokio::sync::Mutex<WriteHalf<S>>>,
) where
S: AsyncRead + AsyncWrite,
{
loop {
match read_frame(&mut read_half, &mut buf).await {
Ok(frame) => {
if frame.opcode == Opcode::ServerPing.as_u16() {
let server_ts = from_cbor_bytes::<ServerPingResponse>(&frame.payload)
.map_or(0, |p| p.server_timestamp_unix_nanos);
let pong = ClientPongRequest {
server_timestamp_unix_nanos: server_ts,
client_timestamp_unix_nanos: now_unix_nanos(),
};
let reply = Frame::new(
Opcode::ClientPong.as_u16(),
FLAG_EOS,
HANDSHAKE_STREAM_ID,
to_cbor_bytes(&pong),
);
let mut w = writer.lock().await;
let _ = write_frame(&mut *w, &reply).await;
continue;
}
let stream_id = frame.stream_id;
let is_last = frame.flags & FLAG_EOS != 0;
let mut t = table.lock().expect("route table mutex poisoned");
if let Some(tx) = t.routes.get(&stream_id) {
let _ = tx.send(frame);
if is_last {
t.routes.remove(&stream_id);
}
}
}
Err(e) => {
let mut t = table.lock().expect("route table mutex poisoned");
t.closed = Some(e.to_string());
t.routes.clear();
return;
}
}
}
}
async fn send<W, T>(write_half: &mut W, opcode: Opcode, stream_id: u32, payload: &T) -> Result<()>
where
W: AsyncWrite + Unpin,
T: serde::Serialize,
{
let frame = Frame::new(
opcode.as_u16(),
FLAG_EOS,
stream_id,
crate::wire::cbor::to_cbor_bytes(payload),
);
write_frame(write_half, &frame).await
}
async fn read_handshake_reply<R: AsyncRead + Unpin>(
read_half: &mut R,
buf: &mut Vec<u8>,
expected: Opcode,
timeout: Option<Duration>,
) -> Result<Frame> {
let frame = match timeout {
Some(t) => tokio::time::timeout(t, read_frame(read_half, buf))
.await
.map_err(|_| BrainError::Timeout(t))??,
None => read_frame(read_half, buf).await?,
};
if frame.opcode == Opcode::Error as u16 {
let err: ErrorResponse = from_cbor_bytes(&frame.payload)?;
return Err(BrainError::from_server(err));
}
if frame.opcode != expected.as_u16() {
return Err(BrainError::Protocol(format!(
"expected {expected:?} ({:#06x}), got opcode {:#06x}",
expected.as_u16(),
frame.opcode
)));
}
Ok(frame)
}