use std::time::Duration;
use interprocess::local_socket::traits::Stream as _;
use interprocess::local_socket::Stream;
use running_process::broker::client_v2::{self, BrokerV2Error, ClientSession};
const DEFAULT_PROBE_TIMEOUT: Duration = Duration::from_millis(250);
const DEFAULT_V2_BROKER_TIMEOUT: Duration = Duration::from_secs(3);
pub fn probe_local_socket(endpoint: &str) -> std::io::Result<()> {
if endpoint.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"probe_local_socket: empty endpoint",
));
}
probe_local_socket_with_deadline(endpoint, DEFAULT_PROBE_TIMEOUT)
}
pub fn probe_local_socket_with_deadline(endpoint: &str, deadline: Duration) -> std::io::Result<()> {
let endpoint = endpoint.to_owned();
call_with_io_deadline("probe_local_socket", deadline, move || {
#[cfg(windows)]
let name = {
use interprocess::local_socket::{GenericNamespaced, ToNsName};
ToNsName::to_ns_name::<GenericNamespaced>(endpoint.as_str())?
};
#[cfg(unix)]
let name = {
use interprocess::local_socket::{GenericFilePath, ToFsName};
ToFsName::to_fs_name::<GenericFilePath>(endpoint.as_str())?
};
let stream = Stream::connect(name)?;
drop(stream);
Ok(())
})
}
pub async fn probe_local_socket_async(endpoint: &str) -> std::io::Result<()> {
probe_local_socket_with_deadline_async(endpoint, DEFAULT_PROBE_TIMEOUT).await
}
pub async fn probe_local_socket_with_deadline_async(
endpoint: &str,
deadline: Duration,
) -> std::io::Result<()> {
let endpoint = endpoint.to_owned();
tokio::task::spawn_blocking(move || probe_local_socket_with_deadline(&endpoint, deadline))
.await
.map_err(join_error_to_io)?
}
fn call_with_io_deadline<T, F>(label: &'static str, deadline: Duration, f: F) -> std::io::Result<T>
where
T: Send + 'static,
F: FnOnce() -> std::io::Result<T> + Send + 'static,
{
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let _ = tx.send(f());
});
match rx.recv_timeout(deadline) {
Ok(result) => result,
Err(_) => Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!("{label}: exceeded deadline of {deadline:?}"),
)),
}
}
fn call_with_brokerv2_deadline<T, F>(deadline: Duration, f: F) -> Result<T, BrokerV2Error>
where
T: Send + 'static,
F: FnOnce() -> Result<T, BrokerV2Error> + Send + 'static,
{
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let _ = tx.send(f());
});
match rx.recv_timeout(deadline) {
Ok(result) => result,
Err(_) => Err(BrokerV2Error::Io(std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!("v2 broker call exceeded deadline of {deadline:?}"),
))),
}
}
#[doc(hidden)]
pub fn connect_v2_broker(wanted_version: &str) -> Result<ClientSession, BrokerV2Error> {
let wanted = wanted_version.to_owned();
call_with_brokerv2_deadline(DEFAULT_V2_BROKER_TIMEOUT, move || {
client_v2::connect("zccache", &wanted)
})
}
#[doc(hidden)]
pub async fn connect_v2_broker_async(wanted_version: &str) -> Result<ClientSession, BrokerV2Error> {
let wanted = wanted_version.to_owned();
tokio::task::spawn_blocking(move || connect_v2_broker(&wanted))
.await
.map_err(join_error_to_brokerv2)?
}
#[doc(hidden)]
pub fn adopt_v2_session(
wanted_version: &str,
) -> Result<
(
interprocess::local_socket::Stream,
running_process::broker::protocol::Negotiated,
),
BrokerV2Error,
> {
let session = connect_v2_broker(wanted_version)?;
Ok(session.into_inner())
}
#[doc(hidden)]
pub async fn adopt_v2_session_async(
wanted_version: &str,
) -> Result<
(
interprocess::local_socket::Stream,
running_process::broker::protocol::Negotiated,
),
BrokerV2Error,
> {
let wanted = wanted_version.to_owned();
tokio::task::spawn_blocking(move || adopt_v2_session(&wanted))
.await
.map_err(join_error_to_brokerv2)?
}
#[doc(hidden)]
pub fn into_backend_io_v2(
wanted_version: &str,
) -> Result<interprocess::local_socket::Stream, BrokerV2Error> {
let (stream, _negotiated) = adopt_v2_session(wanted_version)?;
Ok(stream)
}
#[doc(hidden)]
pub async fn into_backend_io_v2_async(
wanted_version: &str,
) -> Result<interprocess::local_socket::Stream, BrokerV2Error> {
let wanted = wanted_version.to_owned();
tokio::task::spawn_blocking(move || into_backend_io_v2(&wanted))
.await
.map_err(join_error_to_brokerv2)?
}
fn join_error_to_io(err: tokio::task::JoinError) -> std::io::Error {
std::io::Error::other(format!("broker-v2 async bridge worker failed: {err}"))
}
fn join_error_to_brokerv2(err: tokio::task::JoinError) -> BrokerV2Error {
BrokerV2Error::Io(join_error_to_io(err))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn call_with_io_deadline_fires_on_slow_closure() {
let result: std::io::Result<()> =
call_with_io_deadline("test", Duration::from_millis(50), || {
std::thread::sleep(Duration::from_millis(500));
Ok(())
});
let err = result.expect_err("slow closure must time out");
assert_eq!(err.kind(), std::io::ErrorKind::TimedOut);
assert!(err.to_string().contains("exceeded deadline"));
}
#[test]
fn call_with_io_deadline_passes_through_fast_ok() {
let result: std::io::Result<u32> =
call_with_io_deadline("test", Duration::from_secs(5), || Ok(42));
assert_eq!(result.expect("fast closure returns Ok"), 42);
}
#[test]
fn call_with_brokerv2_deadline_stress_repeated_timeouts() {
const ATTEMPTS: usize = 32;
const DEADLINE: Duration = Duration::from_millis(10);
const CLOSURE_STALL: Duration = Duration::from_secs(2);
const WALL_BUDGET: Duration = Duration::from_secs(5);
let start = std::time::Instant::now();
for _ in 0..ATTEMPTS {
let result: Result<(), BrokerV2Error> = call_with_brokerv2_deadline(DEADLINE, || {
std::thread::sleep(CLOSURE_STALL);
Ok(())
});
match result {
Err(BrokerV2Error::Io(io)) if io.kind() == std::io::ErrorKind::TimedOut => {}
other => panic!("expected Io(TimedOut), got: {other:?}"),
}
}
let elapsed = start.elapsed();
assert!(
elapsed < WALL_BUDGET,
"{ATTEMPTS} repeated timeouts took {elapsed:?}; expected under {WALL_BUDGET:?}"
);
}
#[test]
fn call_with_brokerv2_deadline_fires_on_slow_closure() {
let result: Result<(), BrokerV2Error> =
call_with_brokerv2_deadline(Duration::from_millis(50), || {
std::thread::sleep(Duration::from_millis(500));
Ok(())
});
match result.expect_err("slow closure must time out") {
BrokerV2Error::Io(io) => {
assert_eq!(io.kind(), std::io::ErrorKind::TimedOut);
assert!(
io.to_string().contains("exceeded deadline"),
"io error message should self-document: {io}"
);
}
other => panic!("expected BrokerV2Error::Io(TimedOut), got: {other:?}"),
}
}
#[test]
fn connect_v2_broker_no_broker_returns_dial_error() {
let err = connect_v2_broker("0.0.0").expect_err("no broker => Dial error");
match err {
BrokerV2Error::Dial { socket_path, .. } => {
let v2_marker = if cfg!(windows) {
"rpb-v2-zccache-"
} else {
"broker-v2"
};
assert!(
socket_path.contains(v2_marker),
"Dial socket_path should reference the v2 broker namespace \
(expected substring `{v2_marker}`), got: {socket_path}"
);
}
BrokerV2Error::Sid(_) => {}
other => panic!("expected BrokerV2Error::Dial or Sid, got: {other:?}"),
}
}
#[test]
fn adopt_v2_session_no_broker_returns_typed_error() {
let err = adopt_v2_session("0.0.0").expect_err("no broker => error");
match err {
BrokerV2Error::Dial { .. } | BrokerV2Error::Sid(_) => {}
other => panic!("expected Dial or Sid, got: {other:?}"),
}
}
#[test]
fn into_backend_io_v2_no_broker_returns_typed_error() {
let err = into_backend_io_v2("0.0.0").expect_err("no broker => error");
match err {
BrokerV2Error::Dial { .. } | BrokerV2Error::Sid(_) => {}
other => panic!("expected Dial or Sid, got: {other:?}"),
}
}
#[test]
fn probe_local_socket_no_listener_returns_err() {
let endpoint = if cfg!(windows) {
r"\\.\pipe\zccache-slice11-probe-no-listener"
} else {
"/tmp/zccache-slice11-probe-no-listener.sock"
};
let err = probe_local_socket(endpoint).expect_err("no listener => Err");
assert!(
!err.to_string().is_empty(),
"io error should carry a message"
);
}
}