use std::{
sync::{
atomic::{AtomicU64, AtomicUsize, Ordering},
Arc, Mutex,
},
time::Duration,
};
use mercurio_server::plugin::BoxFuture;
use tokio::sync::Semaphore;
use wasmtime::{Store, UpdateDeadline};
use super::{
runtime::{
bindings::{
self, exports::mercurio::plugin::guest::Guest, mercurio::plugin::types::Registration,
},
TICK,
},
sandbox::State,
};
pub(crate) struct Instance {
pub(crate) store: Store<State>,
pub(crate) guest: Guest,
_live: Live,
}
struct Live(Arc<AtomicUsize>);
impl Live {
fn new(count: &Arc<AtomicUsize>) -> Live {
count.fetch_add(1, Ordering::Relaxed);
Live(Arc::clone(count))
}
}
impl Drop for Live {
fn drop(&mut self) {
self.0.fetch_sub(1, Ordering::Relaxed);
}
}
#[derive(Default)]
pub(crate) struct Health {
failures: AtomicU64,
last_error: Mutex<Option<String>>,
}
impl Health {
fn failed(&self, error: String) {
self.failures.fetch_add(1, Ordering::Relaxed);
*self.last_error.lock().unwrap() = Some(error);
}
pub(crate) fn failures(&self) -> u64 {
self.failures.load(Ordering::Relaxed)
}
pub(crate) fn last_error(&self) -> Option<String> {
self.last_error.lock().unwrap().clone()
}
}
pub(crate) struct Pool {
name: Arc<str>,
pre: bindings::PluginPre<State>,
make: Box<dyn Fn() -> State + Send + Sync>,
config: Vec<(String, String)>,
timeout: Duration,
idle: Mutex<Vec<Instance>>,
permits: Semaphore,
live: Arc<AtomicUsize>,
health: Arc<Health>,
}
impl Pool {
pub(crate) fn new(
name: Arc<str>,
pre: bindings::PluginPre<State>,
make: Box<dyn Fn() -> State + Send + Sync>,
config: Vec<(String, String)>,
timeout: Duration,
instances: usize,
health: Arc<Health>,
) -> Pool {
Pool {
name,
pre,
make,
config,
timeout,
idle: Mutex::default(),
permits: Semaphore::new(instances.max(1)),
live: Arc::default(),
health,
}
}
pub(crate) fn live(&self) -> usize {
self.live.load(Ordering::Relaxed)
}
pub(crate) async fn create(&self) -> anyhow::Result<(Instance, Registration)> {
let mut store = Store::new(self.pre.engine(), (self.make)());
store.limiter(|state| &mut state.limits);
store.epoch_deadline_callback(|mut store| {
let state = store.data_mut();
if state.ticks_left == 0 {
return Ok(UpdateDeadline::Interrupt);
}
state.ticks_left -= 1;
Ok(UpdateDeadline::Yield(1))
});
self.arm(&mut store);
let init = async {
let plugin = self.pre.instantiate_async(&mut store).await?;
let guest = plugin.mercurio_plugin_guest().clone();
let registration = guest.call_init(&mut store, &self.config).await?;
Ok::<_, wasmtime::Error>((guest, registration))
};
let (guest, registration) =
tokio::time::timeout(self.timeout, init)
.await
.map_err(|_| {
anyhow::anyhow!(
"plugin `{}`: init took longer than {:?}",
self.name,
self.timeout
)
})??;
let registration =
registration.map_err(|message| anyhow::anyhow!("plugin `{}`: {message}", self.name))?;
let instance = Instance {
store,
guest,
_live: Live::new(&self.live),
};
Ok((instance, registration))
}
pub(crate) fn put(&self, instance: Instance) {
self.idle.lock().unwrap().push(instance);
}
pub(crate) async fn call<T>(
&self,
hook: &str,
call: impl FnOnce(&mut Instance) -> BoxFuture<'_, wasmtime::Result<T>>,
) -> Option<T> {
let _permit = self.permits.acquire().await.ok()?;
let idle = self.idle.lock().unwrap().pop();
let mut instance = match idle {
Some(instance) => instance,
None => match self.create().await {
Ok((instance, _)) => instance,
Err(error) => {
self.failed(format!("Couldn't start a copy for {hook}: {error:#}"));
return None;
}
},
};
self.arm(&mut instance.store);
let result = tokio::time::timeout(self.timeout, call(&mut instance)).await;
match result {
Ok(Ok(value)) => {
self.put(instance);
Some(value)
}
Err(_) => {
self.failed(format!("Took longer than {:?} in {hook}", self.timeout));
None
}
Ok(Err(error)) => {
self.failed(format!("Failed in {hook}: {error:#}"));
None
}
}
}
fn failed(&self, error: String) {
tracing::warn!(plugin = %self.name, "{error}");
self.health.failed(error);
}
fn arm(&self, store: &mut Store<State>) {
store.data_mut().ticks_left = (self.timeout.as_millis() / TICK.as_millis()) as u64;
store.set_epoch_deadline(1);
}
}