use std::collections::HashMap;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock, Weak};
use std::time::Duration;
use tokio::io::BufReader;
use tokio::net::UnixStream;
use tokio::sync::{broadcast, mpsc, oneshot, watch};
use wcode_harness::actor::EVENT_BUFFER;
use wcode_harness::event::AgentEvent;
use wcode_harness::protocol::{Frame, PROTOCOL_VERSION, Request, SessionId, SessionInfo};
use crate::backend::AskError;
use crate::Closed;
use crate::frame::{read_frame, write_frame};
const RECONNECT_BASE: Duration = Duration::from_millis(200);
const RECONNECT_CAP: Duration = Duration::from_secs(5);
const DEFAULT_SESSION: &str = "remote";
pub const FLUSH_TIMEOUT: Duration = Duration::from_millis(2000);
#[allow(clippy::large_enum_variant)]
enum Out {
Frame(Frame<Request>),
Flush(oneshot::Sender<()>),
}
static CONNECTIONS: OnceLock<Mutex<Vec<Weak<Inner>>>> = OnceLock::new();
fn connections() -> &'static Mutex<Vec<Weak<Inner>>> {
CONNECTIONS.get_or_init(|| Mutex::new(Vec::new()))
}
fn register(inner: &Arc<Inner>) {
let mut list = connections().lock().unwrap();
list.retain(|handle| handle.strong_count() > 0);
list.push(Arc::downgrade(inner));
}
pub async fn flush_all(timeout: Duration) {
let inners: Vec<Arc<Inner>> = {
let mut list = connections().lock().unwrap();
list.retain(|handle| handle.strong_count() > 0);
list.iter().filter_map(Weak::upgrade).collect()
};
let mut handles = Vec::with_capacity(inners.len());
for inner in inners {
handles.push(tokio::spawn(async move { inner.flush(timeout).await }));
}
for handle in handles {
let _ = handle.await;
}
}
#[derive(Clone)]
pub struct Client {
inner: Arc<Inner>,
session: Option<SessionId>,
}
struct Inner {
path: PathBuf,
events: Mutex<HashMap<SessionId, broadcast::Sender<AgentEvent>>>,
all_events: broadcast::Sender<AgentEvent>,
roster: watch::Sender<Vec<SessionInfo>>,
pending: Mutex<HashMap<u64, oneshot::Sender<AgentEvent>>>,
next_id: AtomicU64,
out: mpsc::UnboundedSender<Out>,
}
impl Client {
pub async fn connect(path: &Path) -> io::Result<Client> {
let stream = crate::socket::connect(path).await?;
Ok(Client::adopt(path.to_path_buf(), Some(stream)))
}
pub fn lazy(path: &Path) -> Client {
Client::adopt(path.to_path_buf(), None)
}
fn adopt(path: PathBuf, stream: Option<UnixStream>) -> Client {
let (out, out_rx) = mpsc::unbounded_channel::<Out>();
let inner = Arc::new(Inner {
path,
events: Mutex::new(HashMap::new()),
all_events: broadcast::channel(EVENT_BUFFER).0,
roster: watch::channel(Vec::new()).0,
pending: Mutex::new(HashMap::new()),
next_id: AtomicU64::new(1),
out,
});
register(&inner);
tokio::spawn(supervise(Arc::downgrade(&inner), out_rx, stream));
Client {
inner,
session: None,
}
}
pub fn with_session(&self, session: SessionId) -> Client {
Client {
inner: self.inner.clone(),
session: Some(session),
}
}
pub fn send(&self, request: Request) -> Result<(), Closed> {
let id = self.inner.next_id.fetch_add(1, Ordering::Relaxed);
self.inner
.out
.send(Out::Frame(self.frame(id, None, request)))
.map_err(|_| Closed)
}
pub fn send_from(&self, from: SessionId, request: Request) -> Result<(), Closed> {
let id = self.inner.next_id.fetch_add(1, Ordering::Relaxed);
self.inner
.out
.send(Out::Frame(self.frame(id, Some(from), request)))
.map_err(|_| Closed)
}
pub async fn ask(&self, request: Request) -> Result<AgentEvent, Closed> {
self.ask_bounded(request, None).await.map_err(|_| Closed)
}
pub async fn ask_within(
&self,
request: Request,
dur: Duration,
) -> Result<AgentEvent, AskError> {
self.ask_bounded(request, Some(dur)).await
}
async fn ask_bounded(
&self,
request: Request,
deadline: Option<Duration>,
) -> Result<AgentEvent, AskError> {
let id = self.inner.next_id.fetch_add(1, Ordering::Relaxed);
let (tx, rx) = oneshot::channel();
self.inner.pending.lock().unwrap().insert(id, tx);
let _guard = PendingGuard {
inner: &self.inner,
id,
};
if self
.inner
.out
.send(Out::Frame(self.frame(id, None, request)))
.is_err()
{
return Err(AskError::Closed);
}
match deadline {
Some(dur) => tokio::time::timeout(dur, rx)
.await
.map_err(|_| AskError::Timeout)?
.map_err(|_| AskError::Closed),
None => rx.await.map_err(|_| AskError::Closed),
}
}
pub fn subscribe(&self) -> broadcast::Receiver<AgentEvent> {
self.inner.sender(self.session.as_ref()).subscribe()
}
pub fn subscribe_roster(&self) -> watch::Receiver<Vec<SessionInfo>> {
self.inner.roster.subscribe()
}
pub async fn flush(&self, timeout: Duration) {
self.inner.flush(timeout).await;
}
fn frame(&self, id: u64, sender: Option<SessionId>, body: Request) -> Frame<Request> {
Frame {
v: PROTOCOL_VERSION,
id,
reply_to: None,
session: self
.session
.clone()
.unwrap_or_else(|| SessionId::new(DEFAULT_SESSION)),
sender,
body,
}
}
}
struct PendingGuard<'a> {
inner: &'a Inner,
id: u64,
}
impl Drop for PendingGuard<'_> {
fn drop(&mut self) {
self.inner.pending.lock().unwrap().remove(&self.id);
}
}
impl Inner {
fn sender(&self, session: Option<&SessionId>) -> broadcast::Sender<AgentEvent> {
match session {
None => self.all_events.clone(),
Some(session) => self
.events
.lock()
.unwrap()
.entry(session.clone())
.or_insert_with(|| broadcast::channel(EVENT_BUFFER).0)
.clone(),
}
}
async fn flush(&self, timeout: Duration) {
let (tx, rx) = oneshot::channel();
if self.out.send(Out::Flush(tx)).is_err() {
return;
}
let _ = tokio::time::timeout(timeout, rx).await;
}
}
async fn supervise(
inner: Weak<Inner>,
mut out: mpsc::UnboundedReceiver<Out>,
mut next: Option<UnixStream>,
) {
let mut backoff = RECONNECT_BASE;
loop {
let path = match inner.upgrade() {
Some(inner) => inner.path.clone(),
None => return,
};
let stream = match next.take() {
Some(stream) => stream,
None => match crate::socket::connect(&path).await {
Ok(stream) => stream,
Err(_) => {
tokio::time::sleep(backoff).await;
backoff = (backoff * 2).min(RECONNECT_CAP);
continue;
}
},
};
backoff = RECONNECT_BASE;
let (read, mut write) = stream.into_split();
let mut read = BufReader::new(read);
loop {
tokio::select! {
item = out.recv() => match item {
Some(Out::Frame(frame)) => {
if write_frame(&mut write, &frame).await.is_err() {
break;
}
}
Some(Out::Flush(done)) => {
let _ = done.send(());
}
None => return,
},
event = read_frame::<_, AgentEvent>(&mut read) => match event {
Ok(Some(frame)) => match inner.upgrade() {
Some(inner) => deliver(&inner, frame),
None => return,
},
_ => break,
},
}
}
match inner.upgrade() {
Some(inner) => inner.pending.lock().unwrap().clear(),
None => return,
}
}
}
fn deliver(inner: &Inner, frame: Frame<AgentEvent>) {
match frame.reply_to {
Some(id) => {
if let Some(tx) = inner.pending.lock().unwrap().remove(&id) {
let _ = tx.send(frame.body);
}
}
None => {
if let AgentEvent::Sessions { sessions } = &frame.body {
let _ = inner.roster.send(sessions.clone());
}
let _ = inner.all_events.send(frame.body.clone());
let _ = inner.sender(Some(&frame.session)).send(frame.body);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn inner_with_out() -> (Inner, mpsc::UnboundedReceiver<Out>) {
let (out, out_rx) = mpsc::unbounded_channel();
let inner = Inner {
path: PathBuf::from("/nonexistent"),
events: Mutex::new(HashMap::new()),
all_events: broadcast::channel(EVENT_BUFFER).0,
roster: watch::channel(Vec::new()).0,
pending: Mutex::new(HashMap::new()),
next_id: AtomicU64::new(1),
out,
};
(inner, out_rx)
}
fn inner() -> Inner {
inner_with_out().0
}
#[cfg(unix)]
fn weak_with_path(path: &Path) -> Option<Weak<Inner>> {
connections()
.lock()
.unwrap()
.iter()
.find(|handle| handle.upgrade().is_some_and(|inner| inner.path.as_path() == path))
.cloned()
}
fn stream_frame(session: SessionId, body: AgentEvent) -> Frame<AgentEvent> {
Frame {
v: PROTOCOL_VERSION,
id: 0,
reply_to: None,
session,
sender: None,
body,
}
}
#[tokio::test]
async fn deliver_splits_replies_from_streamed_events() {
let inner = inner();
let (tx, rx) = oneshot::channel();
inner.pending.lock().unwrap().insert(7, tx);
let mut stream = inner.sender(None).subscribe();
deliver(
&inner,
Frame {
v: PROTOCOL_VERSION,
id: 7,
reply_to: Some(7),
session: SessionId::new("s"),
sender: None,
body: AgentEvent::Ack,
},
);
assert!(matches!(rx.await.unwrap(), AgentEvent::Ack));
assert!(stream.try_recv().is_err(), "a reply is not streamed");
}
#[tokio::test]
async fn deliver_routes_a_streamed_event_by_session() {
let inner = inner();
let a = SessionId::agent("a");
let b = SessionId::agent("b");
let mut ra = inner.sender(Some(&a)).subscribe();
let mut rb = inner.sender(Some(&b)).subscribe();
let mut all = inner.sender(None).subscribe();
deliver(&inner, stream_frame(a.clone(), AgentEvent::AgentEnd));
assert!(matches!(ra.try_recv().unwrap(), AgentEvent::AgentEnd));
assert!(matches!(all.try_recv().unwrap(), AgentEvent::AgentEnd));
assert!(rb.try_recv().is_err(), "b did not hear a's event");
}
#[tokio::test]
async fn the_supervisor_exits_when_the_last_client_drops() {
let (inner, out_rx) = inner_with_out();
let inner = Arc::new(inner);
let supervisor = tokio::spawn(supervise(Arc::downgrade(&inner), out_rx, None));
tokio::time::sleep(Duration::from_millis(50)).await;
assert!(
!supervisor.is_finished(),
"the supervisor runs while a client holds the connection"
);
drop(inner);
tokio::time::timeout(Duration::from_secs(2), supervisor)
.await
.expect("the supervisor returns once the last client drops")
.expect("the supervisor task did not panic");
}
#[cfg(unix)]
#[tokio::test]
async fn a_dropped_client_leaves_no_live_connection() {
let path = PathBuf::from("/nonexistent-teardown-xyz.sock");
let client = Client::lazy(&path);
tokio::time::sleep(Duration::from_millis(50)).await;
let weak = weak_with_path(&path).expect("the lazy client registered its connection");
assert!(weak.upgrade().is_some(), "registered while the client lives");
drop(client);
let mut dead = false;
for _ in 0..200 {
if weak.upgrade().is_none() {
dead = true;
break;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
assert!(
dead,
"a dropped client leaves no live connection (the supervisor released it)"
);
}
#[cfg(unix)]
#[tokio::test]
async fn ask_within_times_out_without_leaking_a_pending_entry() {
let client = Client::lazy(Path::new("/nonexistent-ask-within.sock"));
let err = client
.ask_within(Request::GetHistory, Duration::from_millis(20))
.await
.expect_err("a silent backend times out");
assert!(matches!(err, AskError::Timeout), "{err:?}");
assert!(
client.inner.pending.lock().unwrap().is_empty(),
"a timed-out ask removed its pending slot"
);
deliver(
&client.inner,
Frame {
v: PROTOCOL_VERSION,
id: 1,
reply_to: Some(1),
session: SessionId::new("s"),
sender: None,
body: AgentEvent::Ack,
},
);
assert!(client.inner.pending.lock().unwrap().is_empty());
}
}