use std::sync::Arc;
use std::time::{Duration, Instant};
use parking_lot::{Condvar, Mutex};
#[cfg(test)]
use crate::NoopSink;
use crate::runtime::FilterRuntime;
use crate::{Isolation, LogLine, RunError, TelemetrySink};
#[cfg(test)]
use anyhow::Result;
pub(crate) type HookResult<T> = std::result::Result<(T, Vec<LogLine>), (RunError, Vec<LogLine>)>;
pub(crate) struct LoadedInner<R: FilterRuntime> {
pub(crate) runtime: R,
pub(crate) filter_id: String,
pub(crate) sink: Arc<dyn TelemetrySink>,
pub(crate) isolation: Isolation,
untrusted_breaker: Mutex<TrapBreaker>,
}
struct TrapBreaker {
consecutive_traps: u32,
cooldown_until: Option<Instant>,
threshold: u32,
cooldown: Duration,
}
impl TrapBreaker {
fn new(threshold: u32, cooldown: Duration) -> Self {
Self {
consecutive_traps: 0,
cooldown_until: None,
threshold,
cooldown,
}
}
fn record_trap(&mut self) {
self.consecutive_traps = self.consecutive_traps.saturating_add(1);
if self.consecutive_traps >= self.threshold {
self.cooldown_until = Some(Instant::now() + self.cooldown);
}
}
fn clear(&mut self) {
self.consecutive_traps = 0;
self.cooldown_until = None;
}
fn is_open(&self) -> bool {
self.cooldown_until.is_some_and(|t| Instant::now() < t)
}
}
const UNTRUSTED_TRAP_BREAKER_THRESHOLD: u32 = 3;
const UNTRUSTED_TRAP_COOLDOWN: Duration = Duration::from_millis(500);
struct LiveSlotGuard<'a, I> {
pool: &'a TrustedPool<I>,
armed: bool,
}
impl<I> Drop for LiveSlotGuard<'_, I> {
fn drop(&mut self) {
if self.armed {
{
let mut g = self.pool.inner.lock();
g.live = g.live.saturating_sub(1);
}
self.pool.available.notify_one();
}
}
}
impl<R: FilterRuntime> LoadedInner<R> {
pub(crate) fn new(
runtime: R,
filter_id: String,
sink: Arc<dyn TelemetrySink>,
isolation: Isolation,
) -> Self {
Self {
runtime,
filter_id,
sink,
isolation,
untrusted_breaker: Mutex::new(TrapBreaker::new(
UNTRUSTED_TRAP_BREAKER_THRESHOLD,
UNTRUSTED_TRAP_COOLDOWN,
)),
}
}
fn checkout(
&self,
pool: &TrustedPool<R::Instance>,
) -> std::result::Result<PooledInstance<R::Instance>, RunError> {
enum Step<I> {
Use(PooledInstance<I>),
Build,
Retry,
}
let deadline = Instant::now() + pool.checkout_timeout;
loop {
let step = {
let mut g = pool.inner.lock();
if let Some(p) = g.idle.pop() {
Step::Use(p)
} else if g.breaker.is_open() {
return Err(RunError::Unavailable);
} else if g.live < pool.cap {
g.live += 1; Step::Build
} else if pool.available.wait_until(&mut g, deadline).timed_out() {
return Err(RunError::Unavailable);
} else {
Step::Retry
}
};
match step {
Step::Use(p) => return Ok(p),
Step::Build => {
let mut guard = LiveSlotGuard { pool, armed: true };
match self.runtime.instantiate_initialized() {
Ok(instance) => {
guard.armed = false;
return Ok(PooledInstance {
instance,
served: 0,
});
}
Err(e) => {
pool.inner.lock().breaker.record_trap();
return Err(RunError::Instantiate(e));
}
}
}
Step::Retry => continue,
}
}
}
fn run_fresh<T>(
&self,
call: impl FnOnce(&mut R::Instance) -> wasmtime::Result<T>,
) -> HookResult<T> {
if self.untrusted_breaker.lock().is_open() {
return Err((RunError::Unavailable, Vec::new()));
}
let mut inst = match self.runtime.instantiate_initialized() {
Ok(inst) => inst,
Err(e) => {
self.untrusted_breaker.lock().record_trap();
return Err((RunError::Instantiate(e), Vec::new()));
}
};
self.runtime.set_request_deadline(&mut inst);
match call(&mut inst) {
Ok(value) => {
let logs = self.runtime.take_logs_final(&mut inst);
self.untrusted_breaker.lock().clear();
Ok((value, logs))
}
Err(e) => {
let logs = self.runtime.take_logs_final(&mut inst);
self.untrusted_breaker.lock().record_trap();
Err((RunError::from_call(e), logs))
}
}
}
fn run_pooled<T>(
&self,
pool: &TrustedPool<R::Instance>,
call: impl FnOnce(&mut R::Instance) -> wasmtime::Result<T>,
) -> HookResult<T> {
let mut pooled = self.checkout(pool).map_err(|e| (e, Vec::new()))?;
let mut slot = LiveSlotGuard { pool, armed: true };
self.runtime.begin_request(&mut pooled.instance);
self.runtime.set_request_deadline(&mut pooled.instance);
let result = call(&mut pooled.instance);
match result {
Ok(value) => {
pooled.served = pooled.served.saturating_add(1);
let recycle = pooled.served >= pool.max_requests_per_instance;
let logs = if recycle {
self.runtime.take_logs_final(&mut pooled.instance)
} else {
self.runtime.take_logs(&mut pooled.instance)
};
slot.armed = false;
if recycle {
drop(pooled);
let mut g = pool.inner.lock();
g.breaker.clear();
g.live = g.live.saturating_sub(1);
} else {
let mut g = pool.inner.lock();
g.breaker.clear();
g.idle.push(pooled);
}
pool.available.notify_one();
Ok((value, logs))
}
Err(e) => {
let logs = self.runtime.take_logs_final(&mut pooled.instance);
slot.armed = false;
drop(pooled);
let mut g = pool.inner.lock();
g.live = g.live.saturating_sub(1);
g.breaker.record_trap();
drop(g);
pool.available.notify_one();
Err((RunError::from_call(e), logs))
}
}
}
pub(crate) fn run_hook<T>(
&self,
trusted: Option<&TrustedPool<R::Instance>>,
call: impl FnOnce(&mut R::Instance) -> wasmtime::Result<T>,
) -> HookResult<T> {
match trusted {
Some(pool) => self.run_pooled(pool, call),
None => self.run_fresh(call),
}
}
}
const TRUSTED_TRAP_BREAKER_THRESHOLD: u32 = 3;
const TRUSTED_TRAP_COOLDOWN: Duration = Duration::from_millis(500);
pub(crate) struct PooledInstance<I> {
instance: I,
served: u64,
}
struct PoolInner<I> {
idle: Vec<PooledInstance<I>>,
live: usize,
breaker: TrapBreaker,
}
pub(crate) struct TrustedPool<I> {
inner: Mutex<PoolInner<I>>,
available: Condvar,
cap: usize,
checkout_timeout: Duration,
max_requests_per_instance: u64,
}
impl<I> TrustedPool<I> {
pub(crate) fn new(
cap: usize,
checkout_timeout: Duration,
max_requests_per_instance: u64,
first: I,
) -> Self {
Self {
inner: Mutex::new(PoolInner {
idle: vec![PooledInstance {
instance: first,
served: 0,
}],
live: 1,
breaker: TrapBreaker::new(TRUSTED_TRAP_BREAKER_THRESHOLD, TRUSTED_TRAP_COOLDOWN),
}),
available: Condvar::new(),
cap,
checkout_timeout,
max_requests_per_instance,
}
}
}
#[cfg(test)]
mod pool_tests {
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use super::*;
struct FakeInstance {
id: u64,
}
struct FakeRuntime {
next_id: AtomicU64,
instantiate_calls: AtomicUsize,
fail_instantiate: AtomicBool,
}
impl FakeRuntime {
fn new() -> Self {
Self {
next_id: AtomicU64::new(0),
instantiate_calls: AtomicUsize::new(0),
fail_instantiate: AtomicBool::new(false),
}
}
fn instantiate_calls(&self) -> usize {
self.instantiate_calls.load(Ordering::SeqCst)
}
}
fn marker_log(message: &str) -> Vec<LogLine> {
vec![LogLine {
level: crate::LogLevel::Debug,
message: message.to_string(),
}]
}
impl FilterRuntime for FakeRuntime {
type Instance = FakeInstance;
fn instantiate_initialized(&self) -> Result<FakeInstance> {
self.instantiate_calls.fetch_add(1, Ordering::SeqCst);
if self.fail_instantiate.load(Ordering::SeqCst) {
anyhow::bail!("fake instantiate failure");
}
Ok(FakeInstance {
id: self.next_id.fetch_add(1, Ordering::SeqCst),
})
}
fn begin_request(&self, _instance: &mut FakeInstance) {}
fn set_request_deadline(&self, _instance: &mut FakeInstance) {}
fn take_logs(&self, _instance: &mut FakeInstance) -> Vec<LogLine> {
marker_log("take_logs")
}
fn take_logs_final(&self, _instance: &mut FakeInstance) -> Vec<LogLine> {
marker_log("take_logs_final")
}
}
fn fake_inner(runtime: FakeRuntime) -> LoadedInner<FakeRuntime> {
LoadedInner::new(
runtime,
"test".to_string(),
Arc::new(NoopSink),
Isolation::Trusted,
)
}
#[test]
fn trusted_pool_init_failure_opens_circuit_breaker() {
let runtime = FakeRuntime::new();
let first = runtime.instantiate_initialized().unwrap();
let pool = TrustedPool::new(4, Duration::from_millis(50), 1000, first);
let inner = fake_inner(runtime);
let held = inner.checkout(&pool).expect("idle checkout succeeds");
inner.runtime.fail_instantiate.store(true, Ordering::SeqCst);
for _ in 0..TRUSTED_TRAP_BREAKER_THRESHOLD {
match inner.checkout(&pool) {
Err(RunError::Instantiate(_)) => {}
Err(e) => panic!("expected Instantiate, got {e:?}"),
Ok(_) => panic!("expected Instantiate, got Ok"),
}
}
match inner.checkout(&pool) {
Err(RunError::Unavailable) => {}
Err(e) => panic!("after threshold, expected Unavailable, got {e:?}"),
Ok(_) => panic!("after threshold, expected Unavailable, got Ok"),
}
pool.inner.lock().idle.push(held);
assert!(
inner.checkout(&pool).is_ok(),
"idle reuse must stay allowed while the breaker is open"
);
}
#[test]
fn trusted_pool_lazily_fills_and_reuses_idle_instance() {
let runtime = FakeRuntime::new();
let first = runtime.instantiate_initialized().unwrap();
let pool = TrustedPool::new(2, Duration::from_millis(50), 1000, first);
let inner = fake_inner(runtime);
let a = inner
.run_hook(Some(&pool), |inst: &mut FakeInstance| {
Ok::<_, wasmtime::Error>(inst.id)
})
.unwrap();
let b = inner
.run_hook(Some(&pool), |inst: &mut FakeInstance| {
Ok::<_, wasmtime::Error>(inst.id)
})
.unwrap();
assert_eq!(
a.0, b.0,
"a single-threaded caller reuses the same instance"
);
assert_eq!(
inner.runtime.instantiate_calls(),
1,
"only the eager initial build — no rebuild needed to reuse an idle instance"
);
}
#[test]
fn trusted_pool_checkout_waits_then_fails_closed_when_saturated() {
let runtime = FakeRuntime::new();
let first = runtime.instantiate_initialized().unwrap();
let pool = TrustedPool::new(1, Duration::from_millis(20), 1000, first);
let inner = fake_inner(runtime);
let _held = inner.checkout(&pool).expect("first checkout succeeds");
let failed_closed = matches!(inner.checkout(&pool), Err(RunError::Unavailable));
assert!(
failed_closed,
"a saturated pool should time out and fail closed"
);
}
#[test]
fn trusted_pool_recycles_after_max_requests_per_instance() {
let runtime = FakeRuntime::new();
let first = runtime.instantiate_initialized().unwrap();
let pool = TrustedPool::new(4, Duration::from_millis(50), 2, first);
let inner = fake_inner(runtime);
for _ in 0..2 {
inner
.run_hook(Some(&pool), |inst: &mut FakeInstance| {
Ok::<_, wasmtime::Error>(inst.id)
})
.unwrap();
}
inner
.run_hook(Some(&pool), |inst: &mut FakeInstance| {
Ok::<_, wasmtime::Error>(inst.id)
})
.unwrap();
assert_eq!(
inner.runtime.instantiate_calls(),
2,
"instance recycles (rebuilds) after serving max_requests_per_instance"
);
}
#[test]
fn trusted_pool_opens_circuit_breaker_after_consecutive_traps_and_cools_down_then_self_heals() {
let runtime = FakeRuntime::new();
let first = runtime.instantiate_initialized().unwrap();
let pool = TrustedPool::new(4, Duration::from_millis(50), 1000, first);
let inner = fake_inner(runtime);
for _ in 0..TRUSTED_TRAP_BREAKER_THRESHOLD {
let result = inner.run_hook(Some(&pool), |_inst: &mut FakeInstance| {
wasmtime::Result::<()>::Err(wasmtime::Error::msg("simulated trap"))
});
assert!(
matches!(result, Err((RunError::Trap(_), _))),
"expected a Trap before the breaker opens, got {result:?}"
);
}
let result = inner.run_hook(
Some(&pool),
|_inst: &mut FakeInstance| -> wasmtime::Result<()> {
panic!("must not be called while the circuit breaker is open")
},
);
assert!(
matches!(result, Err((RunError::Unavailable, _))),
"the pool-wide breaker should be open"
);
std::thread::sleep(TRUSTED_TRAP_COOLDOWN + Duration::from_millis(20));
let result = inner.run_hook(Some(&pool), |inst: &mut FakeInstance| {
Ok::<_, wasmtime::Error>(inst.id)
});
assert!(
result.is_ok(),
"the pool should self-heal (rebuild) once the cooldown elapses, got {result:?}"
);
}
#[test]
fn untrusted_lifecycle_instantiates_fresh_every_call() {
let runtime = FakeRuntime::new();
let inner = fake_inner(runtime);
for _ in 0..3 {
inner
.run_hook(None, |inst: &mut FakeInstance| {
Ok::<_, wasmtime::Error>(inst.id)
})
.unwrap();
}
assert_eq!(
inner.runtime.instantiate_calls(),
3,
"the untrusted lifecycle instantiates fresh + re-inits on every single call"
);
}
#[test]
fn untrusted_lifecycle_opens_circuit_breaker_after_consecutive_traps_and_cools_down_then_self_heals()
{
let runtime = FakeRuntime::new();
let inner = fake_inner(runtime);
for _ in 0..UNTRUSTED_TRAP_BREAKER_THRESHOLD {
let result = inner.run_hook(None, |_inst: &mut FakeInstance| {
wasmtime::Result::<()>::Err(wasmtime::Error::msg("simulated trap"))
});
assert!(
matches!(result, Err((RunError::Trap(_), _))),
"expected a Trap before the breaker opens, got {result:?}"
);
}
let calls_before_open = inner.runtime.instantiate_calls();
let result = inner.run_hook(None, |_inst: &mut FakeInstance| -> wasmtime::Result<()> {
panic!("must not be called while the untrusted breaker is open")
});
assert!(
matches!(result, Err((RunError::Unavailable, _))),
"the per-filter untrusted breaker should be open"
);
assert_eq!(
inner.runtime.instantiate_calls(),
calls_before_open,
"a call during the cooldown must not pay instantiate_initialized at all"
);
std::thread::sleep(UNTRUSTED_TRAP_COOLDOWN + Duration::from_millis(20));
let result = inner.run_hook(None, |inst: &mut FakeInstance| {
Ok::<_, wasmtime::Error>(inst.id)
});
assert!(
result.is_ok(),
"the untrusted lifecycle should self-heal once the cooldown elapses, got {result:?}"
);
}
#[test]
fn run_pooled_uses_final_drain_on_the_recycling_call() {
let runtime = FakeRuntime::new();
let first = runtime.instantiate_initialized().unwrap();
let pool = TrustedPool::new(4, Duration::from_millis(50), 2, first);
let inner = fake_inner(runtime);
let (_v, logs) = inner
.run_hook(Some(&pool), |i: &mut FakeInstance| {
Ok::<_, wasmtime::Error>(i.id)
})
.unwrap();
assert_eq!(
logs,
marker_log("take_logs"),
"a call returning to idle uses the mid-lifetime drain"
);
let (_v, logs) = inner
.run_hook(Some(&pool), |i: &mut FakeInstance| {
Ok::<_, wasmtime::Error>(i.id)
})
.unwrap();
assert_eq!(
logs,
marker_log("take_logs_final"),
"the recycling call must use the final drain"
);
}
#[test]
fn run_fresh_recovers_final_logs_on_success_not_just_on_trap() {
let runtime = FakeRuntime::new();
let inner = fake_inner(runtime);
let (_value, logs) = inner
.run_hook(None, |inst: &mut FakeInstance| {
Ok::<_, wasmtime::Error>(inst.id)
})
.expect("a non-trapping call succeeds");
assert_eq!(
logs,
marker_log("take_logs_final"),
"run_fresh's Ok arm must use the final drain (partial-line flush), \
not the plain mid-lifetime drain — the instance is discarded either way"
);
}
}