use std::collections::VecDeque;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Weak};
use bytes::Bytes;
use tokio::sync::{Mutex, Notify, broadcast, mpsc, watch};
use weida_core::{Address, Error, LossCause, PeerIdentity};
use crate::config::ClientTls;
use crate::conn::ConnHandle;
use crate::reconnect::{EVENT_QUEUE, GiveUp, OutboxFull, PeerEvent, PeerEvents, ReconnectPolicy};
use crate::runtime::RuntimeInner;
use crate::transfer::{
IncomingRequest, IncomingTransfer, OutgoingTransfer, ReplyStream, TransferMeta,
outgoing_header, write_data_preamble,
};
pub(crate) trait Attach: Send + Sync {
fn attach<'a>(
&'a self,
conn: &'a ConnHandle,
path: &'a str,
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>>;
}
enum SlotState {
Live(ConnHandle),
Down(LossCause),
Gone(LossCause),
}
struct Slot {
id: u64,
url: Arc<str>,
path: Arc<str>,
state: SlotState,
stop: Arc<Notify>,
}
struct Queued {
meta: TransferMeta,
body: Bytes,
}
struct Outbox {
queue: std::sync::Mutex<VecDeque<Queued>>,
bytes: AtomicUsize,
room: Notify,
filled: Notify,
draining: AtomicBool,
dropped: AtomicU64,
}
pub(crate) struct PeerShared {
runtime: Arc<RuntimeInner>,
tls: Arc<ClientTls>,
slots: std::sync::Mutex<Vec<Slot>>,
cursor: AtomicUsize,
next_slot: AtomicU64,
changed: Notify,
events: broadcast::Sender<PeerEvent>,
attach: Option<Arc<dyn Attach>>,
outbox: Outbox,
}
impl PeerShared {
fn try_pick(&self) -> Result<Option<(ConnHandle, Arc<str>)>, Error> {
let slots = self.slots.lock().expect("peer list poisoned");
if slots.is_empty() {
return Err(Error::NotConnected);
}
let start = self.cursor.fetch_add(1, Ordering::Relaxed);
let mut cause = None;
let mut pending = false;
for offset in 0..slots.len() {
let slot = &slots[(start + offset) % slots.len()];
match &slot.state {
SlotState::Live(conn) => match conn.conn.close_reason() {
None => return Ok(Some((ConnHandle::clone(conn), Arc::clone(&slot.path)))),
Some(reason) => {
let c = loss_cause(&reason);
if self.runtime.config.reconnect.redials_after(c) {
pending = true;
}
cause = Some(c);
}
},
SlotState::Down(c) => {
pending = true;
cause = Some(*c);
}
SlotState::Gone(c) => cause = Some(*c),
}
}
if pending {
return Ok(None);
}
Err(Error::ConnectionLost(
cause.unwrap_or(LossCause::LocallyClosed),
))
}
async fn pick(&self) -> Result<(ConnHandle, Arc<str>), Error> {
let wait = self.wait_live();
match self.runtime.config.send_timeout {
None => wait.await,
Some(limit) => match self.runtime.exec.within(limit, wait).await {
Some(picked) => picked,
None => Err(match self.try_pick() {
Ok(Some(picked)) => return Ok(picked),
Ok(None) => Error::ConnectionLost(self.last_cause()),
Err(e) => e,
}),
},
}
}
async fn wait_live(&self) -> Result<(ConnHandle, Arc<str>), Error> {
loop {
let notified = self.changed.notified();
tokio::pin!(notified);
notified.as_mut().enable();
match self.try_pick()? {
Some(picked) => return Ok(picked),
None => notified.await,
}
}
}
fn last_cause(&self) -> LossCause {
let slots = self.slots.lock().expect("peer list poisoned");
slots
.iter()
.rev()
.find_map(|slot| match &slot.state {
SlotState::Down(c) | SlotState::Gone(c) => Some(*c),
SlotState::Live(_) => None,
})
.unwrap_or(LossCause::LocallyClosed)
}
fn live_count(&self) -> usize {
self.slots
.lock()
.expect("peer list poisoned")
.iter()
.filter(
|slot| matches!(&slot.state, SlotState::Live(c) if c.conn.close_reason().is_none()),
)
.count()
}
fn live_peers(&self) -> Vec<(ConnHandle, Arc<str>)> {
self.slots
.lock()
.expect("peer list poisoned")
.iter()
.filter_map(|slot| match &slot.state {
SlotState::Live(conn) if conn.conn.close_reason().is_none() => {
Some((ConnHandle::clone(conn), Arc::clone(&slot.path)))
}
_ => None,
})
.collect()
}
fn set_state(&self, id: u64, state: SlotState) {
let mut slots = self.slots.lock().expect("peer list poisoned");
if let Some(slot) = slots.iter_mut().find(|slot| slot.id == id) {
slot.state = state;
}
drop(slots);
self.changed.notify_waiters();
}
fn emit(&self, event: PeerEvent) {
let _ = self.events.send(event);
}
async fn dial_address(
&self,
address: &Address,
proved: Option<&PeerIdentity>,
) -> Result<(ConnHandle, Arc<str>), Error> {
let (conn, path) = match address {
Address::Quic(addr) => {
let path = addr.path.as_str();
let pinned;
let addr = match proved {
Some(PeerIdentity::Key(fp)) if addr.peer.is_none() => {
pinned = weida_core::EndpointAddr {
peer: Some(*fp),
..addr.clone()
};
&pinned
}
_ => addr,
};
let conn = self.runtime.connect(addr, &self.tls).await?;
(conn, path)
}
Address::Inproc(addr) => {
let conn = self.runtime.connect_local(&addr.bus).await?;
(conn, addr.path.as_str())
}
#[cfg(unix)]
Address::Unix(addr) => {
let conn = self.runtime.connect_unix(&addr.socket).await?;
(conn, addr.path.as_str())
}
#[cfg(not(unix))]
Address::Unix(addr) => {
return Err(Error::InvalidAddress(format!(
"{}: AF_UNIX is not available on this platform",
addr.socket
)));
}
#[cfg(windows)]
Address::Pipe(addr) => {
let conn = self.runtime.connect_pipe(addr).await?;
(conn, addr.path.as_str())
}
#[cfg(not(windows))]
Address::Pipe(addr) => {
return Err(Error::InvalidAddress(format!(
"{}: named pipes are not available on this platform",
addr.name
)));
}
};
if let Some(attach) = &self.attach {
attach.attach(&conn, path).await?;
}
Ok((conn, Arc::from(path)))
}
}
pub struct Peer {
shared: Arc<PeerShared>,
alive: watch::Sender<()>,
}
impl Peer {
pub(crate) fn new(runtime: Arc<RuntimeInner>, tls: Arc<ClientTls>) -> Peer {
Peer::with_attach(runtime, tls, None)
}
pub(crate) fn with_attach(
runtime: Arc<RuntimeInner>,
tls: Arc<ClientTls>,
attach: Option<Arc<dyn Attach>>,
) -> Peer {
let (events, _) = broadcast::channel(EVENT_QUEUE);
let (alive, _) = watch::channel(());
Peer {
shared: Arc::new(PeerShared {
runtime,
tls,
slots: std::sync::Mutex::new(Vec::new()),
cursor: AtomicUsize::new(0),
next_slot: AtomicU64::new(0),
changed: Notify::new(),
events,
attach,
outbox: Outbox {
queue: std::sync::Mutex::new(VecDeque::new()),
bytes: AtomicUsize::new(0),
room: Notify::new(),
filled: Notify::new(),
draining: AtomicBool::new(false),
dropped: AtomicU64::new(0),
},
}),
alive,
}
}
pub async fn connect(&self, url: &str) -> Result<(), Error> {
self.dial(url).await.map(|_| ())
}
pub fn disconnect(&self, url: &str) -> bool {
let mut slots = self.shared.slots.lock().expect("peer list poisoned");
let before = slots.len();
slots.retain(|slot| {
let keep = &*slot.url != url;
if !keep {
slot.stop.notify_one();
}
keep
});
let removed = slots.len() != before;
drop(slots);
if removed {
self.shared.changed.notify_waiters();
}
removed
}
pub fn peer_count(&self) -> usize {
self.shared.live_count()
}
pub(crate) fn slot_count(&self) -> usize {
self.shared.slots.lock().expect("peer list poisoned").len()
}
pub fn events(&self) -> PeerEvents {
PeerEvents::new(self.shared.events.subscribe())
}
pub fn dropped(&self) -> u64 {
self.shared.outbox.dropped.load(Ordering::Relaxed)
}
pub async fn open(&self, meta: TransferMeta) -> Result<OutgoingTransfer, Error> {
let (conn, path) = self.shared.pick().await?;
open_transfer_on(&conn, &path, &meta).await
}
pub async fn open_bi(
&self,
meta: TransferMeta,
) -> Result<(OutgoingTransfer, ReplyStream), Error> {
let (conn, path) = self.shared.pick().await?;
let (header, trace, cursors) = outgoing_header(&conn, Some(&path), &meta, None)?;
let (mut send, recv) = conn.open_bi().await?;
write_data_preamble(&mut send, &header).await?;
Ok((
OutgoingTransfer::new(send, trace, Arc::clone(&conn), cursors),
ReplyStream::new(recv, conn),
))
}
pub async fn send(&self, meta: TransferMeta, body: &[u8]) -> Result<(), Error> {
let shared = &self.shared;
if shared.outbox_is_empty()
&& let Some((conn, path)) = shared.try_pick()?
{
match open_transfer_on(&conn, &path, &meta).await {
Ok(mut transfer) => {
transfer.write_all(body).await?;
transfer.finish()?;
return Ok(());
}
Err(Error::ConnectionLost(_)) => {}
Err(e) => return Err(e),
}
}
shared.enqueue(meta, body).await?;
self.ensure_draining();
Ok(())
}
fn ensure_draining(&self) {
if self.shared.outbox.draining.swap(true, Ordering::AcqRel) {
return;
}
let weak = Arc::downgrade(&self.shared);
let alive = self.alive.subscribe();
self.shared.runtime.exec.spawn(drain_outbox(weak, alive));
}
pub(crate) fn exec(&self) -> &crate::runtime::Exec {
&self.shared.runtime.exec
}
pub(crate) async fn dial(&self, url: &str) -> Result<(ConnHandle, Arc<str>), Error> {
let address = Address::parse(url)?;
let (conn, path) = self.shared.dial_address(&address, None).await?;
let url: Arc<str> = Arc::from(url);
let id = self.shared.next_slot.fetch_add(1, Ordering::Relaxed);
let stop = Arc::new(Notify::new());
{
let mut slots = self.shared.slots.lock().expect("peer list poisoned");
slots.push(Slot {
id,
url: Arc::clone(&url),
path: Arc::clone(&path),
state: SlotState::Live(ConnHandle::clone(&conn)),
stop: Arc::clone(&stop),
});
}
self.shared.changed.notify_waiters();
self.shared.emit(PeerEvent::Connected {
url: Arc::clone(&url),
peer: conn.peer.clone(),
});
self.shared.runtime.exec.spawn(watch_slot(
Arc::downgrade(&self.shared),
self.alive.subscribe(),
stop,
id,
url,
address,
ConnHandle::clone(&conn),
));
Ok((conn, path))
}
pub(crate) fn live_peers(&self) -> Vec<(ConnHandle, Arc<str>)> {
self.shared.live_peers()
}
}
fn loss_cause(reason: &Error) -> LossCause {
match reason {
Error::ConnectionLost(cause) => *cause,
Error::Negotiation(_) | Error::Protocol(_) | Error::LimitExceeded => LossCause::PeerClosed,
_ => LossCause::TransportError,
}
}
async fn watch_slot(
weak: Weak<PeerShared>,
mut alive: watch::Receiver<()>,
stop: Arc<Notify>,
id: u64,
url: Arc<str>,
address: Address,
mut conn: ConnHandle,
) {
let policy: ReconnectPolicy = match weak.upgrade() {
Some(shared) => shared.runtime.config.reconnect.clone(),
None => return,
};
let proved = conn.peer.clone();
loop {
let reason = tokio::select! {
reason = conn.conn.closed() => reason,
_ = alive.changed() => return,
_ = stop.notified() => return,
};
let Some(shared) = weak.upgrade() else { return };
let cause = loss_cause(&reason);
shared.set_state(id, SlotState::Down(cause));
shared.emit(PeerEvent::Lost {
url: Arc::clone(&url),
cause,
});
let stopped = !policy.redials_after(cause);
if stopped {
give_up(&shared, id, cause, &url, GiveUp::Policy { attempts: 0 });
return;
}
drop(shared);
let mut attempt = 0u32;
conn = loop {
attempt += 1;
let Some(shared) = weak.upgrade() else { return };
if !policy.allows(attempt) {
give_up(
&shared,
id,
cause,
&url,
GiveUp::Policy {
attempts: attempt - 1,
},
);
return;
}
let delay = policy.delay(attempt);
shared.emit(PeerEvent::Retrying {
url: Arc::clone(&url),
attempt,
delay,
});
let sleep = shared.runtime.exec.sleep(delay);
drop(shared);
let waited = match &address {
Address::Inproc(addr) => tokio::select! {
_ = crate::inproc::wait_bound(&addr.bus) => true,
_ = alive.changed() => false,
_ = stop.notified() => false,
},
_ => tokio::select! {
_ = sleep => true,
_ = alive.changed() => false,
_ = stop.notified() => false,
},
};
if !waited {
return;
}
let Some(shared) = weak.upgrade() else { return };
match shared.dial_address(&address, proved.as_ref()).await {
Ok((conn, _)) => break conn,
Err(Error::Untrusted(presented)) => {
give_up(
&shared,
id,
cause,
&url,
GiveUp::PeerChanged {
presented: Some(presented),
},
);
return;
}
Err(Error::Tls(m)) if proved.is_some() => {
give_up(
&shared,
id,
cause,
&url,
GiveUp::PeerChanged { presented: None },
);
tracing::debug!(%url, reason = %m, "redial reached a peer that proved nothing");
return;
}
Err(
e @ (Error::Tls(_)
| Error::Negotiation(_)
| Error::AlreadyRegistered
| Error::Unsupported
| Error::InvalidAddress(_)),
) => {
give_up(&shared, id, cause, &url, GiveUp::Failed(e.to_string()));
return;
}
Err(e) => {
tracing::debug!(%url, attempt, error = %e, "redial failed");
}
}
};
let Some(shared) = weak.upgrade() else { return };
shared.set_state(id, SlotState::Live(ConnHandle::clone(&conn)));
shared.emit(PeerEvent::Connected {
url: Arc::clone(&url),
peer: conn.peer.clone(),
});
shared.outbox.filled.notify_waiters();
}
}
fn give_up(shared: &PeerShared, id: u64, cause: LossCause, url: &Arc<str>, why: GiveUp) {
shared.set_state(id, SlotState::Gone(cause));
shared.emit(PeerEvent::GaveUp {
url: Arc::clone(url),
why,
});
}
impl PeerShared {
fn outbox_is_empty(&self) -> bool {
self.outbox
.queue
.lock()
.expect("outbox poisoned")
.is_empty()
}
async fn enqueue(&self, meta: TransferMeta, body: &[u8]) -> Result<(), Error> {
let config = &self.runtime.config;
if body.len() > config.outbox_bytes {
return Err(Error::LimitExceeded);
}
loop {
let room = self.outbox.room.notified();
tokio::pin!(room);
room.as_mut().enable();
{
let mut queue = self.outbox.queue.lock().expect("outbox poisoned");
let bytes = self.outbox.bytes.load(Ordering::Relaxed);
if queue.len() < config.outbox_messages && bytes + body.len() <= config.outbox_bytes
{
queue.push_back(Queued {
meta,
body: Bytes::copy_from_slice(body),
});
self.outbox.bytes.fetch_add(body.len(), Ordering::Relaxed);
drop(queue);
self.outbox.filled.notify_waiters();
return Ok(());
}
}
match config.outbox_full {
OutboxFull::Drop => {
self.outbox.dropped.fetch_add(1, Ordering::Relaxed);
return Ok(());
}
OutboxFull::Reject => return Err(Error::LimitExceeded),
OutboxFull::Block => room.await,
}
}
}
fn peek_outbox(&self) -> Option<(TransferMeta, Bytes)> {
self.outbox
.queue
.lock()
.expect("outbox poisoned")
.front()
.map(|q| (q.meta.clone(), q.body.clone()))
}
fn pop_outbox(&self) {
let mut queue = self.outbox.queue.lock().expect("outbox poisoned");
if let Some(queued) = queue.pop_front() {
self.outbox
.bytes
.fetch_sub(queued.body.len(), Ordering::Relaxed);
}
drop(queue);
self.outbox.room.notify_waiters();
}
}
async fn drain_outbox(weak: Weak<PeerShared>, mut alive: watch::Receiver<()>) {
loop {
let Some(shared) = weak.upgrade() else { return };
let Some((meta, body)) = shared.peek_outbox() else {
shared.outbox.draining.store(false, Ordering::Release);
if shared.outbox_is_empty() || shared.outbox.draining.swap(true, Ordering::AcqRel) {
return;
}
continue;
};
let filled = shared.outbox.filled.notified();
tokio::pin!(filled);
filled.as_mut().enable();
let picked = match shared.try_pick() {
Ok(Some(picked)) => picked,
Ok(None) | Err(_) => {
let changed = shared.changed.notified();
tokio::select! {
_ = filled => {}
_ = changed => {}
_ = alive.changed() => return,
}
continue;
}
};
let (conn, path) = picked;
let mut transfer = match open_transfer_on(&conn, &path, &meta).await {
Ok(transfer) => transfer,
Err(Error::ConnectionLost(_)) => continue,
Err(e) => {
tracing::debug!(error = %e, "outbox body refused at the open");
shared.outbox.dropped.fetch_add(1, Ordering::Relaxed);
shared.pop_outbox();
continue;
}
};
let written = async {
transfer.write_all(&body).await?;
transfer.finish()
}
.await;
if let Err(e) = written {
tracing::debug!(error = %e, "outbox body discarded");
shared.outbox.dropped.fetch_add(1, Ordering::Relaxed);
}
shared.pop_outbox();
}
}
pub(crate) async fn open_exchange_on(
conn: &ConnHandle,
path: &str,
meta: TransferMeta,
) -> Result<(OutgoingTransfer, ReplyStream), Error> {
let (header, trace, cursors) = outgoing_header(conn, Some(path), &meta, None)?;
let (mut send, recv) = conn.open_bi().await?;
write_data_preamble(&mut send, &header).await?;
Ok((
OutgoingTransfer::new(send, trace, ConnHandle::clone(conn), cursors),
ReplyStream::new(recv, ConnHandle::clone(conn)),
))
}
pub(crate) async fn open_transfer_on(
conn: &ConnHandle,
path: &str,
meta: &TransferMeta,
) -> Result<OutgoingTransfer, Error> {
let (mut header, trace, cursors) = outgoing_header(conn, Some(path), meta, None)?;
header.sequence = conn.sequencer.next(path);
let mut stream = conn.open_uni().await?;
write_data_preamble(&mut stream, &header).await?;
Ok(OutgoingTransfer::new(
stream,
trace,
ConnHandle::clone(conn),
cursors,
))
}
impl std::fmt::Debug for Peer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Peer")
.field("peer_count", &self.peer_count())
.finish_non_exhaustive()
}
}
#[derive(Debug)]
pub enum Incoming {
Stream(IncomingTransfer),
Exchange(IncomingRequest),
Subscribed(Consumer),
Credit(CreditGrant),
Unsubscribed {
id: ConsumerId,
filter: Option<String>,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ConsumerId(pub(crate) usize);
impl ConsumerId {
pub(crate) fn from_conn(conn_id: usize) -> ConsumerId {
ConsumerId(conn_id)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CreditGrant {
pub id: ConsumerId,
pub filter: String,
pub limit: u64,
}
#[derive(Clone)]
pub struct Consumer {
conn: ConnHandle,
path: Arc<str>,
filter: String,
}
impl Consumer {
pub(crate) fn new(conn: ConnHandle, path: Arc<str>, filter: String) -> Consumer {
Consumer { conn, path, filter }
}
pub fn id(&self) -> ConsumerId {
ConsumerId(self.conn.conn.stable_id())
}
pub fn filter(&self) -> &str {
&self.filter
}
pub fn path(&self) -> &str {
&self.path
}
pub async fn open(&self, meta: TransferMeta) -> Result<OutgoingTransfer, Error> {
let (header, trace, cursors) = outgoing_header(&self.conn, Some(&self.path), &meta, None)?;
let mut send = self.conn.open_uni().await?;
crate::transfer::write_data_preamble(&mut send, &header).await?;
Ok(OutgoingTransfer::new(
send,
trace,
ConnHandle::clone(&self.conn),
cursors,
))
}
pub async fn deliver(&self, meta: TransferMeta, body: &[u8]) -> Result<(), Error> {
let mut transfer = self.open(meta).await?;
transfer.write_all(body).await?;
transfer.finish()?;
Ok(())
}
}
impl std::fmt::Debug for Consumer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Consumer")
.field("id", &self.id())
.field("path", &self.path)
.field("filter", &self.filter)
.finish()
}
}
pub struct Acceptor {
path: Arc<str>,
queue: Mutex<mpsc::Receiver<Incoming>>,
}
impl Acceptor {
pub(crate) fn new(path: &str, queue: mpsc::Receiver<Incoming>) -> Acceptor {
Acceptor {
path: Arc::from(path),
queue: Mutex::new(queue),
}
}
pub fn path(&self) -> &str {
&self.path
}
pub async fn accept(&self) -> Result<Incoming, Error> {
let mut queue = self.queue.lock().await;
queue.recv().await.ok_or(Error::NotConnected)
}
}
impl std::fmt::Debug for Acceptor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Acceptor")
.field("path", &self.path)
.finish_non_exhaustive()
}
}