1use std::fmt;
47use std::sync::Arc;
48use std::sync::atomic::Ordering;
49use std::time::Duration;
50
51use surrealism_types::err::{PrefixErr, SurrealismError, SurrealismResult};
52use tokio::sync::Semaphore;
53use wasmtime::*;
54use web_time::Instant;
55
56use crate::config::{AbiVersion, SurrealismConfig};
57use crate::controller::Controller;
58use crate::epoch::{self, EngineHandle};
59use crate::exports::ExportsManifest;
60use crate::host::{InvocationContext, implement_host_functions};
61use crate::kv::BTreeMapStore;
62use crate::net_allow::{ResolvedNetAllow, resolve_allow_net};
63use crate::package::{AttachedFs, SurrealismPackage};
64use crate::store::StoreData;
65
66pub struct Runtime {
70 engine_handle: EngineHandle,
72 instance_pre: component::InstancePre<StoreData>,
73 config: Arc<SurrealismConfig>,
74 wasm_size: usize,
75 fs_dir: Option<AttachedFs>,
78 pool: parking_lot::Mutex<Vec<Controller>>,
82 controller_slots: Arc<Semaphore>,
85 exports: ExportsManifest,
87 kv_store: Arc<BTreeMapStore>,
90 max_pool_size: usize,
92 max_memory_bytes: Option<usize>,
94 module_execution_time: Option<Duration>,
97 resolved_allow_net: Arc<Vec<ResolvedNetAllow>>,
99}
100
101impl fmt::Debug for Runtime {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 let pool_size = self.pool.lock().len();
104 f.debug_struct("Runtime")
105 .field("config", &self.config)
106 .field("wasm_size", &self.wasm_size)
107 .field("fs_dir", &self.fs_dir)
108 .field("pool_size", &pool_size)
109 .field("max_pool_size", &self.max_pool_size)
110 .field("max_memory_bytes", &self.max_memory_bytes)
111 .field("module_execution_time", &self.module_execution_time)
112 .field("exported_functions", &self.exports.functions.len())
113 .finish_non_exhaustive()
114 }
115}
116
117impl Runtime {
118 pub fn new(
125 SurrealismPackage {
126 wasm,
127 config,
128 exports,
129 fs,
130 logo: _,
131 }: SurrealismPackage,
132 server_pool_size: usize,
133 server_max_memory: Option<usize>,
134 server_max_execution_time: Option<Duration>,
135 server_max_kv_entries: Option<usize>,
136 server_max_kv_value_bytes: Option<usize>,
137 ) -> SurrealismResult<Self> {
138 if config.abi != AbiVersion::CURRENT {
139 return Err(SurrealismError::UnsupportedAbi {
140 expected: AbiVersion::CURRENT.0,
141 got: config.abi.0,
142 });
143 }
144
145 let t0 = Instant::now();
146
147 let max_pool_size = config
148 .capabilities
149 .max_pool_size
150 .map(|m| m.min(server_pool_size))
151 .unwrap_or(server_pool_size);
152
153 let max_memory_bytes = match (server_max_memory, config.capabilities.max_memory_bytes) {
154 (Some(s), Some(m)) => Some(s.min(m)),
155 (s, m) => s.or(m),
156 };
157
158 let module_execution_time =
159 match (server_max_execution_time, config.capabilities.max_execution_time) {
160 (Some(s), Some(m)) => Some(s.min(m)),
161 (s, m) => s.or(m),
162 };
163
164 let max_kv_entries = match (server_max_kv_entries, config.capabilities.max_kv_entries) {
165 (Some(s), Some(m)) => Some(s.min(m)),
166 (s, m) => s.or(m),
167 };
168
169 let max_kv_value_bytes =
170 match (server_max_kv_value_bytes, config.capabilities.max_kv_value_bytes) {
171 (Some(s), Some(m)) => Some(s.min(m)),
172 (s, m) => s.or(m),
173 };
174
175 let kv_store = Arc::new(BTreeMapStore::with_limits(max_kv_entries, max_kv_value_bytes));
176
177 let config = Arc::new(config);
178 let wasm_size = wasm.len();
179 tracing::debug!(
180 wasm_size,
181 fs = fs.is_some(),
182 max_pool_size,
183 ?max_memory_bytes,
184 ?module_execution_time,
185 "Runtime::new starting"
186 );
187
188 let guarded = config.capabilities.strict_timeout;
189 let engine_handle = epoch::shared_engine(guarded);
190 tracing::debug!(
191 strict_timeout = guarded,
192 engine = if guarded {
193 "guarded"
194 } else {
195 "fast"
196 },
197 "Runtime::new: selected engine"
198 );
199 let instance_pre = Self::build(engine_handle.engine(), &wasm)?;
200 tracing::debug!(elapsed = ?t0.elapsed(), "Runtime::new build done");
201
202 let resolved_allow_net = resolve_allow_net(&config.capabilities.allow_net)
203 .prefix_err(|| "Failed to resolve allow_net entries")?;
204
205 let controller_slots = Arc::new(Semaphore::new(max_pool_size.max(1)));
206
207 Ok(Self {
208 engine_handle,
209 instance_pre,
210 config,
211 wasm_size,
212 fs_dir: fs,
213 pool: parking_lot::Mutex::new(Vec::new()),
214 controller_slots,
215 exports,
216 kv_store,
217 max_pool_size,
218 max_memory_bytes,
219 module_execution_time,
220 resolved_allow_net,
221 })
222 }
223
224 pub fn wasm_size(&self) -> usize {
226 self.wasm_size
227 }
228
229 pub fn kv_store(&self) -> &Arc<BTreeMapStore> {
232 &self.kv_store
233 }
234
235 pub fn config(&self) -> &SurrealismConfig {
237 &self.config
238 }
239
240 pub fn resolved_allow_net(&self) -> Arc<Vec<ResolvedNetAllow>> {
242 Arc::clone(&self.resolved_allow_net)
243 }
244
245 pub(crate) fn epoch_deadline_max(&self) -> u64 {
252 let epoch = self.engine_handle.epoch_counter().load(Ordering::Acquire);
253 u64::MAX.saturating_sub(epoch).saturating_sub(1)
254 }
255
256 fn build(engine: &Engine, wasm: &[u8]) -> SurrealismResult<component::InstancePre<StoreData>> {
257 let t0 = Instant::now();
258
259 let comp = component::Component::new(engine, wasm)
260 .prefix_err(|| "Failed to construct component from bytes")?;
261 tracing::debug!(elapsed = ?t0.elapsed(), "build: Component::new");
262
263 let t1 = Instant::now();
264 let mut linker: component::Linker<StoreData> = component::Linker::new(engine);
265 wasmtime_wasi::p2::add_to_linker_async(&mut linker)
266 .prefix_err(|| "failed to add WASI P2 to component linker")?;
267 implement_host_functions(&mut linker)
268 .prefix_err(|| "failed to implement host functions")?;
269 tracing::debug!(elapsed = ?t1.elapsed(), "build: linker setup");
270
271 let t2 = Instant::now();
272 let instance_pre = linker
273 .instantiate_pre(&comp)
274 .prefix_err(|| "failed to pre-instantiate component (import resolution)")?;
275 tracing::debug!(elapsed = ?t2.elapsed(), "build: instantiate_pre");
276
277 tracing::debug!(elapsed = ?t0.elapsed(), "build: total");
278 Ok(instance_pre)
279 }
280
281 #[tracing::instrument(skip_all)]
289 pub async fn acquire_controller(
290 &self,
291 context: Box<dyn InvocationContext>,
292 ) -> SurrealismResult<Controller> {
293 let permit = self.acquire_slot().await?;
294
295 let pooled = {
296 let mut pool = self.pool.lock();
297 let size = pool.len();
298 let ctrl = pool.pop();
299 tracing::debug!(
300 pool_size_before = size,
301 got_pooled = ctrl.is_some(),
302 "acquire_controller: pool.pop()"
303 );
304 ctrl
305 };
306
307 match pooled {
308 Some(mut ctrl) => {
309 tracing::debug!("acquire_controller: reusing pooled controller");
310 ctrl.attach_controller_slot(permit);
311 ctrl.reset_epoch_deadline();
312 ctrl.set_context(context);
313 Ok(ctrl)
314 }
315 None => {
316 tracing::info!("acquire_controller: creating NEW controller + init()");
317 let mut ctrl = self.create_controller(context, permit).await?;
318 ctrl.init().await?;
319 Ok(ctrl)
320 }
321 }
322 }
323
324 pub fn release_controller(&self, mut controller: Controller) {
331 controller.clear_context();
332 drop(controller.take_controller_slot());
336 let mut pool = self.pool.lock();
337 if pool.len() < self.max_pool_size {
338 tracing::debug!(
339 pool_size_after = pool.len() + 1,
340 max_pool_size = self.max_pool_size,
341 "release_controller: returned to pool"
342 );
343 pool.push(controller);
344 } else {
345 tracing::info!(
346 pool_size = pool.len(),
347 max_pool_size = self.max_pool_size,
348 "release_controller: pool full, dropping controller"
349 );
350 }
351 }
352
353 pub fn get_signature(
355 &self,
356 sub: Option<&str>,
357 ) -> SurrealismResult<&crate::exports::FunctionExport> {
358 self.exports.get_signature(sub).ok_or_else(|| {
359 let name = sub.unwrap_or("<default>");
360 SurrealismError::Other(anyhow::anyhow!(
361 "function '{name}' not found in exports manifest"
362 ))
363 })
364 }
365
366 pub fn exports(&self) -> &ExportsManifest {
368 &self.exports
369 }
370
371 #[tracing::instrument(skip_all)]
377 pub async fn new_controller(
378 &self,
379 context: Box<dyn InvocationContext>,
380 ) -> SurrealismResult<Controller> {
381 let permit = self.acquire_slot().await?;
382 self.create_controller(context, permit).await
383 }
384
385 async fn acquire_slot(&self) -> SurrealismResult<tokio::sync::OwnedSemaphorePermit> {
386 Arc::clone(&self.controller_slots).acquire_owned().await.map_err(|_| {
387 SurrealismError::Other(anyhow::anyhow!(
388 "Surrealism controller semaphore closed (runtime shutdown?)"
389 ))
390 })
391 }
392
393 #[tracing::instrument(skip_all)]
395 async fn create_controller(
396 &self,
397 context: Box<dyn InvocationContext>,
398 controller_slot: tokio::sync::OwnedSemaphorePermit,
399 ) -> SurrealismResult<Controller> {
400 let t0 = Instant::now();
401
402 let fs_root = self.fs_dir.as_ref().map(|fs| fs.path());
403 let stdout_cb = crate::wasi_context::new_stdout_callback();
404 let stderr_cb = crate::wasi_context::new_stderr_callback();
405 *stdout_cb.lock() = context.stdout_callback();
406 *stderr_cb.lock() = context.stderr_callback();
407 let (wasi_ctx, table) = crate::wasi_context::build(
408 fs_root,
409 Arc::clone(&self.resolved_allow_net),
410 Arc::clone(&stdout_cb),
411 Arc::clone(&stderr_cb),
412 )?;
413 tracing::debug!(elapsed = ?t0.elapsed(), "new_controller: wasi_context::build");
414
415 let mut limits_builder = StoreLimitsBuilder::new();
416 if let Some(max_mem) = self.max_memory_bytes {
417 limits_builder = limits_builder.memory_size(max_mem);
418 }
419 let limiter = limits_builder.build();
420
421 let store_data = StoreData {
422 wasi: wasi_ctx,
423 table,
424 config: Arc::clone(&self.config),
425 context,
426 limiter,
427 stdout_cb,
428 stderr_cb,
429 };
430 let mut store = Store::new(self.engine_handle.engine(), store_data);
431 store.limiter(|data| &mut data.limiter);
432 store.set_epoch_deadline(self.epoch_deadline_max());
433
434 let t1 = Instant::now();
435 let instance = self
436 .instance_pre
437 .instantiate_async(&mut store)
438 .await
439 .map_err(SurrealismError::Instantiation)?;
440 tracing::debug!(elapsed = ?t1.elapsed(), "new_controller: instantiate_async");
441
442 let t2 = Instant::now();
443
444 let invoke_fn = instance.get_func(&mut store, "invoke").ok_or_else(|| {
445 SurrealismError::Other(anyhow::anyhow!(
446 "component is missing required export 'invoke'. \
447 Ensure the module is built with `surreal module build`"
448 ))
449 })?;
450
451 let args_fn = instance.get_func(&mut store, "function-args");
452 let returns_fn = instance.get_func(&mut store, "function-returns");
453 let list_fn = instance.get_func(&mut store, "list-functions");
454 let writeable_fn = instance.get_func(&mut store, "function-writeable");
455 let comment_fn = instance.get_func(&mut store, "function-comment");
456 let init_fn = instance.get_func(&mut store, "init");
457
458 tracing::debug!(
459 elapsed = ?t2.elapsed(),
460 has_invoke = true,
461 has_args = args_fn.is_some(),
462 has_returns = returns_fn.is_some(),
463 has_list = list_fn.is_some(),
464 has_writeable = writeable_fn.is_some(),
465 has_comment = comment_fn.is_some(),
466 has_init = init_fn.is_some(),
467 "new_controller: export lookup"
468 );
469 tracing::info!(elapsed = ?t0.elapsed(), "new_controller: total");
470
471 Ok(Controller::new(
472 store,
473 invoke_fn,
474 args_fn,
475 returns_fn,
476 list_fn,
477 writeable_fn,
478 comment_fn,
479 init_fn,
480 self.module_execution_time,
481 Arc::clone(self.engine_handle.epoch_counter()),
482 controller_slot,
483 ))
484 }
485}