use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Once};
use std::time::{Duration, Instant};
use async_trait::async_trait;
use platform_core::{
overrides, resources, AppConfigReader, AppError, ComposableFunction, EventEnvelope, Platform,
PostOffice,
};
fn setup_config() {
static INIT: Once = Once::new();
INIT.call_once(|| {
resources::prepend_resource_root("tests/resources");
let holding =
std::env::temp_dir().join(format!("mercury-direct-test-{}", std::process::id()));
overrides::set("transient.data.store", &holding.display().to_string());
let _ = AppConfigReader::get_instance();
});
}
struct ConcurrencyProbe {
current: Arc<AtomicUsize>,
peak: Arc<AtomicUsize>,
done: Arc<AtomicUsize>,
}
#[async_trait]
impl ComposableFunction for ConcurrencyProbe {
async fn handle_event(
&self,
_headers: HashMap<String, String>,
_input: EventEnvelope,
_instance: usize,
) -> Result<EventEnvelope, AppError> {
let now = self.current.fetch_add(1, Ordering::SeqCst) + 1;
self.peak.fetch_max(now, Ordering::SeqCst);
tokio::time::sleep(Duration::from_millis(150)).await;
self.current.fetch_sub(1, Ordering::SeqCst);
self.done.fetch_add(1, Ordering::SeqCst);
Ok(EventEnvelope::new())
}
}
struct Probe {
peak: Arc<AtomicUsize>,
done: Arc<AtomicUsize>,
}
fn probe(platform: &Platform, route: &str) -> Probe {
let current = Arc::new(AtomicUsize::new(0));
let peak = Arc::new(AtomicUsize::new(0));
let done = Arc::new(AtomicUsize::new(0));
platform
.register(
route,
Arc::new(ConcurrencyProbe {
current: current.clone(),
peak: peak.clone(),
done: done.clone(),
}),
1, )
.unwrap();
Probe { peak, done }
}
async fn fire(platform: &Platform, route: &str, n: usize) {
let po = PostOffice::new(platform);
for i in 0..n {
po.send(
EventEnvelope::new()
.set_to(route)
.set_body(format!("event-{i}"))
.unwrap(),
)
.await
.unwrap();
}
}
async fn wait_done(done: &Arc<AtomicUsize>, n: usize, deadline: Duration) {
let end = Instant::now() + deadline;
while done.load(Ordering::SeqCst) < n && Instant::now() < end {
tokio::time::sleep(Duration::from_millis(10)).await;
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn reserved_engine_routes_run_unbounded_despite_one_instance() {
setup_config();
let platform = Platform::new();
let task_executor = probe(&platform, "task.executor");
let manager = probe(&platform, "event.script.manager");
let started = Instant::now();
fire(&platform, "task.executor", 10).await;
fire(&platform, "event.script.manager", 10).await;
wait_done(&task_executor.done, 10, Duration::from_secs(5)).await;
wait_done(&manager.done, 10, Duration::from_secs(5)).await;
let elapsed = started.elapsed();
assert_eq!(task_executor.done.load(Ordering::SeqCst), 10);
assert_eq!(manager.done.load(Ordering::SeqCst), 10);
assert!(
elapsed < Duration::from_millis(1200),
"direct execution should parallelize: took {elapsed:?}"
);
assert!(
task_executor.peak.load(Ordering::SeqCst) > 1,
"task.executor must run concurrently despite 1 instance"
);
assert!(
manager.peak.load(Ordering::SeqCst) > 1,
"event.script.manager must run concurrently despite 1 instance"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn normal_routes_keep_the_reactive_serialized_path() {
setup_config();
let platform = Platform::new();
let control = probe(&platform, "normal.route.probe");
fire(&platform, "normal.route.probe", 4).await;
wait_done(&control.done, 4, Duration::from_secs(5)).await;
assert_eq!(control.done.load(Ordering::SeqCst), 4);
assert_eq!(
control.peak.load(Ordering::SeqCst),
1,
"a normal 1-instance route must serialize through its worker"
);
}