use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use crossbeam_channel::{bounded, Receiver, Sender, TrySendError};
use dashmap::DashMap;
use tensor_wasm_core::types::{InstanceId, TenantId};
use tracing::warn;
use crate::executor::{ExecError, SpawnConfig, TensorWasmExecutor};
use crate::instance::TensorWasmInstance;
pub type ModuleHash = [u8; 32];
const POOL_RESET_DEADLINE: Duration = Duration::from_millis(10);
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct InstancePoolConfig {
pub warm_instances_per_tuple: usize,
pub max_total_warm: usize,
}
impl InstancePoolConfig {
pub fn new(warm_instances_per_tuple: usize, max_total_warm: usize) -> Self {
Self {
warm_instances_per_tuple,
max_total_warm,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct PoolKey {
tenant_id: TenantId,
module_hash: ModuleHash,
}
struct PoolEntry {
sender: Sender<TensorWasmInstance>,
receiver: Receiver<TensorWasmInstance>,
module: wasmtime::Module,
#[allow(dead_code)]
capacity: usize,
}
pub struct InstancePool {
cfg: InstancePoolConfig,
pools: Arc<DashMap<PoolKey, Arc<PoolEntry>>>,
inflight: Arc<DashMap<PoolKey, Arc<tokio::sync::OnceCell<Arc<PoolEntry>>>>>,
warm_total: Arc<AtomicUsize>,
draws_total: Arc<AtomicUsize>,
hit_count: Arc<AtomicUsize>,
miss_count: Arc<AtomicUsize>,
drops_total: Arc<AtomicUsize>,
}
impl InstancePool {
pub fn new(cfg: InstancePoolConfig) -> Self {
Self {
cfg,
pools: Arc::new(DashMap::new()),
inflight: Arc::new(DashMap::new()),
warm_total: Arc::new(AtomicUsize::new(0)),
draws_total: Arc::new(AtomicUsize::new(0)),
hit_count: Arc::new(AtomicUsize::new(0)),
miss_count: Arc::new(AtomicUsize::new(0)),
drops_total: Arc::new(AtomicUsize::new(0)),
}
}
fn channel_capacity(&self) -> usize {
self.cfg.warm_instances_per_tuple.max(1)
}
pub async fn acquire(
&self,
executor: &TensorWasmExecutor,
wasm: &[u8],
cfg: SpawnConfig,
) -> Result<PooledInstance, ExecError> {
self.draws_total.fetch_add(1, Ordering::Relaxed);
if cfg.streaming.is_some() {
self.miss_count.fetch_add(1, Ordering::Relaxed);
let id = executor.spawn_instance(cfg, wasm).await?;
return Ok(PooledInstance {
inner: Some(id),
origin: None,
});
}
if !cfg.input.is_empty() {
self.miss_count.fetch_add(1, Ordering::Relaxed);
let id = executor.spawn_instance(cfg, wasm).await?;
return Ok(PooledInstance {
inner: Some(id),
origin: None,
});
}
let module_hash: ModuleHash = *blake3::hash(wasm).as_bytes();
let entry = self.ensure_entry(executor, wasm, &cfg, module_hash).await?;
match entry.receiver.try_recv() {
Ok(inst) => {
self.warm_total.fetch_sub(1, Ordering::Relaxed);
self.hit_count.fetch_add(1, Ordering::Relaxed);
let id = executor.register_pooled_instance(inst)?;
Ok(PooledInstance {
inner: Some(id),
origin: Some(module_hash),
})
}
Err(_empty) => {
self.miss_count.fetch_add(1, Ordering::Relaxed);
let id = executor.spawn_instance(cfg, wasm).await?;
Ok(PooledInstance {
inner: Some(id),
origin: Some(module_hash),
})
}
}
}
pub async fn release(
&self,
executor: &TensorWasmExecutor,
pooled: PooledInstance,
cfg: &SpawnConfig,
) {
let id = pooled.id();
let origin = pooled.origin();
let _ = pooled.into_inner();
let spent = match executor.detach_pooled_instance(id).await {
Some(inst) => inst,
None => {
return;
}
};
drop(spent);
executor.release_instance_slot(cfg.tenant_id);
if cfg.streaming.is_some() || origin.is_none() {
self.drops_total.fetch_add(1, Ordering::Relaxed);
return;
}
let module_hash = origin.expect("origin is Some by the check above");
let key = PoolKey {
tenant_id: cfg.tenant_id,
module_hash,
};
let entry = match self.pools.get(&key) {
Some(e) => e.value().clone(),
None => {
self.drops_total.fetch_add(1, Ordering::Relaxed);
return;
}
};
if self.cfg.max_total_warm > 0
&& self.warm_total.load(Ordering::Relaxed) >= self.cfg.max_total_warm
{
self.drops_total.fetch_add(1, Ordering::Relaxed);
return;
}
let start = Instant::now();
let replacement = match executor
.rebuild_pooled_from_module(cfg, &entry.module)
.await
{
Ok(inst) => inst,
Err(err) => {
warn!(
target: "tensor_wasm_exec::instance_pool",
error = %err,
"pool reset failed; replacement instance dropped",
);
self.drops_total.fetch_add(1, Ordering::Relaxed);
return;
}
};
let elapsed = start.elapsed();
if elapsed > POOL_RESET_DEADLINE {
warn!(
target: "tensor_wasm_exec::instance_pool",
elapsed_ms = elapsed.as_millis() as u64,
budget_ms = POOL_RESET_DEADLINE.as_millis() as u64,
"pool reset exceeded deadline; replacement instance dropped to keep invoke latency bounded",
);
drop(replacement);
executor.release_instance_slot(cfg.tenant_id);
self.drops_total.fetch_add(1, Ordering::Relaxed);
return;
}
match entry.sender.try_send(replacement) {
Ok(()) => {
self.warm_total.fetch_add(1, Ordering::Relaxed);
}
Err(TrySendError::Full(dropped)) | Err(TrySendError::Disconnected(dropped)) => {
drop(dropped);
executor.release_instance_slot(cfg.tenant_id);
self.drops_total.fetch_add(1, Ordering::Relaxed);
}
}
}
async fn ensure_entry(
&self,
executor: &TensorWasmExecutor,
wasm: &[u8],
cfg: &SpawnConfig,
module_hash: ModuleHash,
) -> Result<Arc<PoolEntry>, ExecError> {
let key = PoolKey {
tenant_id: cfg.tenant_id,
module_hash,
};
if let Some(entry) = self.pools.get(&key) {
return Ok(entry.value().clone());
}
let cell = self
.inflight
.entry(key.clone())
.or_insert_with(|| Arc::new(tokio::sync::OnceCell::new()))
.value()
.clone();
let entry = cell
.get_or_try_init(|| self.build_entry(executor, wasm, cfg, key.clone()))
.await?
.clone();
self.inflight.remove(&key);
Ok(entry)
}
async fn build_entry(
&self,
executor: &TensorWasmExecutor,
wasm: &[u8],
cfg: &SpawnConfig,
key: PoolKey,
) -> Result<Arc<PoolEntry>, ExecError> {
let warm_n = self.cfg.warm_instances_per_tuple;
let cap = self.channel_capacity();
let (sender, receiver) = bounded::<TensorWasmInstance>(cap);
let mut module_opt: Option<wasmtime::Module> = None;
for i in 0..warm_n {
if self.cfg.max_total_warm > 0
&& self.warm_total.load(Ordering::Relaxed) >= self.cfg.max_total_warm
{
break;
}
if i == 0 {
let (inst, module, _) = executor.build_pooled_instance(cfg, wasm).await?;
if sender.try_send(inst).is_err() {
executor.release_instance_slot(cfg.tenant_id);
} else {
self.warm_total.fetch_add(1, Ordering::Relaxed);
}
module_opt = Some(module);
} else {
let module = module_opt.as_ref().expect("module captured on i==0");
let inst = executor.rebuild_pooled_from_module(cfg, module).await?;
if sender.try_send(inst).is_err() {
executor.release_instance_slot(cfg.tenant_id);
} else {
self.warm_total.fetch_add(1, Ordering::Relaxed);
}
}
}
let module = match module_opt {
Some(m) => m,
None => {
let (inst, module, _) = executor.build_pooled_instance(cfg, wasm).await?;
drop(inst);
executor.release_instance_slot(cfg.tenant_id);
module
}
};
let entry = Arc::new(PoolEntry {
sender,
receiver,
module,
capacity: cap,
});
self.pools.insert(key, entry.clone());
Ok(entry)
}
pub fn warm_count(&self) -> usize {
self.warm_total.load(Ordering::Relaxed)
}
pub fn draws_total(&self) -> usize {
self.draws_total.load(Ordering::Relaxed)
}
pub fn hit_count(&self) -> usize {
self.hit_count.load(Ordering::Relaxed)
}
pub fn miss_count(&self) -> usize {
self.miss_count.load(Ordering::Relaxed)
}
pub fn drops_total(&self) -> usize {
self.drops_total.load(Ordering::Relaxed)
}
pub fn config(&self) -> &InstancePoolConfig {
&self.cfg
}
pub fn shutdown(&self, executor: &TensorWasmExecutor) {
for entry in self.pools.iter() {
let tenant = entry.key().tenant_id;
while let Ok(inst) = entry.value().receiver.try_recv() {
drop(inst);
executor.release_instance_slot(tenant);
self.warm_total.fetch_sub(1, Ordering::Relaxed);
self.drops_total.fetch_add(1, Ordering::Relaxed);
}
}
}
}
pub struct PooledInstance {
inner: Option<InstanceId>,
origin: Option<ModuleHash>,
}
impl PooledInstance {
pub fn id(&self) -> InstanceId {
self.inner.expect("PooledInstance was already returned")
}
pub fn into_inner(mut self) -> InstanceId {
self.inner
.take()
.expect("PooledInstance was already returned")
}
pub(crate) fn origin(&self) -> Option<ModuleHash> {
self.origin
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_is_disabled() {
let cfg = InstancePoolConfig::default();
assert_eq!(cfg.warm_instances_per_tuple, 0);
assert_eq!(cfg.max_total_warm, 0);
}
#[test]
fn warm_count_starts_at_zero() {
let pool = InstancePool::new(InstancePoolConfig::default());
assert_eq!(pool.warm_count(), 0);
}
#[test]
fn config_round_trips() {
let pool = InstancePool::new(InstancePoolConfig {
warm_instances_per_tuple: 4,
max_total_warm: 32,
});
assert_eq!(pool.config().warm_instances_per_tuple, 4);
assert_eq!(pool.config().max_total_warm, 32);
}
#[test]
fn channel_capacity_floor_at_one() {
let pool = InstancePool::new(InstancePoolConfig::default());
assert_eq!(pool.channel_capacity(), 1);
}
}