use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use camel_api::{CamelError, Exchange};
use camel_component_api::InlineRouteDispatcher;
use tokio_util::sync::CancellationToken;
use tower::Service;
use crate::lifecycle::adapters::pipeline_runtime::SharedPipeline;
use crate::lifecycle::adapters::route_compiler::CANCEL_TOKEN;
use crate::lifecycle::adapters::route_helpers::{DrainGuard, ready_with_backoff};
use crate::lifecycle::cohort_activation::CohortActivationGate;
const HOP_YIELD_INTERVAL: u32 = 32;
struct DispatcherState {
route_id: String,
pipeline: SharedPipeline,
cancel: CancellationToken,
drain_in_flight: Arc<AtomicU64>,
admission: Arc<tokio::sync::Mutex<()>>,
cohort: Arc<CohortActivationGate>,
hop_budget: AtomicU32,
#[cfg(test)]
yields: AtomicU32,
}
pub(crate) struct RouteInlineDispatcher {
state: Arc<DispatcherState>,
}
impl std::fmt::Debug for RouteInlineDispatcher {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RouteInlineDispatcher")
.field("route_id", &self.state.route_id)
.finish()
}
}
impl RouteInlineDispatcher {
pub(crate) fn new(
route_id: String,
pipeline: SharedPipeline,
cancel: CancellationToken,
drain_in_flight: Arc<AtomicU64>,
cohort: Arc<CohortActivationGate>,
) -> Self {
Self {
state: Arc::new(DispatcherState {
route_id,
pipeline,
cancel,
drain_in_flight,
admission: Arc::new(tokio::sync::Mutex::new(())),
cohort,
hop_budget: AtomicU32::new(0),
#[cfg(test)]
yields: AtomicU32::new(0),
}),
}
}
#[cfg(test)]
fn hop_budget_for_test(&self) -> u32 {
self.state.hop_budget.load(Ordering::Relaxed)
}
#[cfg(test)]
fn yields_for_test(&self) -> u32 {
self.state.yields.load(Ordering::Relaxed)
}
#[cfg(test)]
fn admission_for_test(&self) -> &Arc<tokio::sync::Mutex<()>> {
&self.state.admission
}
}
impl InlineRouteDispatcher for RouteInlineDispatcher {
fn dispatch(
&self,
exchange: Exchange,
) -> Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send + 'static>> {
let state = Arc::clone(&self.state);
Box::pin(async move {
let _drain_guard = DrainGuard::new(Arc::clone(&state.drain_in_flight));
let mut pipe = state.pipeline.load().processor.clone_inner();
let admission = Arc::clone(&state.admission);
let cohort = Arc::clone(&state.cohort);
let operation_cancel = state.cancel.clone();
let operation = async move {
let _admission = admission.lock().await;
let mut cohort_rx = cohort.subscribe();
let _ = cohort_rx.wait_for(|open| *open).await;
ready_with_backoff(&mut pipe, &operation_cancel).await?;
CANCEL_TOKEN
.scope(operation_cancel, async move { pipe.call(exchange).await })
.await
};
let result = tokio::select! {
biased;
_ = state.cancel.cancelled() => Err(CamelError::ConsumerStopping),
result = operation => result,
};
if result.is_ok() {
let prev = state.hop_budget.fetch_add(1, Ordering::Relaxed);
if (prev + 1).is_multiple_of(HOP_YIELD_INTERVAL) {
#[cfg(test)]
state.yields.fetch_add(1, Ordering::Relaxed);
tokio::task::yield_now().await;
}
}
result
})
}
}
#[cfg(test)]
#[path = "inline_dispatcher_tests.rs"]
mod tests;