use super::*;
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct PoolHealthStatus {
pub available: usize,
pub checked_out: usize,
pub total: usize,
pub replacements: usize,
pub contracts_in_flight: usize,
}
pub struct RuntimePool {
runtimes: Vec<Option<Executor<Runtime>>>,
available: Semaphore,
config: Arc<Config>,
op_manager: Arc<OpManager>,
pool_size: usize,
checked_out: AtomicUsize,
replacements_count: AtomicUsize,
shared_state_store: StateStore<Storage>,
shared_notifications: SharedNotifications,
shared_summaries: SharedSummaries,
shared_client_counts: SharedClientCounts,
shared_contract_modules: SharedModuleCache<ContractKey>,
shared_delegate_modules: SharedModuleCache<DelegateKey>,
shared_delegate_contexts: crate::wasm_runtime::DelegateContextCache,
shared_backend_engine: BackendEngine,
shared_recovery_guard: super::CorruptedStateRecoveryGuard,
delegate_notification_tx: super::DelegateNotificationSender,
delegate_notification_rx: Option<super::DelegateNotificationReceiver>,
in_flight_contracts: HashMap<ContractKey, usize>,
export_semaphore: Arc<Semaphore>,
_inactive_user_sweep: Option<crate::util::AbortOnDrop>,
}
pub(crate) const MAX_CONCURRENT_EXPORTS: usize = 2;
pub(crate) fn effective_export_permits(pool_size: usize) -> usize {
MAX_CONCURRENT_EXPORTS.min(pool_size.saturating_sub(1))
}
pub(crate) enum ExportAdmission {
Admitted(Box<ExportJob>),
Busy,
Unsupported,
}
pub(crate) struct ExportJob {
executor: Executor<Runtime>,
_permit: tokio::sync::OwnedSemaphorePermit,
user_context: UserSecretContext,
token: zeroize::Zeroizing<Vec<u8>>,
}
impl ExportJob {
pub(crate) async fn run(self) -> ExportDone {
let ExportJob {
executor,
_permit,
user_context,
token,
} = self;
match super::run_blocking_offloaded(executor, move |executor| {
let result = executor.export_user_secrets(&user_context, token.as_slice());
(executor, result)
})
.await
{
super::OffloadOutcome::Completed(executor, result) => ExportDone {
executor: Some(executor),
result,
},
super::OffloadOutcome::Panicked => ExportDone {
executor: None,
result: Err(ExecutorError::other(anyhow::anyhow!(
"secret export task panicked"
))),
},
}
}
}
pub(crate) struct ExportDone {
executor: Option<Executor<Runtime>>,
pub(crate) result: Result<Vec<u8>, ExecutorError>,
}
impl ExportDone {
pub(crate) fn lost_to_drop() -> Self {
Self {
executor: None,
result: Err(ExecutorError::other(anyhow::anyhow!(
"secret export task was dropped before completion"
))),
}
}
pub(crate) fn into_result(self) -> Result<Vec<u8>, ExecutorError> {
self.result
}
}
impl RuntimePool {
pub async fn new(
config: Arc<Config>,
op_manager: Arc<OpManager>,
pool_size: NonZeroUsize,
) -> anyhow::Result<Self> {
let pool_size_usize: usize = pool_size.into();
let mut runtimes = Vec::with_capacity(pool_size_usize);
let (_, _, _, shared_state_store) = Executor::<Runtime>::get_stores(&config).await?;
let shared_notifications: SharedNotifications = Arc::new(DashMap::new());
let shared_summaries: SharedSummaries = Arc::new(DashMap::new());
let shared_client_counts: SharedClientCounts = Arc::new(DashMap::new());
let (delegate_notification_tx, delegate_notification_rx) =
tokio::sync::mpsc::channel(super::DELEGATE_NOTIFICATION_CHANNEL_SIZE);
let contract_cache_budget = config.module_cache_budget_bytes;
let delegate_cache_budget = (contract_cache_budget
/ crate::wasm_runtime::DELEGATE_MODULE_CACHE_BUDGET_DIVISOR)
.max(1);
let module_cache_metrics = op_manager.ring.module_cache_metrics();
let shared_contract_modules: SharedModuleCache<ContractKey> =
Arc::new(Mutex::new(ModuleCache::with_label(
contract_cache_budget,
"contract",
Some(module_cache_metrics.clone()),
)));
let shared_delegate_modules: SharedModuleCache<DelegateKey> =
Arc::new(Mutex::new(ModuleCache::with_label(
delegate_cache_budget,
"delegate",
Some(module_cache_metrics),
)));
let shared_delegate_contexts = crate::wasm_runtime::new_delegate_context_cache();
let shared_recovery_guard: super::CorruptedStateRecoveryGuard =
Arc::new(std::sync::Mutex::new(HashSet::new()));
let mut first_executor = Executor::from_config_with_shared_modules(
config.clone(),
shared_state_store.clone(),
Some(op_manager.clone()),
shared_contract_modules.clone(),
shared_delegate_modules.clone(),
shared_delegate_contexts.clone(),
None, )
.await?;
let shared_backend_engine = first_executor.runtime.clone_backend_engine();
first_executor.set_shared_notifications(
shared_notifications.clone(),
shared_summaries.clone(),
shared_client_counts.clone(),
);
first_executor.set_recovery_guard(shared_recovery_guard.clone());
first_executor.set_delegate_notification_tx(delegate_notification_tx.clone());
runtimes.push(Some(first_executor));
for i in 1..pool_size_usize {
let mut executor = Executor::from_config_with_shared_modules(
config.clone(),
shared_state_store.clone(),
Some(op_manager.clone()),
shared_contract_modules.clone(),
shared_delegate_modules.clone(),
shared_delegate_contexts.clone(),
Some(shared_backend_engine.clone()),
)
.await?;
executor.set_shared_notifications(
shared_notifications.clone(),
shared_summaries.clone(),
shared_client_counts.clone(),
);
executor.set_recovery_guard(shared_recovery_guard.clone());
executor.set_delegate_notification_tx(delegate_notification_tx.clone());
runtimes.push(Some(executor));
if i < pool_size_usize - 1 {
tokio::task::yield_now().await;
}
}
tracing::info!(pool_size = pool_size_usize, "RuntimePool created");
let inactive_user_sweep = if crate::wasm_runtime::should_spawn_inactive_user_sweep(
config.ws_api.hosted_mode,
config.per_user_inactive_ttl_secs,
) {
crate::wasm_runtime::spawn_inactive_user_sweep(
config.secrets_dir(),
shared_state_store.storage(),
config.per_user_inactive_ttl_secs,
config.inactive_user_sweep_interval_secs,
)
.map(|handle| {
let mut guard = crate::util::AbortOnDrop::new();
guard.push(handle.abort_handle());
guard
})
} else {
None
};
Ok(Self {
runtimes,
available: Semaphore::new(pool_size_usize),
config,
op_manager,
pool_size: pool_size_usize,
checked_out: AtomicUsize::new(0),
replacements_count: AtomicUsize::new(0),
shared_state_store,
shared_notifications,
shared_summaries,
shared_client_counts,
shared_contract_modules,
shared_delegate_modules,
shared_delegate_contexts,
shared_backend_engine,
shared_recovery_guard,
delegate_notification_tx,
delegate_notification_rx: Some(delegate_notification_rx),
in_flight_contracts: HashMap::new(),
export_semaphore: Arc::new(Semaphore::new(effective_export_permits(pool_size_usize))),
_inactive_user_sweep: inactive_user_sweep,
})
}
pub fn health_status(&self) -> PoolHealthStatus {
let available = self.runtimes.iter().filter(|s| s.is_some()).count();
let checked_out = self.checked_out.load(Ordering::SeqCst);
let replacements = self.replacements_count.load(Ordering::SeqCst);
let expected_total = available + checked_out;
if expected_total != self.pool_size {
tracing::warn!(
available = available,
checked_out = checked_out,
expected = self.pool_size,
actual = expected_total,
"Pool capacity mismatch detected - possible semaphore drift"
);
}
PoolHealthStatus {
available,
checked_out,
total: self.pool_size,
replacements,
contracts_in_flight: self.in_flight_contracts.len(),
}
}
fn log_health_if_degraded(&self) {
let status = self.health_status();
if status.replacements > 0 {
tracing::info!(
available = status.available,
checked_out = status.checked_out,
total = status.total,
replacements = status.replacements,
"RuntimePool health status (degraded - {} replacements)",
status.replacements
);
}
}
fn track_contract_checkout(&mut self, key: &ContractKey) {
*self.in_flight_contracts.entry(*key).or_insert(0) += 1;
}
fn track_contract_return(&mut self, key: &ContractKey) {
if let std::collections::hash_map::Entry::Occupied(mut e) =
self.in_flight_contracts.entry(*key)
{
*e.get_mut() = e.get().saturating_sub(1);
if *e.get() == 0 {
e.remove();
}
}
}
async fn pop_executor(&mut self) -> Executor<Runtime> {
let permit = self
.available
.acquire()
.await
.expect("Semaphore should not be closed");
permit.forget();
self.checked_out.fetch_add(1, Ordering::SeqCst);
for slot in &mut self.runtimes {
if let Some(executor) = slot.take() {
return executor;
}
}
self.checked_out.fetch_sub(1, Ordering::SeqCst);
unreachable!("No executors available despite semaphore permit")
}
fn try_pop_executor(&mut self) -> Option<Executor<Runtime>> {
let permit = self.available.try_acquire().ok()?;
let executor = self
.runtimes
.iter_mut()
.find_map(|slot| slot.take())
.or_else(|| {
tracing::error!(
"try_pop_executor acquired a permit but found no executor slot \
(pool invariant violated)"
);
None
})?;
permit.forget();
self.checked_out.fetch_add(1, Ordering::SeqCst);
Some(executor)
}
fn return_executor(&mut self, executor: Executor<Runtime>) {
self.checked_out.fetch_sub(1, Ordering::SeqCst);
if let Some(empty_slot) = self.runtimes.iter_mut().find(|slot| slot.is_none()) {
*empty_slot = Some(executor);
self.available.add_permits(1);
} else {
tracing::error!(
pool_size = self.pool_size,
checked_out = self.checked_out.load(Ordering::SeqCst),
"No empty slot found when returning executor - pool may be corrupted"
);
self.available.add_permits(1);
}
}
fn is_executor_healthy(executor: &Executor<Runtime>) -> bool {
executor.runtime.is_healthy()
}
async fn create_replacement_executor(&self) -> anyhow::Result<Executor<Runtime>> {
tracing::warn!("Creating replacement executor due to previous failure");
let mut executor = Executor::from_config_with_shared_modules(
self.config.clone(),
self.shared_state_store.clone(),
Some(self.op_manager.clone()),
self.shared_contract_modules.clone(),
self.shared_delegate_modules.clone(),
self.shared_delegate_contexts.clone(),
Some(self.shared_backend_engine.clone()),
)
.await?;
executor.set_shared_notifications(
self.shared_notifications.clone(),
self.shared_summaries.clone(),
self.shared_client_counts.clone(),
);
executor.set_recovery_guard(self.shared_recovery_guard.clone());
executor.set_delegate_notification_tx(self.delegate_notification_tx.clone());
Ok(executor)
}
async fn return_checked(&mut self, executor: Executor<Runtime>, operation: &str) {
if Self::is_executor_healthy(&executor) {
self.return_executor(executor);
return;
}
let replacement_num = self.replacements_count.fetch_add(1, Ordering::SeqCst) + 1;
tracing::warn!(
operation,
replacement_number = replacement_num,
"Executor became unhealthy, creating replacement"
);
match self.create_replacement_executor().await {
Ok(new_executor) => {
self.return_executor(new_executor);
}
Err(e) => {
tracing::error!(error = %e, "Failed to create replacement executor");
self.return_executor(executor);
}
}
self.log_health_if_degraded();
}
pub fn state_store(&self) -> &StateStore<Storage> {
&self.shared_state_store
}
pub fn code_hash_from_id(&self, instance_id: &ContractInstanceId) -> Option<CodeHash> {
self.runtimes.iter().flatten().find_map(|executor| {
executor
.runtime
.contract_store
.code_hash_from_id(instance_id)
})
}
}
impl ContractExecutor for RuntimePool {
fn track_pending_reclamation(&self, key: ContractKey, expected_generation: u64) {
self.op_manager
.ring
.pending_reclamation_add(key, expected_generation);
}
fn lookup_key(&self, instance_id: &ContractInstanceId) -> Option<ContractKey> {
self.runtimes.iter().flatten().find_map(|executor| {
executor
.runtime
.contract_store
.code_hash_from_id(instance_id)
.map(|key| ContractKey::from_id_and_code(*instance_id, key))
})
}
fn op_manager_handle(&self) -> Option<Arc<crate::node::OpManager>> {
Some(self.op_manager.clone())
}
async fn fetch_contract(
&mut self,
key: ContractKey,
return_contract_code: bool,
) -> Result<(Option<WrappedState>, Option<ContractContainer>), ExecutorError> {
self.track_contract_checkout(&key);
let mut executor = self.pop_executor().await;
let result = executor.fetch_contract(key, return_contract_code).await;
self.return_checked(executor, "fetch_contract").await;
self.track_contract_return(&key);
result
}
async fn remove_contract(
&mut self,
key: &ContractKey,
expected_generation: u64,
) -> Result<(), ExecutorError> {
if self.op_manager.ring.is_hosting_contract(key) {
self.op_manager.ring.pending_reclamation_remove(key);
tracing::debug!(
contract = %key,
"Skipping eviction reclamation — contract was re-hosted \
(hosting cache contains it) since it was evicted"
);
return Ok(());
}
if self.op_manager.ring.contract_in_use(key) {
self.op_manager
.ring
.pending_reclamation_add(*key, expected_generation);
tracing::debug!(
contract = %key,
"Skipping eviction reclamation — contract is in use \
(client subscription or downstream subscriber); queued \
for retry by the periodic sweep"
);
return Ok(());
}
let current_generation = self.op_manager.ring.state_generation(key);
if current_generation != expected_generation {
if self.op_manager.ring.is_hosting_contract(key) {
self.op_manager.ring.pending_reclamation_remove(key);
} else {
self.op_manager
.ring
.pending_reclamation_add(*key, current_generation);
}
tracing::debug!(
contract = %key,
expected_generation,
current_generation,
"Skipping eviction reclamation — contract was written since eviction"
);
return Ok(());
}
self.track_contract_checkout(key);
let mut executor = self.pop_executor().await;
let result = executor.reclaim_contract_storage(key).await;
self.return_checked(executor, "remove_contract").await;
self.track_contract_return(key);
match &result {
Ok(ReclaimOutcome::Full) => {
self.op_manager.ring.forget_state_generation(key);
self.op_manager.ring.pending_reclamation_remove(key);
}
Ok(ReclaimOutcome::Partial) => {
let current_gen = self.op_manager.ring.state_generation(key);
self.op_manager
.ring
.pending_reclamation_add(*key, current_gen);
tracing::debug!(
contract = %key,
"partial reclaim — queued for retry via pending_reclamation"
);
}
Err(_) => {
let current_gen = self.op_manager.ring.state_generation(key);
self.op_manager
.ring
.pending_reclamation_add(*key, current_gen);
}
}
result.map(|_| ())
}
async fn upsert_contract_state(
&mut self,
key: ContractKey,
update: Either<WrappedState, StateDelta<'static>>,
related_contracts: RelatedContracts<'static>,
code: Option<ContractContainer>,
) -> Result<UpsertResult, ExecutorError> {
self.track_contract_checkout(&key);
let mut executor = self.pop_executor().await;
let result = executor
.upsert_contract_state(key, update, related_contracts, code)
.await;
self.return_checked(executor, "upsert_contract_state").await;
self.track_contract_return(&key);
result
}
async fn upsert_contract_state_deferrable(
&mut self,
key: ContractKey,
update: Either<WrappedState, StateDelta<'static>>,
related_contracts: RelatedContracts<'static>,
code: Option<ContractContainer>,
) -> Result<UpsertOutcome, ExecutorError> {
self.track_contract_checkout(&key);
let mut executor = self.pop_executor().await;
let result = bridged_upsert_outcome(
executor
.bridged_upsert_contract_state_inner(key, update, related_contracts, code, true)
.await,
);
self.return_checked(executor, "upsert_contract_state_deferrable")
.await;
self.track_contract_return(&key);
result
}
fn register_contract_notifier(
&mut self,
instance_id: ContractInstanceId,
cli_id: ClientId,
notification_ch: tokio::sync::mpsc::Sender<HostResult>,
summary: Option<StateSummary<'_>>,
) -> Result<(), Box<RequestError>> {
let owned_summary = summary.map(StateSummary::into_owned);
let already_registered = self
.shared_notifications
.get(&instance_id)
.and_then(|channels| {
channels
.binary_search_by_key(&&cli_id, |(p, _)| p)
.ok()
.map(|i| (i, channels[i].1.same_channel(¬ification_ch)))
});
if let Some((idx, same_channel)) = already_registered {
if !same_channel {
if let Some(mut channels) = self.shared_notifications.get_mut(&instance_id) {
channels[idx] = (cli_id, notification_ch);
}
tracing::debug!(
client = %cli_id,
contract = %instance_id,
"Updated notification channel for existing subscription"
);
}
} else {
let contract_sub_count = self
.shared_notifications
.get(&instance_id)
.map_or(0, |ch| ch.len());
if contract_sub_count >= super::MAX_SUBSCRIBERS_PER_CONTRACT {
tracing::warn!(
client = %cli_id,
contract = %instance_id,
limit = super::MAX_SUBSCRIBERS_PER_CONTRACT,
"Subscriber limit reached for contract, rejecting registration"
);
return Err(subscriber_limit_error(
instance_id,
&format!(
"subscriber limit ({}) reached for contract",
super::MAX_SUBSCRIBERS_PER_CONTRACT
),
));
}
let client_sub_count = self.shared_client_counts.get(&cli_id).map_or(0, |c| *c);
if client_sub_count >= super::MAX_SUBSCRIPTIONS_PER_CLIENT {
tracing::warn!(
client = %cli_id,
contract = %instance_id,
limit = super::MAX_SUBSCRIPTIONS_PER_CLIENT,
current = client_sub_count,
"Per-client subscription limit reached, rejecting registration"
);
return Err(subscriber_limit_error(
instance_id,
&format!(
"per-client subscription limit ({}) reached",
super::MAX_SUBSCRIPTIONS_PER_CLIENT
),
));
}
let mut channels = self.shared_notifications.entry(instance_id).or_default();
let insert_pos = channels.partition_point(|(id, _)| id < &cli_id);
channels.insert(insert_pos, (cli_id, notification_ch));
let total = channels.len();
drop(channels);
*self.shared_client_counts.entry(cli_id).or_insert(0) += 1;
tracing::debug!(
client = %cli_id,
contract = %instance_id,
total_subscribers = total,
"Registered new subscription in shared pool storage"
);
}
self.shared_summaries
.entry(instance_id)
.or_default()
.insert(cli_id, owned_summary);
Ok(())
}
async fn execute_delegate_request(
&mut self,
req: DelegateRequest<'_>,
origin_contract: Option<&ContractInstanceId>,
caller_delegate: Option<&DelegateKey>,
user_context: Option<&UserSecretContext>,
) -> Response {
let mut executor = self.pop_executor().await;
let result = executor.delegate_request(req, origin_contract, caller_delegate, user_context);
self.return_checked(executor, "execute_delegate_request")
.await;
result
}
fn try_begin_export(
&mut self,
user_context: &UserSecretContext,
token: &[u8],
) -> ExportAdmission {
let Ok(permit) = self.export_semaphore.clone().try_acquire_owned() else {
return ExportAdmission::Busy;
};
let Some(executor) = self.try_pop_executor() else {
return ExportAdmission::Busy;
};
ExportAdmission::Admitted(Box::new(ExportJob {
executor,
_permit: permit,
user_context: user_context.clone(),
token: zeroize::Zeroizing::new(token.to_vec()),
}))
}
async fn finish_export(&mut self, done: ExportDone) -> Result<Vec<u8>, ExecutorError> {
let ExportDone { executor, result } = done;
match executor {
Some(executor) => {
self.return_checked(executor, "export_user_secrets").await;
}
None => {
tracing::error!("export offload task panicked; replacing the lost pool executor");
match self.create_replacement_executor().await {
Ok(replacement) => self.return_executor(replacement),
Err(e) => {
tracing::error!(
error = %e,
"failed to replace export-panicked executor; pool capacity reduced by one"
);
self.checked_out.fetch_sub(1, Ordering::SeqCst);
}
}
}
}
result
}
fn get_subscription_info(&self) -> Vec<crate::message::SubscriptionInfo> {
self.shared_notifications
.iter()
.flat_map(|entry| {
let instance_id = *entry.key();
entry
.value()
.iter()
.map(move |(client_id, _)| crate::message::SubscriptionInfo {
instance_id,
client_id: *client_id,
last_update: None, })
.collect::<Vec<_>>()
})
.collect()
}
fn remove_client(&self, client_id: ClientId) {
let mut removed_notifications = 0usize;
let mut removed_summaries = 0usize;
self.shared_notifications.retain(|_contract, channels| {
if let Ok(i) = channels.binary_search_by_key(&&client_id, |(id, _)| id) {
channels.remove(i);
removed_notifications += 1;
}
!channels.is_empty()
});
if removed_notifications > 0 {
self.shared_client_counts.remove(&client_id);
}
self.shared_summaries.retain(|_contract, client_summaries| {
if client_summaries.remove(&client_id).is_some() {
removed_summaries += 1;
}
!client_summaries.is_empty()
});
if removed_notifications > 0 || removed_summaries > 0 {
tracing::info!(
client = %client_id,
removed_notifications,
removed_summaries,
"Cleaned up subscriptions for disconnected client"
);
}
}
async fn summarize_contract_state(
&mut self,
key: ContractKey,
) -> Result<StateSummary<'static>, ExecutorError> {
self.track_contract_checkout(&key);
let mut executor = self.pop_executor().await;
let result = executor.summarize_contract_state(key).await;
self.return_checked(executor, "summarize_contract_state")
.await;
self.track_contract_return(&key);
result
}
async fn get_contract_state_delta(
&mut self,
key: ContractKey,
their_summary: StateSummary<'static>,
) -> Result<StateDelta<'static>, ExecutorError> {
self.track_contract_checkout(&key);
let mut executor = self.pop_executor().await;
let result = executor.get_contract_state_delta(key, their_summary).await;
self.return_checked(executor, "get_contract_state_delta")
.await;
self.track_contract_return(&key);
result
}
fn take_delegate_notification_rx(&mut self) -> Option<super::DelegateNotificationReceiver> {
self.delegate_notification_rx.take()
}
}