use std::collections::BTreeMap;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::MutexGuard;
use std::time::Duration;
use camel_api::Body;
use camel_api::CamelError;
use camel_api::Exchange;
use camel_api::Message;
use camel_api::Value;
use camel_component_api::NoOpComponentContext;
use camel_core::CamelContext;
use futures::future::BoxFuture;
use tokio::sync::Mutex as AsyncMutex;
use tokio::sync::mpsc;
use tower::ServiceExt;
#[cfg(feature = "http")]
pub mod http;
#[derive(Debug, Clone, PartialEq)]
pub struct OutgoingMessage {
pub body: Value,
pub headers: BTreeMap<String, Value>,
pub method: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct IncomingMessage {
pub body: Value,
pub headers: BTreeMap<String, Value>,
pub status: Option<u16>,
pub method: Option<String>,
pub path: Option<String>,
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum TransportError {
#[error("no partner adapter bound for endpoint {endpoint}")]
Unbound {
endpoint: String,
},
#[error("{message}")]
Other {
message: String,
},
#[error("send did not complete within {after:?}")]
Deadline {
after: Duration,
},
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
#[error("nothing reached {endpoint} within {deadline:?} (waited {elapsed:?})")]
pub struct ReceiveTimeout {
pub endpoint: String,
pub deadline: Duration,
pub elapsed: Duration,
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum ReceiveError {
#[error("{0}")]
Timeout(ReceiveTimeout),
#[error("{0}")]
Transport(TransportError),
}
pub trait PartnerAdapter: Send + Sync {
fn send<'a>(
&'a self,
lane_key: &'a str,
target_uri: &'a str,
msg: OutgoingMessage,
) -> BoxFuture<'a, Result<(), TransportError>> {
let _ = (lane_key, target_uri, msg);
Box::pin(async {
Err(TransportError::Other {
message: "adapter does not implement client-role sends".to_string(),
})
})
}
fn receive<'a>(
&'a self,
lane_key: &'a str,
source_uri: &'a str,
deadline: Duration,
) -> BoxFuture<'a, Result<IncomingMessage, ReceiveError>>;
fn bound_authority(&self) -> Option<String> {
None
}
#[cfg(feature = "http")]
fn recorded_requests(&self) -> Vec<http::HttpWireRequest> {
Vec::new()
}
}
pub struct PartnerRouter {
adapters: BTreeMap<String, Box<dyn PartnerAdapter>>,
#[cfg(feature = "http")]
client_lane: Arc<http::ClientLane>,
}
impl PartnerRouter {
pub fn new(adapters: BTreeMap<String, Box<dyn PartnerAdapter>>) -> Self {
Self {
adapters,
#[cfg(feature = "http")]
client_lane: Arc::new(http::ClientLane::new()),
}
}
pub fn adapter(&self, key: &str) -> Option<&dyn PartnerAdapter> {
self.adapters.get(key).map(|boxed| boxed.as_ref())
}
#[cfg(feature = "http")]
pub fn recorded_requests(&self, key: &str) -> Vec<http::HttpWireRequest> {
self.adapters
.get(key)
.map(|adapter| adapter.recorded_requests())
.unwrap_or_default()
}
pub fn authorities(&self) -> Vec<(String, String)> {
self.adapters
.iter()
.filter_map(|(key, adapter)| Some((key.clone(), adapter.bound_authority()?)))
.collect()
}
pub fn lane_key_for(&self, declared: &str, interpolated: &str) -> Option<String> {
if self.adapters.contains_key(declared) {
return Some(declared.to_string());
}
let authority = uri_authority(interpolated)?;
self.authorities()
.into_iter()
.find(|(_, bound)| bound == authority)
.map(|(key, _)| key)
}
pub fn wire_target(&self, declared_key: &str, interpolated_uri: &str) -> Option<String> {
if let Some(adapter) = self.adapters.get(declared_key) {
let bound = adapter.bound_authority()?;
if !authority_is_port_zero(declared_key) {
return None;
}
return rewrite_authority(interpolated_uri, &bound);
}
self.partner_by_authority(interpolated_uri)
.and_then(|(_, bound)| rewrite_authority(interpolated_uri, &bound))
}
pub async fn send(
&self,
declared: &str,
interpolated: &str,
msg: OutgoingMessage,
) -> Result<(), TransportError> {
#[cfg(feature = "http")]
if interpolated.starts_with("http://") {
return self.send_http(declared, interpolated, msg).await;
}
match self.adapters.get(declared) {
Some(adapter) => adapter.send(declared, interpolated, msg).await,
None => Err(TransportError::Unbound {
endpoint: declared.to_string(),
}),
}
}
#[cfg(feature = "http")]
async fn send_http(
&self,
declared: &str,
interpolated: &str,
msg: OutgoingMessage,
) -> Result<(), TransportError> {
if let Some(adapter) = self.adapters.get(declared) {
if adapter.bound_authority().is_some() {
let target = self
.wire_target(declared, interpolated)
.unwrap_or_else(|| interpolated.to_string());
return Arc::clone(&self.client_lane)
.launch(declared, &target, msg)
.await;
}
return adapter.send(declared, interpolated, msg).await;
}
if let Some((lane_key, target)) = self
.partner_by_authority(interpolated)
.and_then(|(key, bound)| Some((key, rewrite_authority(interpolated, &bound)?)))
{
return Arc::clone(&self.client_lane)
.launch(&lane_key, &target, msg)
.await;
}
Arc::clone(&self.client_lane)
.launch(declared, interpolated, msg)
.await
}
pub async fn receive(
&self,
declared: &str,
interpolated: &str,
deadline: Duration,
) -> Result<IncomingMessage, ReceiveError> {
let lane_key = self
.lane_key_for(declared, interpolated)
.unwrap_or_else(|| declared.to_string());
#[cfg(feature = "http")]
if let Some(parked) = self.client_lane.take(&lane_key) {
return self
.client_lane
.await_parked(interpolated, deadline, parked)
.await;
}
match self.adapters.get(lane_key.as_str()) {
Some(adapter) => adapter.receive(&lane_key, interpolated, deadline).await,
None => Err(ReceiveError::Transport(TransportError::Unbound {
endpoint: declared.to_string(),
})),
}
}
fn partner_by_authority(&self, uri: &str) -> Option<(String, String)> {
let authority = uri_authority(uri)?;
self.authorities()
.into_iter()
.find(|(_, bound)| bound == authority)
}
}
fn uri_authority(uri: &str) -> Option<&str> {
let start = uri.find("://")? + 3;
let rest = &uri[start..];
let end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
Some(&rest[..end])
}
fn authority_is_port_zero(uri: &str) -> bool {
let Some(authority) = uri_authority(uri) else {
return false;
};
match authority.rsplit_once(':') {
Some((_, port)) => port == "0",
None => false,
}
}
fn rewrite_authority(uri: &str, authority: &str) -> Option<String> {
let rest_start = uri.find("://")? + 3;
let rest = &uri[rest_start..];
let path_start = rest.find(['/', '?', '#']).unwrap_or(rest.len());
let mut rewritten = String::with_capacity(uri.len());
rewritten.push_str(&uri[..rest_start]);
rewritten.push_str(authority);
rewritten.push_str(&rest[path_start..]);
Some(rewritten)
}
#[derive(Debug, Clone, PartialEq)]
pub struct RecordedSend {
pub endpoint: String,
pub message: OutgoingMessage,
}
#[derive(Clone)]
pub struct FakeRecorder {
sent: Arc<Mutex<Vec<RecordedSend>>>,
}
impl FakeRecorder {
pub fn sent_messages(&self) -> Vec<RecordedSend> {
lock_through(&self.sent).clone()
}
}
struct FakeInner {
fail_send: Option<String>,
fail_receive: Option<String>,
sent: Arc<Mutex<Vec<RecordedSend>>>,
queue_rx: AsyncMutex<mpsc::Receiver<IncomingMessage>>,
}
#[derive(Clone)]
pub struct FakeAdapter {
inner: Arc<FakeInner>,
}
impl FakeAdapter {
pub fn scripted(queue: Vec<IncomingMessage>) -> Self {
let capacity = queue.len().max(1);
let (queue_tx, queue_rx) = mpsc::channel(capacity);
for message in queue {
if queue_tx.try_send(message).is_err() {
break;
}
}
drop(queue_tx);
Self {
inner: Arc::new(FakeInner {
fail_send: None,
fail_receive: None,
sent: Arc::new(Mutex::new(Vec::new())),
queue_rx: AsyncMutex::new(queue_rx),
}),
}
}
pub fn failing_send(reason: impl Into<String>) -> Self {
Self {
inner: Arc::new(FakeInner {
fail_send: Some(reason.into()),
fail_receive: None,
sent: Arc::new(Mutex::new(Vec::new())),
queue_rx: AsyncMutex::new(mpsc::channel(1).1),
}),
}
}
pub fn failing_receive(reason: impl Into<String>) -> Self {
Self {
inner: Arc::new(FakeInner {
fail_send: None,
fail_receive: Some(reason.into()),
sent: Arc::new(Mutex::new(Vec::new())),
queue_rx: AsyncMutex::new(mpsc::channel(1).1),
}),
}
}
pub fn recorder(&self) -> FakeRecorder {
FakeRecorder {
sent: Arc::clone(&self.inner.sent),
}
}
}
impl PartnerAdapter for FakeAdapter {
fn send<'a>(
&'a self,
lane_key: &'a str,
_target_uri: &'a str,
msg: OutgoingMessage,
) -> BoxFuture<'a, Result<(), TransportError>> {
Box::pin(async move {
if let Some(reason) = &self.inner.fail_send {
return Err(TransportError::Other {
message: reason.clone(),
});
}
lock_through(&self.inner.sent).push(RecordedSend {
endpoint: lane_key.to_string(),
message: msg,
});
Ok(())
})
}
fn receive<'a>(
&'a self,
_lane_key: &'a str,
source_uri: &'a str,
deadline: Duration,
) -> BoxFuture<'a, Result<IncomingMessage, ReceiveError>> {
Box::pin(async move {
if let Some(reason) = &self.inner.fail_receive {
return Err(ReceiveError::Transport(TransportError::Other {
message: reason.clone(),
}));
}
let mut queue_rx = self.inner.queue_rx.lock().await;
let started = tokio::time::Instant::now();
let outcome = tokio::time::timeout(deadline, queue_rx.recv()).await;
match outcome {
Ok(Some(message)) => Ok(message),
Ok(None) | Err(_) => Err(ReceiveError::Timeout(ReceiveTimeout {
endpoint: source_uri.to_string(),
deadline,
elapsed: started.elapsed(),
})),
}
})
}
}
fn lock_through<T>(lock: &Mutex<T>) -> MutexGuard<'_, T> {
lock.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
const STIMULUS_RETRY_SLEEP: Duration = Duration::from_millis(20);
const STIMULUS_RETRY_DEADLINE: Duration = Duration::from_secs(1);
pub struct DirectStimulus {
ctx: Arc<AsyncMutex<CamelContext>>,
}
impl DirectStimulus {
pub fn new(ctx: Arc<AsyncMutex<CamelContext>>) -> Self {
Self { ctx }
}
}
impl PartnerAdapter for DirectStimulus {
fn send<'a>(
&'a self,
lane_key: &'a str,
_target_uri: &'a str,
msg: OutgoingMessage,
) -> BoxFuture<'a, Result<(), TransportError>> {
Box::pin(async move {
let exchange = stimulus_exchange(msg);
let transport = |detail: String| TransportError::Other { message: detail };
let deadline = tokio::time::Instant::now() + STIMULUS_RETRY_DEADLINE;
loop {
let producer = {
let ctx = self.ctx.lock().await;
let producer_ctx = ctx.producer_context();
let component = ctx
.registry()
.get("direct")
.ok_or_else(|| transport("direct component not registered".to_string()))?;
let endpoint = component.create_endpoint(lane_key, &*ctx).map_err(|e| {
transport(format!("failed to create endpoint {lane_key}: {e}"))
})?;
endpoint
.create_producer(Arc::new(NoOpComponentContext), &producer_ctx)
.map_err(|e| {
transport(format!("failed to create producer for {lane_key}: {e}"))
})?
};
match producer.oneshot(exchange.clone()).await {
Ok(_reply) => return Ok(()),
Err(e) => {
let is_startup_race = matches!(e, CamelError::EndpointCreationFailed(_));
if is_startup_race && tokio::time::Instant::now() < deadline {
tokio::time::sleep(STIMULUS_RETRY_SLEEP).await;
continue;
}
return Err(transport(format!("send to {lane_key} failed: {e}")));
}
}
}
})
}
fn receive<'a>(
&'a self,
_lane_key: &'a str,
source_uri: &'a str,
_deadline: Duration,
) -> BoxFuture<'a, Result<IncomingMessage, ReceiveError>> {
Box::pin(async move {
Err(ReceiveError::Transport(TransportError::Other {
message: format!(
"{source_uri} is a context stimulus endpoint; receive is a partner role"
),
}))
})
}
}
fn stimulus_exchange(msg: OutgoingMessage) -> Exchange {
let body = match &msg.body {
Value::Null => Body::Empty,
Value::String(text) => Body::Text(text.clone()),
other => Body::Json(other.clone()),
};
let mut message = Message::new(body);
for (name, value) in &msg.headers {
message.set_header(name.clone(), value.clone());
}
Exchange::new(message)
}