use std::error::Error;
use std::fmt;
use std::io::{self, Read, Write};
use running_process::broker::client as backend;
pub const DISABLE_ENV: &str = backend::RUNNING_PROCESS_DISABLE_ENV;
pub const DISABLE_VALUE: &str = backend::RUNNING_PROCESS_DISABLE_VALUE;
pub const FAKE_BACKEND_ENV: &str = backend::RUNNING_PROCESS_FAKE_BACKEND_ENV;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum BackendRoute {
HelloSkip,
BrokerNegotiated,
HandlePassed,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum RefusalCode {
Unspecified,
VersionUnsupported,
ServiceUnknown,
BackendSpawnFailed,
RateLimited,
ShuttingDown,
PeerRejected,
Internal,
VersionBlocked,
FdPressure,
Unrecognized(i32),
}
impl RefusalCode {
#[must_use]
pub const fn from_wire(code: i32) -> Self {
match code {
0 => Self::Unspecified,
1 => Self::VersionUnsupported,
2 => Self::ServiceUnknown,
3 => Self::BackendSpawnFailed,
4 => Self::RateLimited,
5 => Self::ShuttingDown,
6 => Self::PeerRejected,
7 => Self::Internal,
8 => Self::VersionBlocked,
9 => Self::FdPressure,
other => Self::Unrecognized(other),
}
}
#[must_use]
pub const fn to_wire(self) -> i32 {
match self {
Self::Unspecified => 0,
Self::VersionUnsupported => 1,
Self::ServiceUnknown => 2,
Self::BackendSpawnFailed => 3,
Self::RateLimited => 4,
Self::ShuttingDown => 5,
Self::PeerRejected => 6,
Self::Internal => 7,
Self::VersionBlocked => 8,
Self::FdPressure => 9,
Self::Unrecognized(code) => code,
}
}
}
impl From<i32> for RefusalCode {
fn from(code: i32) -> Self {
Self::from_wire(code)
}
}
impl From<RefusalCode> for i32 {
fn from(code: RefusalCode) -> Self {
code.to_wire()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum RefusalKind {
VersionUnsupported,
VersionBlocked,
ServiceUnknown,
RateLimited,
ShuttingDown,
Other(RefusalCode),
}
impl RefusalKind {
#[must_use]
pub const fn from_code(code: RefusalCode) -> Self {
match code {
RefusalCode::VersionUnsupported => Self::VersionUnsupported,
RefusalCode::VersionBlocked => Self::VersionBlocked,
RefusalCode::ServiceUnknown => Self::ServiceUnknown,
RefusalCode::RateLimited => Self::RateLimited,
RefusalCode::ShuttingDown => Self::ShuttingDown,
other => Self::Other(other),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BrokerRefusal {
code: RefusalCode,
reason: String,
retry_after_ms: u64,
}
impl BrokerRefusal {
#[must_use]
pub fn new(code: RefusalCode, reason: impl Into<String>, retry_after_ms: u64) -> Self {
Self {
code,
reason: reason.into(),
retry_after_ms,
}
}
#[must_use]
pub fn code(&self) -> RefusalCode {
self.code
}
#[must_use]
pub fn kind(&self) -> RefusalKind {
RefusalKind::from_code(self.code)
}
#[must_use]
pub fn reason(&self) -> &str {
&self.reason
}
#[must_use]
pub fn retry_after_ms(&self) -> u64 {
self.retry_after_ms
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum BrokerClientError {
Disabled,
InvalidDisableValue {
value: String,
},
BrokerConnect {
endpoint: String,
source: io::Error,
},
BackendConnect(io::Error),
Refused(BrokerRefusal),
Protocol {
detail: String,
},
}
impl BrokerClientError {
#[must_use]
pub fn refused(code: RefusalCode, reason: impl Into<String>, retry_after_ms: u64) -> Self {
Self::Refused(BrokerRefusal::new(code, reason, retry_after_ms))
}
#[must_use]
pub fn refusal(&self) -> Option<&BrokerRefusal> {
match self {
Self::Refused(refusal) => Some(refusal),
_ => None,
}
}
#[must_use]
pub fn refusal_kind(&self) -> Option<RefusalKind> {
self.refusal().map(BrokerRefusal::kind)
}
}
impl fmt::Display for BrokerClientError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Disabled => write!(
formatter,
"broker disabled via {DISABLE_ENV}={DISABLE_VALUE}; use the direct path"
),
Self::InvalidDisableValue { value } => write!(
formatter,
"{DISABLE_ENV} must be unset or {DISABLE_VALUE}, got {value:?}"
),
Self::BrokerConnect { endpoint, source } => {
write!(formatter, "failed to connect to broker {endpoint:?}: {source}")
}
Self::BackendConnect(source) => {
write!(formatter, "failed to connect to negotiated backend: {source}")
}
Self::Refused(refusal) => write!(
formatter,
"broker refused Hello: {} ({:?}, retry_after_ms={})",
refusal.reason, refusal.code, refusal.retry_after_ms
),
Self::Protocol { detail } => write!(formatter, "broker protocol failure: {detail}"),
}
}
}
impl Error for BrokerClientError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::BrokerConnect { source, .. } | Self::BackendConnect(source) => Some(source),
_ => None,
}
}
}
fn client_error(error: backend::BrokerClientError, broker_endpoint: &str) -> BrokerClientError {
match error {
backend::BrokerClientError::BrokerConnect(source) => BrokerClientError::BrokerConnect {
endpoint: broker_endpoint.to_owned(),
source,
},
backend::BrokerClientError::BackendConnect(source) => {
BrokerClientError::BackendConnect(source)
}
backend::BrokerClientError::Refused {
code,
reason,
retry_after_ms,
} => BrokerClientError::refused(RefusalCode::from_wire(code as i32), reason, retry_after_ms),
other => BrokerClientError::Protocol {
detail: other.to_string(),
},
}
}
fn route(route: backend::BackendConnectionRoute) -> BackendRoute {
match route {
backend::BackendConnectionRoute::HelloSkip => BackendRoute::HelloSkip,
backend::BackendConnectionRoute::BrokerNegotiated => BackendRoute::BrokerNegotiated,
backend::BackendConnectionRoute::HandlePassed => BackendRoute::HandlePassed,
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BackendRequest {
broker_endpoint: String,
service_name: String,
wanted_version: String,
self_version: String,
cached_backend_endpoint: Option<String>,
client_version: String,
client_library: Option<(String, String)>,
client_keepalive_secs: u64,
}
impl BackendRequest {
#[must_use]
pub fn new(
broker_endpoint: impl Into<String>,
service_name: impl Into<String>,
wanted_version: impl Into<String>,
self_version: impl Into<String>,
) -> Self {
Self {
broker_endpoint: broker_endpoint.into(),
service_name: service_name.into(),
wanted_version: wanted_version.into(),
self_version: self_version.into(),
cached_backend_endpoint: None,
client_version: String::new(),
client_library: None,
client_keepalive_secs: 0,
}
}
#[must_use]
pub fn cached_backend_endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.cached_backend_endpoint = Some(endpoint.into());
self
}
#[must_use]
pub fn client_version(mut self, version: impl Into<String>) -> Self {
self.client_version = version.into();
self
}
#[must_use]
pub fn client_library(mut self, name: impl Into<String>, version: impl Into<String>) -> Self {
self.client_library = Some((name.into(), version.into()));
self
}
#[must_use]
pub fn client_keepalive_secs(mut self, seconds: u64) -> Self {
self.client_keepalive_secs = seconds;
self
}
#[must_use]
pub fn broker_endpoint(&self) -> &str {
&self.broker_endpoint
}
#[must_use]
pub fn service_name(&self) -> &str {
&self.service_name
}
}
#[derive(Debug)]
pub struct BackendConnection {
inner: backend::BackendConnection,
}
impl BackendConnection {
#[must_use]
pub fn route(&self) -> BackendRoute {
route(self.inner.route)
}
#[must_use]
pub fn endpoint(&self) -> &str {
&self.inner.endpoint
}
}
impl Read for BackendConnection {
fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
self.inner.stream.read(buffer)
}
}
impl Write for BackendConnection {
fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
self.inner.stream.write(buffer)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.stream.flush()
}
}
pub fn broker_disabled() -> Result<bool, BrokerClientError> {
backend::broker_disabled_by_env()
.map_err(|error| BrokerClientError::InvalidDisableValue { value: error.value })
}
pub fn connect_backend(request: &BackendRequest) -> Result<BackendConnection, BrokerClientError> {
if broker_disabled()? {
return Err(BrokerClientError::Disabled);
}
let mut backend_request = backend::ConnectBackendRequest::new(
&request.broker_endpoint,
&request.service_name,
&request.wanted_version,
&request.self_version,
);
backend_request.cached_backend_endpoint = request.cached_backend_endpoint.as_deref();
backend_request.client_version = &request.client_version;
if let Some((name, version)) = &request.client_library {
backend_request.client_lib_name = name;
backend_request.client_lib_version = version;
}
backend_request.client_keepalive_secs = request.client_keepalive_secs;
backend::connect_to_backend(backend_request)
.map(|inner| BackendConnection { inner })
.map_err(|error| client_error(error, &request.broker_endpoint))
}
#[cfg(test)]
mod tests {
use super::*;
use running_process::broker::protocol::ErrorCode;
#[test]
fn backend_refusals_convert_to_matching_owned_codes_and_kinds() {
for wire in 0..=30 {
let Ok(code) = ErrorCode::try_from(wire) else {
assert!(matches!(
RefusalCode::from_wire(wire),
RefusalCode::Unrecognized(value) if value == wire
));
continue;
};
let expected_kind = backend::RefusalKind::from_code(code);
let converted = client_error(
backend::BrokerClientError::Refused {
code,
reason: "test".to_owned(),
retry_after_ms: 1234,
},
"broker",
);
let refusal = converted.refusal().expect("refusal stays a refusal");
assert_eq!(refusal.code().to_wire(), wire);
assert_eq!(refusal.reason(), "test");
assert_eq!(refusal.retry_after_ms(), 1234);
let owned_kind = refusal.kind();
let agrees = match (expected_kind, owned_kind) {
(backend::RefusalKind::VersionUnsupported, RefusalKind::VersionUnsupported)
| (backend::RefusalKind::VersionBlocked, RefusalKind::VersionBlocked)
| (backend::RefusalKind::ServiceUnknown, RefusalKind::ServiceUnknown)
| (backend::RefusalKind::RateLimited, RefusalKind::RateLimited)
| (backend::RefusalKind::ShuttingDown, RefusalKind::ShuttingDown) => true,
(backend::RefusalKind::Other(backend_code), RefusalKind::Other(owned_code)) => {
backend_code as i32 == owned_code.to_wire()
}
_ => false,
};
assert!(agrees, "wire code {wire}: {expected_kind:?} vs {owned_kind:?}");
}
}
#[test]
fn backend_transport_and_protocol_failures_are_not_refusals() {
let broker = client_error(
backend::BrokerClientError::BrokerConnect(io::Error::from(io::ErrorKind::NotFound)),
"broker.sock",
);
assert!(matches!(
&broker,
BrokerClientError::BrokerConnect { endpoint, .. } if endpoint == "broker.sock"
));
let backend_dial = client_error(
backend::BrokerClientError::BackendConnect(io::Error::from(
io::ErrorKind::ConnectionRefused,
)),
"broker.sock",
);
assert!(matches!(backend_dial, BrokerClientError::BackendConnect(_)));
let protocol = client_error(
backend::BrokerClientError::MissingHelloReplyResult,
"broker.sock",
);
assert!(matches!(protocol, BrokerClientError::Protocol { .. }));
for error in [broker, backend_dial, protocol] {
assert_eq!(error.refusal_kind(), None);
}
}
#[test]
fn backend_routes_convert_one_to_one() {
assert_eq!(
route(backend::BackendConnectionRoute::HelloSkip),
BackendRoute::HelloSkip
);
assert_eq!(
route(backend::BackendConnectionRoute::BrokerNegotiated),
BackendRoute::BrokerNegotiated
);
assert_eq!(
route(backend::BackendConnectionRoute::HandlePassed),
BackendRoute::HandlePassed
);
}
}