use bytes::Bytes;
use std::collections::HashMap;
use std::future::Future;
use std::sync::Arc;
use arc_swap::ArcSwap;
use crate::pipe::ProximaError;
use crate::pipe::SendPipe;
use crate::pipe::handler::{PipeHandle, into_handle};
use crate::pipe::request::{Request, Response};
pub struct SwappablePipe {
delegate: ArcSwap<PipeHandle>,
}
impl SwappablePipe {
#[must_use]
pub fn new(initial: PipeHandle) -> Self {
Self {
delegate: ArcSwap::from_pointee(initial),
}
}
pub fn swap(&self, new: PipeHandle) {
self.delegate.store(Arc::new(new));
}
#[must_use]
pub fn current(&self) -> PipeHandle {
PipeHandle::clone(&self.delegate.load_full())
}
}
impl SendPipe for SwappablePipe {
type In = Request<Bytes>;
type Out = Response<Bytes>;
type Err = ProximaError;
fn call(
&self,
request: Request<Bytes>,
) -> impl Future<Output = Result<Response<Bytes>, ProximaError>> + Send {
let handle = PipeHandle::clone(&self.delegate.load_full());
async move { SendPipe::call(&handle, request).await }
}
}
pub struct SwapRegistry {
nodes: ArcSwap<HashMap<String, Arc<SwappablePipe>>>,
}
impl Default for SwapRegistry {
fn default() -> Self {
Self {
nodes: ArcSwap::from_pointee(HashMap::new()),
}
}
}
impl SwapRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register(&self, node_id: impl Into<String>, initial: PipeHandle) -> PipeHandle {
let node_id = node_id.into();
let node = Arc::new(SwappablePipe::new(initial));
let handle = node.clone();
loop {
let current = self.nodes.load_full();
let mut next: HashMap<String, Arc<SwappablePipe>> = (*current).clone();
next.insert(node_id.clone(), node.clone());
let prev = self.nodes.compare_and_swap(¤t, Arc::new(next));
if Arc::ptr_eq(&prev, ¤t) {
break;
}
}
into_handle(SwappableHandle { inner: handle })
}
pub fn swap(&self, node_id: &str, new: PipeHandle) -> Result<(), ProximaError> {
let snapshot = self.nodes.load_full();
let Some(node) = snapshot.get(node_id) else {
return Err(ProximaError::Config(format!(
"swap: no node registered for id \"{node_id}\""
)));
};
node.swap(new);
Ok(())
}
#[must_use]
pub fn node_ids(&self) -> Vec<String> {
self.nodes.load_full().keys().cloned().collect()
}
}
struct SwappableHandle {
inner: Arc<SwappablePipe>,
}
impl SendPipe for SwappableHandle {
type In = Request<Bytes>;
type Out = Response<Bytes>;
type Err = ProximaError;
fn call(
&self,
request: Request<Bytes>,
) -> impl Future<Output = Result<Response<Bytes>, ProximaError>> + Send {
SendPipe::call(self.inner.as_ref(), request)
}
}
#[cfg(all(test, not(loom)))]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use bytes::Bytes;
struct Constant {
body: &'static [u8],
}
impl SendPipe for Constant {
type In = Request<Bytes>;
type Out = Response<Bytes>;
type Err = ProximaError;
fn call(
&self,
_request: Request<Bytes>,
) -> impl Future<Output = Result<Response<Bytes>, ProximaError>> + Send {
let body = self.body;
async move { Ok(Response::ok(Bytes::from_static(body))) }
}
}
fn fresh_request() -> Request<Bytes> {
Request::builder()
.method("GET")
.path("/x")
.body("")
.build()
.expect("builder")
}
#[proxima::test]
async fn swap_replaces_delegate_for_subsequent_calls() {
let registry = SwapRegistry::new();
let handle = registry.register("downstream-proxy", into_handle(Constant { body: b"v1" }));
let response = SendPipe::call(&handle, fresh_request()).await.expect("v1");
let body = response.collect_body().await.expect("body");
assert_eq!(&body[..], b"v1");
registry
.swap("downstream-proxy", into_handle(Constant { body: b"v2" }))
.expect("swap");
let response = SendPipe::call(&handle, fresh_request()).await.expect("v2");
let body = response.collect_body().await.expect("body");
assert_eq!(&body[..], b"v2");
}
#[proxima::test(runtime = "tokio")]
async fn in_flight_call_finishes_on_pre_swap_delegate() {
struct Held {
label: &'static [u8],
notify: Arc<crate::sync::Notify>,
}
impl SendPipe for Held {
type In = Request<Bytes>;
type Out = Response<Bytes>;
type Err = ProximaError;
fn call(
&self,
_request: Request<Bytes>,
) -> impl Future<Output = Result<Response<Bytes>, ProximaError>> + Send {
let notify = self.notify.clone();
let label = self.label;
async move {
notify.notified().await;
Ok(Response::ok(Bytes::from_static(label)))
}
}
}
tokio::task::LocalSet::new()
.run_until(async move {
let notify_old = Arc::new(crate::sync::Notify::new());
let notify_new = Arc::new(crate::sync::Notify::new());
let registry = SwapRegistry::new();
let handle = registry.register(
"downstream-proxy",
into_handle(Held {
label: b"old",
notify: notify_old.clone(),
}),
);
let handle_for_old_call = handle.clone();
let in_flight_old = tokio::task::spawn_local(async move {
SendPipe::call(&handle_for_old_call, fresh_request()).await
});
for _ in 0..16 {
tokio::task::yield_now().await;
}
registry
.swap(
"downstream-proxy",
into_handle(Held {
label: b"new",
notify: notify_new.clone(),
}),
)
.expect("swap");
notify_old.notify_waiters();
let response_old = in_flight_old.await.expect("join").expect("call");
let body_old = response_old.collect_body().await.expect("body");
assert_eq!(
&body_old[..],
b"old",
"in-flight call must finish on pre-swap delegate"
);
let handle_for_new_call = handle.clone();
let in_flight_new = tokio::task::spawn_local(async move {
SendPipe::call(&handle_for_new_call, fresh_request()).await
});
for _ in 0..16 {
tokio::task::yield_now().await;
}
notify_new.notify_waiters();
let response_new = in_flight_new.await.expect("join").expect("call");
let body_new = response_new.collect_body().await.expect("body");
assert_eq!(&body_new[..], b"new", "post-swap call hits new delegate");
})
.await;
}
#[proxima::test]
async fn swap_unknown_node_returns_config_error() {
let registry = SwapRegistry::new();
let outcome = registry.swap("missing", into_handle(Constant { body: b"x" }));
assert!(matches!(outcome, Err(ProximaError::Config(_))));
}
#[test]
fn node_ids_lists_registered_nodes() {
let registry = SwapRegistry::new();
let _ = registry.register("alpha", into_handle(Constant { body: b"" }));
let _ = registry.register("beta", into_handle(Constant { body: b"" }));
let mut ids = registry.node_ids();
ids.sort();
assert_eq!(ids, vec!["alpha".to_string(), "beta".to_string()]);
}
}