use std::collections::HashMap;
use std::fmt;
use std::sync::{Arc, OnceLock, RwLock};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ServiceEndpoint(Arc<str>);
impl ServiceEndpoint {
pub const HOST_KERNEL_ID: &'static str = "host-kernel";
pub fn new(value: impl Into<String>) -> Result<Self, ServiceEndpointError> {
let value = value.into();
if value.is_empty() {
return Err(ServiceEndpointError::Empty);
}
if value.trim() != value {
return Err(ServiceEndpointError::SurroundingWhitespace);
}
if value.chars().any(char::is_control) {
return Err(ServiceEndpointError::ControlCharacter);
}
Ok(Self(Arc::from(value)))
}
#[must_use]
pub fn host_kernel() -> Self {
Self(Arc::from(Self::HOST_KERNEL_ID))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn is_host_kernel(&self) -> bool {
self.as_str() == Self::HOST_KERNEL_ID
}
}
impl fmt::Display for ServiceEndpoint {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServiceEndpointError {
Empty,
SurroundingWhitespace,
ControlCharacter,
}
impl fmt::Display for ServiceEndpointError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Empty => "service endpoint id cannot be empty",
Self::SurroundingWhitespace => {
"service endpoint id cannot contain surrounding whitespace"
}
Self::ControlCharacter => "service endpoint id cannot contain control characters",
})
}
}
impl std::error::Error for ServiceEndpointError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ServiceLocation {
Local,
Remote {
endpoint: ServiceEndpoint,
},
}
#[derive(Default, Debug)]
pub struct ServiceLocationMap {
entries: RwLock<HashMap<String, ServiceLocation>>,
}
impl ServiceLocationMap {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn mark_local(&self, plugin_id: &str) {
let mut guard = self
.entries
.write()
.expect("service location map lock poisoned");
guard.insert(plugin_id.to_string(), ServiceLocation::Local);
}
pub fn mark_remote(&self, plugin_id: &str) {
self.mark_remote_endpoint(plugin_id, ServiceEndpoint::host_kernel());
}
pub fn mark_remote_endpoint(&self, plugin_id: &str, endpoint: ServiceEndpoint) {
let mut guard = self
.entries
.write()
.expect("service location map lock poisoned");
match guard.get(plugin_id) {
Some(ServiceLocation::Local) => {}
Some(ServiceLocation::Remote { .. }) | None => {
guard.insert(plugin_id.to_string(), ServiceLocation::Remote { endpoint });
}
}
}
#[must_use]
pub fn get(&self, plugin_id: &str) -> Option<ServiceLocation> {
self.entries
.read()
.expect("service location map lock poisoned")
.get(plugin_id)
.cloned()
}
pub fn clear(&self) {
self.entries
.write()
.expect("service location map lock poisoned")
.clear();
}
#[must_use]
pub fn len(&self) -> usize {
self.entries
.read()
.expect("service location map lock poisoned")
.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[must_use]
pub fn global_service_locations() -> Arc<ServiceLocationMap> {
static GLOBAL: OnceLock<Arc<ServiceLocationMap>> = OnceLock::new();
GLOBAL
.get_or_init(|| Arc::new(ServiceLocationMap::new()))
.clone()
}
#[cfg(test)]
mod tests {
use super::{
ServiceEndpoint, ServiceEndpointError, ServiceLocation, ServiceLocationMap,
global_service_locations,
};
use std::sync::Arc;
#[test]
fn endpoint_validation_preserves_opaque_identity() {
let endpoint = ServiceEndpoint::new("region-a/server-2").unwrap();
assert_eq!(endpoint.as_str(), "region-a/server-2");
assert!(!endpoint.is_host_kernel());
assert_eq!(ServiceEndpoint::new(""), Err(ServiceEndpointError::Empty));
assert_eq!(
ServiceEndpoint::new(" endpoint "),
Err(ServiceEndpointError::SurroundingWhitespace)
);
assert_eq!(
ServiceEndpoint::new("endpoint\n"),
Err(ServiceEndpointError::SurroundingWhitespace)
);
}
#[test]
fn mark_local_records_local_location() {
let map = ServiceLocationMap::new();
map.mark_local("bmux.contexts");
assert_eq!(map.get("bmux.contexts"), Some(ServiceLocation::Local));
}
#[test]
fn mark_remote_uses_host_kernel_compatibility_endpoint() {
let map = ServiceLocationMap::new();
map.mark_remote("bmux.contexts");
assert_eq!(
map.get("bmux.contexts"),
Some(ServiceLocation::Remote {
endpoint: ServiceEndpoint::host_kernel()
})
);
}
#[test]
fn explicit_remote_endpoint_can_be_reselected() {
let map = ServiceLocationMap::new();
map.mark_remote_endpoint("bmux.contexts", ServiceEndpoint::new("endpoint-a").unwrap());
map.mark_remote_endpoint("bmux.contexts", ServiceEndpoint::new("endpoint-b").unwrap());
assert_eq!(
map.get("bmux.contexts"),
Some(ServiceLocation::Remote {
endpoint: ServiceEndpoint::new("endpoint-b").unwrap()
})
);
}
#[test]
fn remote_routes_do_not_override_local() {
let map = ServiceLocationMap::new();
map.mark_local("bmux.contexts");
map.mark_remote_endpoint("bmux.contexts", ServiceEndpoint::new("endpoint-a").unwrap());
assert_eq!(map.get("bmux.contexts"), Some(ServiceLocation::Local));
}
#[test]
fn mark_local_overrides_remote() {
let map = ServiceLocationMap::new();
map.mark_remote("bmux.contexts");
map.mark_local("bmux.contexts");
assert_eq!(map.get("bmux.contexts"), Some(ServiceLocation::Local));
}
#[test]
fn get_missing_plugin_returns_none() {
let map = ServiceLocationMap::new();
assert_eq!(map.get("bmux.unknown"), None);
}
#[test]
fn concurrent_mark_and_get_is_safe() {
use std::thread;
let map = Arc::new(ServiceLocationMap::new());
let mut handles = Vec::new();
for i in 0..8 {
let map = Arc::clone(&map);
handles.push(thread::spawn(move || {
for j in 0..500 {
let id = format!("plugin-{i}-{}", j % 16);
if j % 2 == 0 {
map.mark_local(&id);
} else {
map.mark_remote(&id);
}
let _ = map.get(&id);
}
}));
}
for handle in handles {
handle.join().unwrap();
}
assert!(!map.is_empty());
}
#[test]
fn clear_resets_all_entries() {
let map = ServiceLocationMap::new();
map.mark_local("a");
map.mark_remote("b");
assert_eq!(map.len(), 2);
map.clear();
assert!(map.is_empty());
}
#[test]
fn global_service_locations_returns_same_instance() {
let a = global_service_locations();
let b = global_service_locations();
assert!(Arc::ptr_eq(&a, &b));
}
}