use std::sync::Arc;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::time::Duration;
use camel_api::{CamelError, InFlightGauge, MetricsCollector, PlatformService};
use camel_component_api::{
Component, ConsumerContext, ExchangeEnvelope, InFlightClaim, NetworkRetryPolicy,
is_retryable_camel_error,
};
use tokio::time::timeout;
use tokio_util::sync::CancellationToken;
use tracing::{debug, error, info, warn};
use crate::consumer::DelegateState;
use crate::endpoint::MasterDelegateContext;
pub(crate) const LEADER_EPOCH_PROPERTY: &str = "x-camel-leader-epoch";
pub(crate) fn spawn_epoch_bridge(
real_sender: tokio::sync::mpsc::Sender<ExchangeEnvelope>,
my_epoch: u64,
parent_cancel: CancellationToken,
in_flight: Option<Arc<InFlightGauge>>,
) -> (
tokio::sync::mpsc::Sender<ExchangeEnvelope>,
tokio::task::JoinHandle<()>,
) {
let (stamp_tx, mut stamp_rx) = tokio::sync::mpsc::channel::<ExchangeEnvelope>(128);
let handle = tokio::spawn(async move {
loop {
tokio::select! {
_ = parent_cancel.cancelled() => break,
env = stamp_rx.recv() => {
match env {
Some(mut env) => {
stamp_epoch(&mut env, my_epoch);
bridge_mint_claim(&mut env, &in_flight);
if real_sender.send(env).await.is_err() {
break;
}
}
None => {
while let Ok(mut env) = stamp_rx.try_recv() {
stamp_epoch(&mut env, my_epoch);
bridge_mint_claim(&mut env, &in_flight);
if real_sender.send(env).await.is_err() {
break;
}
}
break;
}
}
}
}
}
});
(stamp_tx, handle)
}
fn bridge_mint_claim(env: &mut ExchangeEnvelope, in_flight: &Option<Arc<InFlightGauge>>) {
if env.in_flight_claim.is_none()
&& let Some(counter) = in_flight
{
env.in_flight_claim = Some(InFlightClaim::attach(counter));
}
}
fn stamp_epoch(env: &mut ExchangeEnvelope, epoch: u64) {
env.exchange.properties.insert(
LEADER_EPOCH_PROPERTY.to_string(),
serde_json::Value::String(epoch.to_string()),
);
}
pub(crate) fn emit_leadership_transition(
metrics: &Arc<dyn MetricsCollector>,
lock: &str,
route_id: &str,
event: &str,
) {
metrics.record_counter(
"master_leadership_transitions_total",
1.0,
&[("lock", lock), ("route_id", route_id), ("event", event)],
);
metrics.set_master_leadership(lock, event == "acquired");
}
pub(crate) fn emit_lifecycle(
metrics: &Arc<dyn MetricsCollector>,
lock: &str,
route_id: &str,
event: &str,
reason: &str,
) {
metrics.record_counter(
"master_delegate_lifecycle_total",
1.0,
&[
("lock", lock),
("route_id", route_id),
("event", event),
("reason", reason),
],
);
}
async fn supervise_background(
bg: Option<tokio::task::JoinHandle<Result<(), CamelError>>>,
run_token: &CancellationToken,
) -> Result<(), CamelError> {
let Some(mut bg) = bg else {
return Ok(());
};
tokio::select! {
result = &mut bg => match result {
Ok(Ok(())) => Ok(()),
Ok(Err(err)) if !run_token.is_cancelled() => Err(err),
Ok(Err(err)) => {
debug!(error = %err, "delegate background task error during shutdown");
Ok(())
}
Err(join_err) if !run_token.is_cancelled() => Err(CamelError::ProcessorError(
format!("master delegate background task panicked: {join_err}"),
)),
Err(join_err) => {
debug!(error = %join_err, "delegate background task panicked during shutdown");
Ok(())
}
},
_ = run_token.cancelled() => {
bg.abort();
Ok(())
}
}
}
pub(crate) async fn stop_delegate(
state: &mut DelegateState,
drain_timeout: Duration,
lock_name: &str,
route_id: &str,
metrics: &Arc<dyn MetricsCollector>,
) -> Result<(), CamelError> {
if let DelegateState::Active {
run_token,
mut handle,
mut bridge_handle,
..
} = std::mem::replace(state, DelegateState::Inactive)
{
run_token.cancel();
let delegate_result: Result<(), CamelError> =
match timeout(drain_timeout, &mut handle).await {
Ok(Ok(Ok(()))) => Ok(()),
Ok(Ok(Err(err))) => Err(err),
Ok(Err(e)) if e.is_panic() => {
error!(error = %e, "master delegate task panicked");
Err(CamelError::ProcessorError(format!(
"master delegate task panicked: {e}"
)))
}
Ok(Err(e)) => {
warn!(error = %e, "master delegate task cancelled");
Err(CamelError::ProcessorError(format!(
"master delegate task cancelled: {e}"
)))
}
Err(_) => {
warn!("master delegate shutdown timed out, aborting");
handle.abort();
let _ = handle.await;
Ok(())
}
};
if let Some(mut bridge) = bridge_handle.take() {
match timeout(drain_timeout, &mut bridge).await {
Ok(Ok(())) => {}
Ok(Err(e)) => {
warn!(error = %e, "epoch bridge task failed");
}
Err(_) => {
warn!("epoch bridge drain timed out, aborting");
bridge.abort();
let _ = bridge.await;
}
}
}
emit_lifecycle(metrics, lock_name, route_id, "stopped", "none");
return delegate_result;
}
Ok(())
}
pub(crate) struct ReconcileContext<'a> {
pub(crate) lock_name: &'a str,
pub(crate) delegate_component: &'a Arc<dyn Component>,
pub(crate) delegate_uri: &'a str,
pub(crate) route_id: String,
pub(crate) sender: &'a tokio::sync::mpsc::Sender<ExchangeEnvelope>,
pub(crate) parent_cancel: &'a CancellationToken,
pub(crate) drain_timeout: Duration,
pub(crate) metrics: &'a Arc<dyn MetricsCollector>,
pub(crate) platform_service: &'a Arc<dyn PlatformService>,
pub(crate) runtime: Arc<dyn camel_component_api::RuntimeObservability>,
pub(crate) leader_epoch: Arc<AtomicU64>,
pub(crate) attempts: AtomicU32,
pub(crate) reconnect: NetworkRetryPolicy,
pub(crate) in_flight: Option<Arc<InFlightGauge>>,
}
pub(crate) async fn reconcile_event(
event: camel_api::LeadershipEvent,
state: &mut DelegateState,
ctx: &ReconcileContext<'_>,
) -> Result<(), CamelError> {
match event {
camel_api::LeadershipEvent::StartedLeading => {
let current_epoch = ctx.leader_epoch.load(Ordering::Acquire);
if let DelegateState::Active { epoch, .. } = state
&& *epoch == current_epoch
{
debug!(
lock = %ctx.lock_name,
current_epoch,
"dropping duplicate StartedLeading delivery at the published epoch"
);
return Ok(());
}
let term_bump = matches!(&*state, DelegateState::Active { .. });
stop_delegate(
state,
ctx.drain_timeout,
ctx.lock_name,
ctx.route_id.as_str(),
ctx.metrics,
)
.await?;
let delegate_ctx = MasterDelegateContext {
delegate_component: Arc::clone(ctx.delegate_component),
metrics: Arc::clone(ctx.metrics),
platform_service: Arc::clone(ctx.platform_service),
};
if term_bump {
ctx.attempts.swap(0, Ordering::Relaxed);
}
let count = ctx.attempts.load(Ordering::Relaxed);
ctx.attempts
.store(count.saturating_add(1), Ordering::Relaxed);
if !ctx.reconnect.should_retry(count) {
return Ok(());
}
info!(lock = %ctx.lock_name, "master leadership acquired");
let endpoint = match ctx
.delegate_component
.create_endpoint(ctx.delegate_uri, &delegate_ctx)
{
Ok(endpoint) => endpoint,
Err(err) => {
let is_retryable = is_retryable_camel_error(&err);
emit_lifecycle(
ctx.metrics,
ctx.lock_name,
ctx.route_id.as_str(),
"create_error",
if is_retryable {
"transient"
} else {
"permanent"
},
);
if is_retryable {
warn!(lock = %ctx.lock_name, error = %err, "transient delegate endpoint error (will retry)");
return Ok(()); }
return Err(err); }
};
let mut consumer = match endpoint.create_consumer(Arc::clone(&ctx.runtime)) {
Ok(consumer) => consumer,
Err(err) => {
let is_retryable = is_retryable_camel_error(&err);
emit_lifecycle(
ctx.metrics,
ctx.lock_name,
ctx.route_id.as_str(),
"create_error",
if is_retryable {
"transient"
} else {
"permanent"
},
);
if is_retryable {
warn!(lock = %ctx.lock_name, error = %err, "transient delegate consumer error (will retry)");
return Ok(()); }
return Err(err); }
};
let my_epoch = ctx.leader_epoch.load(Ordering::Acquire);
let (stamp_tx, bridge_handle) = spawn_epoch_bridge(
ctx.sender.clone(),
my_epoch,
ctx.parent_cancel.child_token(),
ctx.in_flight.clone(),
);
let run_token = ctx.parent_cancel.child_token();
let mut delegate_ctx =
ConsumerContext::new(stamp_tx, run_token.clone(), ctx.route_id.clone());
if let Some(counter) = ctx.in_flight.clone() {
delegate_ctx = delegate_ctx.with_in_flight_counter(counter);
}
let supervise_token = run_token.clone();
let handle = tokio::spawn(async move {
consumer.start(delegate_ctx).await?;
let supervised =
supervise_background(consumer.background_task_handle(), &supervise_token).await;
consumer.stop().await?;
supervised
});
*state = DelegateState::Active {
run_token,
handle,
bridge_handle: Some(bridge_handle),
epoch: my_epoch,
};
emit_lifecycle(
ctx.metrics,
ctx.lock_name,
ctx.route_id.as_str(),
"started",
"none",
);
}
camel_api::LeadershipEvent::StoppedLeading => {
info!(lock = %ctx.lock_name, "master leadership lost");
stop_delegate(
state,
ctx.drain_timeout,
ctx.lock_name,
ctx.route_id.as_str(),
ctx.metrics,
)
.await?;
}
_ => {}
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use std::time::Duration;
use camel_api::{Body, CamelError, Exchange, Message, NoOpMetrics};
use camel_component_api::ExchangeEnvelope;
use serde_json::Value;
use tokio::time::timeout;
use tokio_util::sync::CancellationToken;
use super::{spawn_epoch_bridge, stop_delegate};
use crate::consumer::DelegateState;
fn make_envelope(text: &str) -> ExchangeEnvelope {
ExchangeEnvelope {
exchange: Exchange::new(Message::new(Body::Text(text.to_string()))),
reply_tx: None,
in_flight_claim: None,
}
}
#[tokio::test]
async fn fence_token_stamped_on_exchange() {
let (pipeline_tx, mut pipeline_rx) = tokio::sync::mpsc::channel::<ExchangeEnvelope>(10);
let cancel = CancellationToken::new();
let (stamp_tx, _bridge) = spawn_epoch_bridge(pipeline_tx, 42, cancel, None);
stamp_tx.send(make_envelope("hello")).await.unwrap();
drop(stamp_tx);
let received = timeout(Duration::from_secs(1), pipeline_rx.recv())
.await
.expect("bridge should forward within 1s")
.expect("channel should yield an envelope");
let epoch_val = received
.exchange
.properties
.get(super::LEADER_EPOCH_PROPERTY)
.expect("x-camel-leader-epoch should be stamped");
assert_eq!(epoch_val, &Value::String("42".to_string()));
}
#[tokio::test]
async fn bridge_drains_buffer_on_delegate_stop() {
let (pipeline_tx, mut pipeline_rx) = tokio::sync::mpsc::channel::<ExchangeEnvelope>(10);
let cancel = CancellationToken::new();
let (stamp_tx, bridge_handle) = spawn_epoch_bridge(pipeline_tx, 5, cancel, None);
for i in 0..3 {
stamp_tx
.send(make_envelope(&format!("msg-{i}")))
.await
.unwrap();
}
drop(stamp_tx);
timeout(Duration::from_secs(1), bridge_handle)
.await
.expect("bridge should finish within 1s")
.expect("bridge task should not panic");
for i in 0..3 {
let env = timeout(Duration::from_secs(2), pipeline_rx.recv())
.await
.expect("envelope should arrive after drain within 2s")
.expect("pipeline channel alive");
let epoch = env
.exchange
.properties
.get(super::LEADER_EPOCH_PROPERTY)
.and_then(|v| v.as_str())
.and_then(|s| s.parse::<u64>().ok());
assert_eq!(epoch, Some(5), "envelope {i} should carry constant epoch 5");
}
assert!(
pipeline_rx.try_recv().is_err(),
"bridge should have exited — no more envelopes"
);
}
#[tokio::test]
async fn bridge_snapshot_constant_across_epoch_change() {
let epoch = Arc::new(AtomicU64::new(7));
let (pipeline_tx, mut pipeline_rx) = tokio::sync::mpsc::channel::<ExchangeEnvelope>(10);
let cancel = CancellationToken::new();
let my_epoch = epoch.load(std::sync::atomic::Ordering::Acquire);
let (stamp_tx, bridge_handle) = spawn_epoch_bridge(pipeline_tx, my_epoch, cancel, None);
epoch.store(99, std::sync::atomic::Ordering::Release);
stamp_tx.send(make_envelope("stale")).await.unwrap();
drop(stamp_tx);
timeout(Duration::from_secs(1), bridge_handle)
.await
.unwrap()
.unwrap();
let env = timeout(Duration::from_secs(2), pipeline_rx.recv())
.await
.expect("envelope within 2s")
.expect("pipeline channel alive");
let stamped = env
.exchange
.properties
.get(super::LEADER_EPOCH_PROPERTY)
.and_then(|v| v.as_str())
.expect("stamp should be present");
assert_eq!(
stamped, "7",
"bridge must stamp with snapshot (7), not live (99)"
);
}
#[tokio::test]
async fn stale_bridge_carries_old_epoch_not_current() {
let (pipeline_tx, mut pipeline_rx) = tokio::sync::mpsc::channel::<ExchangeEnvelope>(10);
let cancel = CancellationToken::new();
let (stamp_tx, bridge_handle) = spawn_epoch_bridge(pipeline_tx, 3, cancel, None);
stamp_tx
.send(make_envelope("from-old-leader"))
.await
.unwrap();
drop(stamp_tx);
timeout(Duration::from_secs(1), bridge_handle)
.await
.expect("bridge should finish within 1s")
.expect("bridge task should not panic");
let env = timeout(Duration::from_secs(2), pipeline_rx.recv())
.await
.expect("stamped envelope should arrive within 2s")
.expect("pipeline channel alive");
let stamped = env
.exchange
.properties
.get(super::LEADER_EPOCH_PROPERTY)
.and_then(|v| v.as_str())
.expect("x-camel-leader-epoch should be stamped");
assert_eq!(
stamped, "3",
"old bridge stamps with its snapshot epoch (3), not the current epoch"
);
}
#[tokio::test]
async fn bridge_aborts_on_parent_cancel() {
let (pipeline_tx, mut _pipeline_rx) = tokio::sync::mpsc::channel::<ExchangeEnvelope>(10);
let cancel = CancellationToken::new();
let (_stamp_tx, bridge_handle) = spawn_epoch_bridge(pipeline_tx, 1, cancel.clone(), None);
cancel.cancel();
timeout(Duration::from_millis(100), bridge_handle)
.await
.expect("bridge should abort within 100ms")
.expect("bridge task should not panic");
}
#[tokio::test]
async fn bridge_aborted_on_drain_timeout_when_pipeline_full() {
let (pipeline_tx, _pipeline_rx) = tokio::sync::mpsc::channel::<ExchangeEnvelope>(1);
let cancel = CancellationToken::new();
pipeline_tx.send(make_envelope("blocker")).await.unwrap();
let (stamp_tx, mut bridge_handle) = spawn_epoch_bridge(pipeline_tx, 42, cancel, None);
stamp_tx.send(make_envelope("will-block")).await.unwrap();
drop(stamp_tx);
let drain_timeout = Duration::from_millis(50);
match timeout(drain_timeout, &mut bridge_handle).await {
Err(_) => {
bridge_handle.abort();
let _ = bridge_handle.await;
}
Ok(_) => panic!("bridge should not complete while pipeline is full"),
}
}
#[tokio::test]
async fn stop_delegate_drains_bridge_on_delegate_error() {
let delegate_handle = tokio::spawn(async {
Err::<(), CamelError>(CamelError::ProcessorError("test delegate failure".into()))
});
let cancel = CancellationToken::new();
let (pipeline_tx, _pipeline_rx) = tokio::sync::mpsc::channel::<ExchangeEnvelope>(10);
let (stamp_tx, bridge_handle) = spawn_epoch_bridge(pipeline_tx, 42, cancel, None);
let bridge_abort = bridge_handle.abort_handle();
assert!(
!bridge_abort.is_finished(),
"bridge should not be finished before stop_delegate"
);
stamp_tx.send(make_envelope("test")).await.unwrap();
drop(stamp_tx);
let mut state = DelegateState::Active {
run_token: CancellationToken::new(),
handle: delegate_handle,
bridge_handle: Some(bridge_handle),
epoch: 42,
};
let metrics: Arc<dyn camel_api::MetricsCollector> = Arc::new(NoOpMetrics);
let result = stop_delegate(
&mut state,
Duration::from_secs(5),
"test-lock",
"test-route",
&metrics,
)
.await;
assert!(result.is_err(), "expected Err from delegate failure");
assert!(
result
.unwrap_err()
.to_string()
.contains("test delegate failure"),
"error should contain delegate message"
);
assert!(
bridge_abort.is_finished(),
"bridge should have been drained after delegate error"
);
}
#[tokio::test]
async fn stop_delegate_drains_bridge_on_delegate_timeout() {
let delegate_handle = tokio::spawn(async {
std::future::pending::<Result<(), CamelError>>().await
});
let cancel = CancellationToken::new();
let (pipeline_tx, _pipeline_rx) = tokio::sync::mpsc::channel::<ExchangeEnvelope>(10);
let (stamp_tx, bridge_handle) = spawn_epoch_bridge(pipeline_tx, 42, cancel, None);
let bridge_abort = bridge_handle.abort_handle();
assert!(
!bridge_abort.is_finished(),
"bridge should not be finished before stop_delegate"
);
stamp_tx.send(make_envelope("timeout-test")).await.unwrap();
let mut state = DelegateState::Active {
run_token: CancellationToken::new(),
handle: delegate_handle,
bridge_handle: Some(bridge_handle),
epoch: 42,
};
let metrics: Arc<dyn camel_api::MetricsCollector> = Arc::new(NoOpMetrics);
let result = stop_delegate(
&mut state,
Duration::from_millis(100),
"test-lock",
"test-route",
&metrics,
)
.await;
assert!(result.is_ok(), "timeout path should return Ok");
assert!(
bridge_abort.is_finished(),
"bridge should have been drained after delegate timeout"
);
drop(stamp_tx);
}
#[tokio::test]
async fn bridge_mints_claim_for_uncounted_envelopes() {
let counter = Arc::new(camel_api::InFlightGauge::new());
let (pipeline_tx, mut pipeline_rx) = tokio::sync::mpsc::channel::<ExchangeEnvelope>(10);
let cancel = CancellationToken::new();
let (stamp_tx, bridge_handle) =
spawn_epoch_bridge(pipeline_tx, 9, cancel, Some(Arc::clone(&counter)));
stamp_tx.send(make_envelope("uncounted")).await.unwrap();
drop(stamp_tx);
timeout(Duration::from_secs(1), bridge_handle)
.await
.expect("bridge should finish within 1s")
.expect("bridge task should not panic");
let mut received = timeout(Duration::from_secs(2), pipeline_rx.recv())
.await
.expect("envelope must arrive within 2s")
.expect("pipeline channel alive");
let claim = received
.in_flight_claim
.take()
.expect("bridge must have minted a claim for the uncounted envelope");
assert_eq!(counter.total(), 1);
drop(claim);
drop(received);
assert_eq!(
counter.total(),
0,
"dropping the envelope must release exactly once"
);
}
#[tokio::test]
async fn bridge_preserves_existing_claim_without_double_counting() {
let counter = Arc::new(camel_api::InFlightGauge::new());
let (pipeline_tx, mut pipeline_rx) = tokio::sync::mpsc::channel::<ExchangeEnvelope>(10);
let cancel = CancellationToken::new();
let (stamp_tx, bridge_handle) =
spawn_epoch_bridge(pipeline_tx, 9, cancel, Some(Arc::clone(&counter)));
let mut pre_claimed = make_envelope("already-counted");
pre_claimed.in_flight_claim = Some(camel_component_api::InFlightClaim::attach(&counter));
assert_eq!(counter.total(), 1);
stamp_tx.send(pre_claimed).await.unwrap();
drop(stamp_tx);
timeout(Duration::from_secs(1), bridge_handle)
.await
.expect("bridge should finish within 1s")
.expect("bridge task should not panic");
let mut received = timeout(Duration::from_secs(2), pipeline_rx.recv())
.await
.expect("envelope must arrive within 2s")
.expect("pipeline channel alive");
let claim = received
.in_flight_claim
.take()
.expect("pre-existing claim must survive the bridge");
assert_eq!(
counter.total(),
1,
"bridge must not mint a sibling for an already-counted envelope"
);
drop(claim);
drop(received);
assert_eq!(counter.total(), 0);
}
}