use std::time::Duration;
use camel_api::{CamelError, Exchange, Message, Value};
use camel_test::CamelTestContext;
use tower::ServiceExt;
mod cache_test_support;
use cache_test_support::{send_to_direct, test_rt};
async fn send_to_direct_tolerant(
h: &CamelTestContext,
endpoint_uri: &str,
exchange: Exchange,
timeout: Duration,
) {
let deadline = tokio::time::Instant::now() + timeout;
loop {
let producer = {
let ctx = h.ctx().lock().await;
let producer_ctx = ctx.producer_context();
let registry = ctx.registry();
let component = registry
.get("direct")
.expect("direct component not registered");
let endpoint = component
.create_endpoint(endpoint_uri, &*ctx)
.expect("failed to create direct endpoint");
endpoint
.create_producer(test_rt(), &producer_ctx)
.expect("failed to create direct producer")
};
match producer.oneshot(exchange.clone()).await {
Ok(_) => return,
Err(e) => {
let is_startup_race = matches!(e, CamelError::EndpointCreationFailed(_))
|| e.to_string().contains("not registered");
if is_startup_race && tokio::time::Instant::now() < deadline {
tokio::time::sleep(Duration::from_millis(20)).await;
continue;
}
if is_startup_race {
panic!(
"direct consumer for {endpoint_uri} never registered within {timeout:?}"
);
}
return;
}
}
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn cache_peek_stale_serves_expired_entry_in_route() {
let h = CamelTestContext::builder()
.with_direct()
.with_mock()
.build()
.await;
let yaml = r#"
routes:
- id: "populate"
from: "direct:populate"
steps:
- cache:
key: "${header.cacheKey}"
ttl: "1ms"
on_miss:
- set_body: "tile_data_v1"
- id: "serve-stale"
from: "direct:serve"
steps:
- cache_peek_stale:
key: "${header.cacheKey}"
- to: "mock:result"
"#;
for route in camel_dsl::parse_yaml(yaml).unwrap() {
h.add_route(route).await.unwrap();
}
h.start().await;
let mock = h
.mock()
.get_endpoint("result")
.expect("mock endpoint created during route compilation");
let mut ex1 = Exchange::new(Message::new("original"));
ex1.input
.set_header("cacheKey", Value::String("tile1".into()));
send_to_direct(&h, "direct:populate", ex1, Duration::from_secs(2)).await;
tokio::time::sleep(Duration::from_millis(50)).await;
let mut ex2 = Exchange::new(Message::new("original"));
ex2.input
.set_header("cacheKey", Value::String("tile1".into()));
send_to_direct(&h, "direct:serve", ex2, Duration::from_secs(2)).await;
mock.await_exchanges(1, Duration::from_secs(2)).await;
{
let received = mock.get_received_exchanges().await;
let body = received[0].input.body.as_text();
assert_eq!(
body,
Some("tile_data_v1"),
"cache_peek_stale must serve the expired entry body"
);
}
h.stop().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn cache_peek_stale_returns_stopped_on_absent_key() {
let h = CamelTestContext::builder()
.with_direct()
.with_mock()
.build()
.await;
let yaml = r#"
routes:
- id: "serve-absent"
from: "direct:serve"
steps:
- cache_peek_stale:
key: "${header.cacheKey}"
- to: "mock:result"
"#;
for route in camel_dsl::parse_yaml(yaml).unwrap() {
h.add_route(route).await.unwrap();
}
h.start().await;
let mock = h
.mock()
.get_endpoint("result")
.expect("mock endpoint created during route compilation");
let mut ex = Exchange::new(Message::new("original"));
ex.input
.set_header("cacheKey", Value::String("never-cached".into()));
send_to_direct(&h, "direct:serve", ex, Duration::from_secs(2)).await;
tokio::time::sleep(Duration::from_millis(100)).await;
let received = mock.get_received_exchanges().await;
assert_eq!(
received.len(),
0,
"no exchange should reach mock:result when cache_peek_stale finds no entry"
);
h.stop().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn cache_peek_stale_in_do_try_catch_serves_stale_body() {
let h = CamelTestContext::builder()
.with_direct()
.with_mock()
.build()
.await;
let yaml = r#"
routes:
- id: "seed"
from: "direct:seed"
steps:
- cache:
key: "${header.cacheKey}"
ttl: "1ms"
on_miss:
- set_body: "stale-payload"
- id: "stale-via-catch"
from: "direct:stale"
steps:
- cache:
key: "${header.cacheKey}"
on_miss:
- do_try:
steps:
- recipient_list:
simple: "bogus-fail:nowhere"
catch:
- exception: ["*"]
steps:
- cache_peek_stale:
key: "${header.cacheKey}"
- to: "mock:result"
"#;
for route in camel_dsl::parse_yaml(yaml).unwrap() {
h.add_route(route).await.unwrap();
}
h.start().await;
let mock = h
.mock()
.get_endpoint("result")
.expect("mock endpoint created during route compilation");
let mut seed_ex = Exchange::new(Message::new("seed"));
seed_ex
.input
.set_header("cacheKey", Value::String("k".into()));
send_to_direct(&h, "direct:seed", seed_ex, Duration::from_secs(2)).await;
tokio::time::sleep(Duration::from_millis(50)).await;
let mut test_ex = Exchange::new(Message::new("request"));
test_ex
.input
.set_header("cacheKey", Value::String("k".into()));
send_to_direct(&h, "direct:stale", test_ex, Duration::from_secs(2)).await;
mock.await_exchanges(1, Duration::from_secs(2)).await;
{
let received = mock.get_received_exchanges().await;
assert_eq!(
received.len(),
1,
"exactly one exchange must reach mock:result"
);
let body = received[0].input.body.as_text();
assert_eq!(
body,
Some("stale-payload"),
"stale body must be served through the do_try catch and survive \
the outer cache write-back"
);
}
h.stop().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn cache_poison_timer_recipient_list_all_failed_no_writeback() {
let h = CamelTestContext::builder()
.with_direct()
.with_mock_fail_fast()
.build()
.await;
let yaml = r#"
routes:
- id: "seed"
from: "direct:seed"
steps:
- cache:
key: "${header.cacheKey}"
ttl: "1ms"
on_miss:
- set_body: "stale-seed"
- id: "poison"
from: "direct:poison"
steps:
- cache:
key: "${header.cacheKey}"
ttl: "15m"
on_miss:
- to: "mock:poison-ran"
- recipient_list:
simple: "mock:poison-fail"
parallel: true
- id: "inspect"
from: "direct:inspect"
steps:
- cache_peek_stale:
key: "${header.cacheKey}"
- to: "mock:inspect"
"#;
for route in camel_dsl::parse_yaml(yaml).unwrap() {
h.add_route(route).await.unwrap();
}
h.start().await;
camel_component_api::Component::create_endpoint(
h.mock(),
"mock:poison-fail",
&camel_component_api::NoOpComponentContext,
)
.expect("pre-create mock:poison-fail endpoint");
h.mock()
.get_endpoint("poison-fail")
.expect("mock endpoint 'poison-fail' pre-created above")
.trigger_fail_fast(CamelError::ProcessorError(
"simulated downstream failure".to_string(),
));
let poison_ran = h
.mock()
.get_endpoint("poison-ran")
.expect("mock endpoint 'poison-ran' created during route compilation");
let inspect = h
.mock()
.get_endpoint("inspect")
.expect("mock endpoint 'inspect' created during route compilation");
let mut seed_ex = Exchange::new(Message::new("seed"));
seed_ex
.input
.set_header("cacheKey", Value::String("k".into()));
send_to_direct(&h, "direct:seed", seed_ex, Duration::from_secs(2)).await;
tokio::time::sleep(Duration::from_millis(50)).await;
let mut poison_ex = Exchange::new(Message::new("inbound-poison-body"));
poison_ex
.input
.set_header("cacheKey", Value::String("k".into()));
send_to_direct_tolerant(&h, "direct:poison", poison_ex, Duration::from_secs(2)).await;
poison_ran.await_exchanges(1, Duration::from_secs(2)).await;
let mut inspect_ex = Exchange::new(Message::new("inspect"));
inspect_ex
.input
.set_header("cacheKey", Value::String("k".into()));
send_to_direct(&h, "direct:inspect", inspect_ex, Duration::from_secs(2)).await;
inspect.await_exchanges(1, Duration::from_secs(2)).await;
{
let received = inspect.get_received_exchanges().await;
assert_eq!(
received.len(),
1,
"the stale entry must be peek_stale-visible (not absent / not poisoned)"
);
let body = received[0].input.body.as_text();
assert_eq!(
body,
Some("stale-seed"),
"cache must preserve the stale seed; the inbound body must NOT be \
written back (rc-20yn regression would poison with the inbound body)"
);
}
h.stop().await;
}