use std::time::Duration;
use camel_api::{Exchange, Message, Value};
use camel_test::CamelTestContext;
use tower::ServiceExt;
fn test_rt() -> std::sync::Arc<dyn camel_component_api::RuntimeObservability> {
std::sync::Arc::new(camel_component_api::NoOpComponentContext)
}
async fn send_to_direct(
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(_) if tokio::time::Instant::now() < deadline => {
tokio::time::sleep(Duration::from_millis(20)).await;
}
Err(e) => panic!("failed to send exchange within {timeout:?}: {e}"),
}
}
}
#[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;
}