use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use camel_api::CamelError;
use camel_component_api::{ConcurrencyModel, Consumer, ConsumerContext};
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use tracing::{debug, warn};
use wasmtime::component::{Component, Linker};
use wasmtime::{Config, Engine, Store};
use crate::config::WasmConfig;
use crate::source_bindings::Source;
use crate::source_bindings::camel::plugin::source_host::CapabilityRequest;
use crate::source_host::{
HttpListenerHandle, SourceChannels, SourceHostState, add_to_linker, run_http_listener,
run_pipeline_bridge,
};
const CONFIGURE_EPOCH_DEADLINE: u64 = u64::MAX;
pub struct WasmSourceConsumer {
module_path: PathBuf,
guest_config: Vec<(String, String)>,
#[allow(dead_code)]
registry: Arc<std::sync::Mutex<camel_core::Registry>>,
cancel_token: CancellationToken,
engine: Option<Arc<Engine>>,
run_task: Option<JoinHandle<Result<(), CamelError>>>,
listener_task: Option<JoinHandle<()>>,
bridge_task: Option<JoinHandle<()>>,
}
impl WasmSourceConsumer {
pub fn new(
module_path: PathBuf,
_config: WasmConfig,
guest_config: Vec<(String, String)>,
registry: Arc<std::sync::Mutex<camel_core::Registry>>,
) -> Self {
Self {
module_path,
guest_config,
registry,
cancel_token: CancellationToken::new(),
engine: None,
run_task: None,
listener_task: None,
bridge_task: None,
}
}
}
#[async_trait::async_trait]
impl Consumer for WasmSourceConsumer {
async fn start(&mut self, ctx: ConsumerContext) -> Result<(), CamelError> {
let mut wasm_config = Config::new();
wasm_config.wasm_component_model(true);
wasm_config.epoch_interruption(true);
let engine = Arc::new(
Engine::new(&wasm_config)
.map_err(|e| CamelError::ProcessorError(format!("wasmtime engine: {e}")))?,
);
let component = Component::from_file(&engine, &self.module_path)
.map_err(|e| CamelError::ProcessorError(format!("component load: {e}")))?;
let channels = SourceChannels::new();
let wasi = wasmtime_wasi::WasiCtxBuilder::new().build();
let cancel = self.cancel_token.clone();
let mut store = Store::new(
&engine,
SourceHostState {
table: wasmtime::component::ResourceTable::new(),
wasi,
request_rx: channels.request_rx,
exchange_tx: channels.exchange_tx,
cancel_token: cancel.clone(),
},
);
let mut linker: Linker<SourceHostState> = Linker::new(&engine);
add_to_linker(&mut linker)
.map_err(|e| CamelError::ProcessorError(format!("linker: {e}")))?;
let source = Source::instantiate(&mut store, &component, &linker)
.map_err(|e| CamelError::ProcessorError(format!("instantiate: {e}")))?;
store.set_epoch_deadline(CONFIGURE_EPOCH_DEADLINE);
let plan = source
.call_configure(&mut store, &self.guest_config)
.map_err(|e| CamelError::ProcessorError(format!("configure: {e}")))?
.map_err(|e| CamelError::ProcessorError(format!("configure guest error: {e:?}")))?;
if plan.capabilities.len() != 1 {
return Err(CamelError::EndpointCreationFailed(
"source-plan must have exactly one capability".into(),
));
}
let CapabilityRequest::HttpListener(listener_spec) = &plan.capabilities[0];
use crate::source_bindings::camel::plugin::source_host::ConcurrencyModel as PlanConcurrency;
match plan.concurrency {
PlanConcurrency::Sequential => {}
PlanConcurrency::Concurrent(max) => {
return Err(CamelError::EndpointCreationFailed(format!(
"WASM source does not support concurrent({max}); only sequential is implemented"
)));
}
}
let listener = store
.data_mut()
.table
.push(HttpListenerHandle)
.map_err(|e| CamelError::ProcessorError(format!("resource table: {e}")))?;
let bind_addr: std::net::SocketAddr = listener_spec.bind.parse().map_err(|e| {
CamelError::EndpointCreationFailed(format!(
"invalid bind address '{}': {}",
listener_spec.bind, e
))
})?;
let path_filter = listener_spec.path.clone();
let tcp_listener = tokio::net::TcpListener::bind(bind_addr)
.await
.map_err(|e| {
CamelError::Io(format!(
"failed to bind source HTTP listener {bind_addr}: {e}"
))
})?;
tracing::info!(%bind_addr, "source HTTP listener bound");
let listener_cancel = cancel.clone();
let request_tx = channels.request_tx;
let lt = tokio::spawn(async move {
if let Err(e) =
run_http_listener(tcp_listener, path_filter, request_tx, listener_cancel).await
{
warn!("WASM source HTTP listener exited: {e:?}");
}
});
let bridge_ctx = ctx;
let exchange_rx = channels.exchange_rx;
let bt = tokio::spawn(async move {
if let Err(e) = run_pipeline_bridge(exchange_rx, bridge_ctx).await {
warn!("WASM source pipeline bridge exited: {e:?}");
}
});
let run_handle = tokio::task::spawn_blocking(move || {
store.set_epoch_deadline(1);
match source.call_run(&mut store, listener) {
Ok(Ok(())) => {
debug!("WASM source guest run() exited normally");
Ok(())
}
Ok(Err(e)) => {
warn!("WASM source guest run() returned error: {e:?}");
Err(CamelError::ProcessorError(format!("guest run: {e:?}")))
}
Err(e) => {
warn!("WASM source guest trapped: {e}");
Err(CamelError::ProcessorError(format!("guest trap: {e}")))
}
}
});
self.engine = Some(engine);
self.run_task = Some(run_handle);
self.listener_task = Some(lt);
self.bridge_task = Some(bt);
Ok(())
}
async fn stop(&mut self) -> Result<(), CamelError> {
self.cancel_token.cancel();
if let Some(ref engine) = self.engine {
engine.increment_epoch();
}
let _ = self.run_task.take();
let grace = std::time::Duration::from_secs(5);
if let Some(task) = self.listener_task.take() {
join_or_abort(task, "listener", grace).await;
}
if let Some(task) = self.bridge_task.take() {
join_or_abort(task, "bridge", grace).await;
}
Ok(())
}
fn concurrency_model(&self) -> ConcurrencyModel {
ConcurrencyModel::Sequential
}
fn background_task_handle(&mut self) -> Option<JoinHandle<Result<(), CamelError>>> {
self.run_task.take()
}
}
async fn join_or_abort<T: Send + 'static>(task: JoinHandle<T>, label: &str, grace: Duration) {
tokio::pin!(task);
tokio::select! {
result = &mut task => {
if let Err(e) = result {
warn!("{label} task panicked on shutdown: {e}");
}
}
_ = tokio::time::sleep(grace) => {
warn!("{label} task did not exit within {grace:?}, aborting");
task.abort();
if let Err(e) = task.await {
debug!("{label} task aborted: {e}");
}
}
}
}
pub fn parse_guest_config(uri: &str) -> Vec<(String, String)> {
let query = match uri.find('?') {
Some(i) => &uri[i + 1..],
None => return Vec::new(),
};
query
.split('&')
.filter_map(|pair| {
let (k, v) = pair.split_once('=')?;
if matches!(k, "bind" | "path" | "method") {
Some((k.to_string(), v.to_string()))
} else {
None
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
#[tokio::test]
async fn stop_does_not_wait_for_runtime_owned_run_task() {
let config = WasmConfig {
timeout_secs: 1,
..WasmConfig::default()
};
let mut consumer = WasmSourceConsumer::new(
PathBuf::from("unused.wasm"),
config,
Vec::new(),
Arc::new(Mutex::new(camel_core::Registry::new())),
);
consumer.run_task = Some(tokio::task::spawn_blocking(|| {
std::thread::sleep(std::time::Duration::from_secs(2));
Ok(())
}));
tokio::time::timeout(std::time::Duration::from_millis(100), consumer.stop())
.await
.expect("stop() must not wait for run_task; runtime owns that handle")
.expect("stop() should succeed");
}
}