use std::sync::Arc;
use std::time::Instant;
use crate::cell::PluginCell;
use crate::error::WasmtimeRuntimeError;
mod hooks;
mod worker;
pub use hooks::{
call_filter_hook, call_observe_hook, call_shape_hook, call_transform_response_hook,
call_transform_sse_event_hook,
};
pub(crate) use hooks::{
call_filter_hook_scoped, call_shape_hook_scoped, call_transform_response_hook_scoped,
call_transform_sse_event_hook_scoped,
};
use worker::{WorkerInstance, build_worker_instance};
fn trap_phase_label(err: &WasmtimeRuntimeError) -> &'static str {
match err {
WasmtimeRuntimeError::GuestTrap { phase, .. } => phase,
WasmtimeRuntimeError::ModuleRejected { .. } => "reject",
WasmtimeRuntimeError::InstantiateFailed(_) => "instantiate",
WasmtimeRuntimeError::ModuleCompile(_) => "compile",
WasmtimeRuntimeError::EngineInit(_) => "engine_init",
WasmtimeRuntimeError::ProbeFailed { .. } => "probe",
WasmtimeRuntimeError::PoolSaturated { .. } => "pool_saturated",
}
}
pub const DEFAULT_ALIGN: u32 = 16;
#[derive(Clone, Copy)]
enum HookFn {
Filter,
Shape,
Observe,
TransformResponse,
TransformSseEvent,
}
impl HookFn {
fn export_name(self) -> &'static str {
match self {
HookFn::Filter => "cc_lb_filter",
HookFn::Shape => "cc_lb_shape",
HookFn::Observe => "cc_lb_observe",
HookFn::TransformResponse => "cc_lb_transform_response",
HookFn::TransformSseEvent => "cc_lb_transform_sse_event",
}
}
fn metric_label(self) -> &'static str {
match self {
HookFn::Filter => "filter",
HookFn::Shape => "shape",
HookFn::Observe => "observe",
HookFn::TransformResponse => "transform_response",
HookFn::TransformSseEvent => "transform_sse_event",
}
}
}
fn call_hook_scoped<R, F>(
cell: &Arc<PluginCell>,
input: &[u8],
hook: HookFn,
with_output: F,
) -> Result<R, WasmtimeRuntimeError>
where
F: for<'a> FnOnce(&'a [u8]) -> R,
{
let _store_permit = cell
.store_budget
.try_acquire()
.inspect_err(record_pool_saturation)?;
let mut wi = build_worker_instance(cell, hook).inspect_err(record_pool_saturation)?;
execute_call_scoped(&mut wi, cell, input, hook, with_output)
}
fn record_pool_saturation(err: &WasmtimeRuntimeError) {
if let WasmtimeRuntimeError::PoolSaturated { resource } = err {
metrics::counter!("cc_lb_plugin_pool_saturation_total", "resource" => *resource)
.increment(1);
}
}
fn execute_call_scoped<R, F>(
wi: &mut WorkerInstance,
cell: &PluginCell,
input: &[u8],
hook: HookFn,
with_output: F,
) -> Result<R, WasmtimeRuntimeError>
where
F: for<'a> FnOnce(&'a [u8]) -> R,
{
let start = Instant::now();
let plugin: Arc<str> = Arc::clone(&cell.plugin_name);
let hook_label = hook.metric_label();
let result = execute_call_inner_scoped(wi, input, hook, with_output);
metrics::histogram!(
"cc_lb_plugin_call_duration_seconds",
"plugin" => Arc::clone(&plugin),
"hook" => hook_label,
)
.record(start.elapsed().as_secs_f64());
match &result {
Ok(_) => {}
Err(err) => {
metrics::counter!(
"cc_lb_plugin_trap_total",
"plugin" => plugin,
"hook" => hook_label,
"phase" => trap_phase_label(err),
)
.increment(1);
}
}
result
}
fn execute_call_inner_scoped<R, F>(
wi: &mut WorkerInstance,
input: &[u8],
hook: HookFn,
with_output: F,
) -> Result<R, WasmtimeRuntimeError>
where
F: for<'a> FnOnce(&'a [u8]) -> R,
{
let WorkerInstance {
store,
memory,
alloc_fn,
free_fn,
filter_fn,
shape_fn,
observe_fn,
transform_response_fn,
transform_sse_event_fn,
} = wi;
let hook_fn = match hook {
HookFn::Filter => filter_fn.as_ref(),
HookFn::Shape => shape_fn.as_ref(),
HookFn::Observe => observe_fn.as_ref(),
HookFn::TransformResponse => transform_response_fn.as_ref(),
HookFn::TransformSseEvent => transform_sse_event_fn.as_ref(),
}
.ok_or_else(|| WasmtimeRuntimeError::ModuleRejected {
reason: format!(
"plugin does not export `{}` — slot kind mismatch (inspect should have caught this)",
hook.export_name()
),
})?;
let input_len: u32 =
input
.len()
.try_into()
.map_err(|_| WasmtimeRuntimeError::ModuleRejected {
reason: format!("input too large: {} bytes exceeds u32::MAX", input.len()),
})?;
let in_ptr = alloc_fn
.call(&mut *store, (input_len, DEFAULT_ALIGN))
.map_err(|e| WasmtimeRuntimeError::GuestTrap {
phase: "cc_lb_alloc",
source: anyhow::Error::from(e),
})?;
if in_ptr == 0 {
return Err(WasmtimeRuntimeError::GuestTrap {
phase: "cc_lb_alloc",
source: anyhow::anyhow!("guest returned null pointer for {input_len}-byte allocation"),
});
}
memory
.write(&mut *store, in_ptr as usize, input)
.map_err(|e| WasmtimeRuntimeError::GuestTrap {
phase: "memory.write",
source: anyhow::Error::from(e),
})?;
let packed = hook_fn
.call(&mut *store, (in_ptr, input_len))
.map_err(|e| WasmtimeRuntimeError::GuestTrap {
phase: hook.export_name(),
source: anyhow::Error::from(e),
})?;
let out_ptr = (packed >> 32) as u32;
let out_len = (packed & 0xFFFF_FFFF) as u32;
let output = if matches!(hook, HookFn::Observe) && out_ptr == 0 && out_len == 0 {
with_output(&[])
} else if out_ptr == 0 || out_len == 0 {
return Err(WasmtimeRuntimeError::ModuleRejected {
reason: format!(
"{}: guest returned invalid (ptr={out_ptr}, len={out_len}); only observe may return (0, 0)",
hook.export_name()
),
});
} else {
let mem_view = memory.data(&*store);
let out_end = (out_ptr as usize)
.checked_add(out_len as usize)
.ok_or_else(|| WasmtimeRuntimeError::ModuleRejected {
reason: "guest output ptr+len overflows usize".into(),
})?;
if out_end > mem_view.len() {
return Err(WasmtimeRuntimeError::ModuleRejected {
reason: format!(
"guest output [{}..{}] out of bounds (memory size {})",
out_ptr,
out_end,
mem_view.len()
),
});
}
let output = with_output(&mem_view[out_ptr as usize..out_end]);
let _ = free_fn;
output
};
Ok(output)
}