remove self managed wasm as it has been replaced with official smg wa… (#17746)
This commit is contained in:
@@ -88,6 +88,7 @@ smg-auth = "1.0.0"
|
|||||||
wfaas = "1.0.0"
|
wfaas = "1.0.0"
|
||||||
data-connector = "1.0.0"
|
data-connector = "1.0.0"
|
||||||
smg-mcp = "1.0.0"
|
smg-mcp = "1.0.0"
|
||||||
|
smg-wasm = "1.0.0"
|
||||||
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
|
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
|
||||||
rustls-pemfile = "2.2"
|
rustls-pemfile = "2.2"
|
||||||
openssl = "0.10.73"
|
openssl = "0.10.73"
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ use crate::{
|
|||||||
module::{MiddlewareAttachPoint, WasmModuleAttachPoint},
|
module::{MiddlewareAttachPoint, WasmModuleAttachPoint},
|
||||||
spec::{
|
spec::{
|
||||||
apply_modify_action_to_headers, build_wasm_headers_from_axum_headers,
|
apply_modify_action_to_headers, build_wasm_headers_from_axum_headers,
|
||||||
sgl::model_gateway::middleware_types::{
|
smg::gateway::middleware_types::{
|
||||||
Action, Request as WasmRequest, Response as WasmResponse,
|
Action, Request as WasmRequest, Response as WasmResponse,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,227 +0,0 @@
|
|||||||
# WebAssembly (WASM) Extensibility for sgl-model-gateway
|
|
||||||
|
|
||||||
This module provides WebAssembly-based extensibility for sgl-model-gateway, enabling dynamic, safe, and portable middleware execution without requiring router restarts or recompilation.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The WASM module allows you to extend sgl-model-gateway functionality by deploying WebAssembly components that can:
|
|
||||||
|
|
||||||
- **Intercept requests/responses** at various lifecycle points (OnRequest, OnResponse)
|
|
||||||
- **Modify HTTP headers and bodies** before/after processing
|
|
||||||
- **Reject requests** with custom status codes
|
|
||||||
- **Execute custom logic** in a sandboxed, isolated environment
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Components
|
|
||||||
|
|
||||||
The WASM module consists of several key components:
|
|
||||||
|
|
||||||
```
|
|
||||||
src/wasm/
|
|
||||||
├── module.rs # Data structures (metadata, types, attach points)
|
|
||||||
├── module_manager.rs # Module lifecycle management (add/remove/list)
|
|
||||||
├── runtime.rs # WASM execution engine and thread pool
|
|
||||||
├── route.rs # HTTP API endpoints for module management
|
|
||||||
├── spec.rs # WASM interface types bindings and type conversions
|
|
||||||
├── types.rs # Generic input/output types
|
|
||||||
├── errors.rs # Error definitions
|
|
||||||
├── config.rs # Runtime configuration
|
|
||||||
└── interface/ # WebAssembly Interface Types definitions
|
|
||||||
```
|
|
||||||
|
|
||||||
### Execution Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
1. HTTP Request arrives at router
|
|
||||||
↓
|
|
||||||
2. Middleware chain checks for WASM modules attached to OnRequest
|
|
||||||
↓
|
|
||||||
3. For each module:
|
|
||||||
a. Module manager retrieves pre-loaded WASM bytes
|
|
||||||
b. Runtime executes component in isolated worker thread
|
|
||||||
c. Component processes request via WASM type interface
|
|
||||||
d. Returns Action (Continue/Reject/Modify)
|
|
||||||
↓
|
|
||||||
4. If Continue: proceed to next middleware/upstream
|
|
||||||
If Reject: return error response immediately
|
|
||||||
If Modify: apply changes (headers, body, status)
|
|
||||||
↓
|
|
||||||
5. After upstream response:
|
|
||||||
- Modules attached to OnResponse process response
|
|
||||||
- Apply modifications
|
|
||||||
↓
|
|
||||||
6. Return final response to client
|
|
||||||
```
|
|
||||||
|
|
||||||
### WebAssembly Interface Types
|
|
||||||
|
|
||||||
The module uses the WebAssembly Component Model with WASM interface type for type-safe communication between host and WASM components:
|
|
||||||
|
|
||||||
- **Request Processing**: `middleware-on-request::on-request(req: Request) -> Action`
|
|
||||||
- **Response Processing**: `middleware-on-response::on-response(resp: Response) -> Action`
|
|
||||||
- **Actions**: `Continue`, `Reject(status)`, or `Modify(modify-action)`
|
|
||||||
|
|
||||||
See [`interface/`](./interface/) for the complete interface definition.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- sgl-model-gateway compiled with WASM support
|
|
||||||
- Rust toolchain (for building WASM components)
|
|
||||||
- `wasm32-wasip2` target: `rustup target add wasm32-wasip2`
|
|
||||||
- `wasm-tools`: `cargo install wasm-tools`
|
|
||||||
|
|
||||||
### Starting the Router
|
|
||||||
|
|
||||||
Enable WASM support when starting the router:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./sgl-model-gateway --enable-wasm --worker-urls=http://0.0.0.0:30000 --port=3000
|
|
||||||
```
|
|
||||||
|
|
||||||
### Deploying a WASM Module
|
|
||||||
|
|
||||||
Use the `/wasm` POST endpoint to deploy modules:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:3000/wasm \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"modules": [{
|
|
||||||
"name": "my-middleware",
|
|
||||||
"file_path": "/path/to/my-component.component.wasm",
|
|
||||||
"module_type": "Middleware",
|
|
||||||
"attach_points": [{"Middleware": "OnRequest"}]
|
|
||||||
}]
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Managing Modules
|
|
||||||
|
|
||||||
**List all modules:**
|
|
||||||
```bash
|
|
||||||
curl http://localhost:3000/wasm
|
|
||||||
```
|
|
||||||
|
|
||||||
**Remove a module:**
|
|
||||||
```bash
|
|
||||||
curl -X DELETE http://localhost:3000/wasm/{module-uuid}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Module Configuration
|
|
||||||
|
|
||||||
Each module requires:
|
|
||||||
|
|
||||||
- **name**: Unique identifier for the module
|
|
||||||
- **file_path**: Absolute path to the WASM component file
|
|
||||||
- **module_type**: Currently supports `"Middleware"`
|
|
||||||
- **attach_points**: List of attachment points, e.g., `[{"Middleware": "OnRequest"}]`
|
|
||||||
|
|
||||||
Supported attachment points:
|
|
||||||
- `{"Middleware": "OnRequest"}` - Execute before forwarding to upstream
|
|
||||||
- `{"Middleware": "OnResponse"}` - Execute after receiving upstream response
|
|
||||||
- `{"Middleware": "OnError"}` - Not yet implemented
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
See [`examples/wasm/`](../../examples/wasm/) for complete examples:
|
|
||||||
|
|
||||||
1. **[wasm-guest-auth](../../examples/wasm/wasm-guest-auth/)** - API key authentication middleware
|
|
||||||
2. **[wasm-guest-logging](../../examples/wasm/wasm-guest-logging/)** - Request tracking and status code conversion
|
|
||||||
3. **[wasm-guest-ratelimit](../../examples/wasm/wasm-guest-ratelimit/)** - Rate limiting middleware
|
|
||||||
|
|
||||||
Each example includes:
|
|
||||||
- Complete source code
|
|
||||||
- Build instructions
|
|
||||||
- Deployment examples
|
|
||||||
- Testing guidelines
|
|
||||||
|
|
||||||
## Security and Resource Management
|
|
||||||
|
|
||||||
### Sandboxing
|
|
||||||
|
|
||||||
WASM modules run in isolated environments provided by wasmtime, preventing:
|
|
||||||
- Direct system access
|
|
||||||
- Memory corruption of the host process
|
|
||||||
- Unauthorized network access
|
|
||||||
- File system access (unless explicitly granted via WASI)
|
|
||||||
|
|
||||||
### Resource Limits
|
|
||||||
|
|
||||||
Runtime configuration allows setting limits:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
WasmRuntimeConfig {
|
|
||||||
max_memory_pages: 1024, // 64MB limit
|
|
||||||
max_execution_time_ms: 1000, // 1 second timeout
|
|
||||||
max_stack_size: 1024 * 1024, // 1MB stack
|
|
||||||
thread_pool_size: 4, // Worker threads
|
|
||||||
module_cache_size: 10, // Cached modules per worker
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
|
|
||||||
- Failed module executions are logged and don't crash the router
|
|
||||||
- Invalid WASM components are rejected during load time
|
|
||||||
- Metrics track execution success/failure rates
|
|
||||||
|
|
||||||
## Metrics
|
|
||||||
|
|
||||||
The module exposes execution metrics via the `/wasm` GET endpoint:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"modules": [...],
|
|
||||||
"metrics": {
|
|
||||||
"total_executions": 1000,
|
|
||||||
"successful_executions": 995,
|
|
||||||
"failed_executions": 5,
|
|
||||||
"total_execution_time_ms": 50000,
|
|
||||||
"max_execution_time_ms": 150,
|
|
||||||
"average_execution_time_ms": 50.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
### Building WASM Components
|
|
||||||
|
|
||||||
WASM components must be built using the Component Model. For Rust:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Build as WASM module
|
|
||||||
cargo build --target wasm32-wasip2 --release
|
|
||||||
|
|
||||||
# 2. Wrap into component format
|
|
||||||
wasm-tools component new target/wasm32-wasip2/release/my_module.wasm \
|
|
||||||
-o my_module.component.wasm
|
|
||||||
```
|
|
||||||
|
|
||||||
### WASM Interface Type
|
|
||||||
|
|
||||||
Define your component using the WASM interface from `interface/spec.*`:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
wit_bindgen::generate!({
|
|
||||||
path: "../../../src/wasm/interface",
|
|
||||||
world: "sgl-model-gateway",
|
|
||||||
});
|
|
||||||
|
|
||||||
use exports::sgl::model_gateway::middleware_on_request::Guest as OnRequestGuest;
|
|
||||||
use sgl::model_gateway::middleware_types::{Request, Action};
|
|
||||||
|
|
||||||
struct Middleware;
|
|
||||||
|
|
||||||
impl OnRequestGuest for Middleware {
|
|
||||||
fn on_request(req: Request) -> Action {
|
|
||||||
// Your logic here
|
|
||||||
Action::Continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export!(Middleware);
|
|
||||||
```
|
|
||||||
@@ -1,353 +0,0 @@
|
|||||||
//! WASM Runtime Configuration
|
|
||||||
//!
|
|
||||||
//! Defines configuration parameters for the WASM runtime,
|
|
||||||
//! including memory limits, execution timeouts, and thread pool settings.
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct WasmRuntimeConfig {
|
|
||||||
/// Maximum memory size in pages (64KB per page)
|
|
||||||
pub max_memory_pages: u32,
|
|
||||||
/// Maximum execution time in milliseconds
|
|
||||||
pub max_execution_time_ms: u64,
|
|
||||||
/// Maximum stack size in bytes
|
|
||||||
pub max_stack_size: usize,
|
|
||||||
/// Number of worker threads in the pool
|
|
||||||
pub thread_pool_size: usize,
|
|
||||||
/// Maximum number of modules to cache per worker
|
|
||||||
pub module_cache_size: usize,
|
|
||||||
/// Maximum HTTP body size in bytes for middleware request/response processing
|
|
||||||
pub max_body_size: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for WasmRuntimeConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
let default_thread_pool_size = std::thread::available_parallelism()
|
|
||||||
.map(|n| n.get())
|
|
||||||
.unwrap_or(4)
|
|
||||||
.clamp(1, 4);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
max_memory_pages: 1024, // 64MB
|
|
||||||
max_execution_time_ms: 1000, // 1 seconds
|
|
||||||
max_stack_size: 1024 * 1024, // 1MB
|
|
||||||
thread_pool_size: default_thread_pool_size, // based on cpu count and capped
|
|
||||||
module_cache_size: 10, // Cache up to 10 modules per worker
|
|
||||||
max_body_size: 10 * 1024 * 1024, // 10MB
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WasmRuntimeConfig {
|
|
||||||
/// Validate the configuration parameters
|
|
||||||
pub fn validate(&self) -> Result<(), String> {
|
|
||||||
// Validate max_memory_pages
|
|
||||||
if self.max_memory_pages == 0 {
|
|
||||||
return Err("max_memory_pages cannot be 0".to_string());
|
|
||||||
}
|
|
||||||
if self.max_memory_pages > 65536 {
|
|
||||||
return Err("max_memory_pages cannot exceed 65536 (4GB)".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate max_execution_time_ms
|
|
||||||
if self.max_execution_time_ms == 0 {
|
|
||||||
return Err("max_execution_time_ms cannot be 0".to_string());
|
|
||||||
}
|
|
||||||
if self.max_execution_time_ms > 300000 {
|
|
||||||
return Err("max_execution_time_ms cannot exceed 300000ms (5 minutes)".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate max_stack_size
|
|
||||||
if self.max_stack_size == 0 {
|
|
||||||
return Err("max_stack_size cannot be 0".to_string());
|
|
||||||
}
|
|
||||||
if self.max_stack_size < 64 * 1024 {
|
|
||||||
return Err("max_stack_size must be at least 64KB".to_string());
|
|
||||||
}
|
|
||||||
if self.max_stack_size > 16 * 1024 * 1024 {
|
|
||||||
return Err("max_stack_size cannot exceed 16MB".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate thread_pool_size
|
|
||||||
if self.thread_pool_size == 0 {
|
|
||||||
return Err("thread_pool_size cannot be 0".to_string());
|
|
||||||
}
|
|
||||||
if self.thread_pool_size > 128 {
|
|
||||||
return Err("thread_pool_size cannot exceed 128".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate module_cache_size
|
|
||||||
if self.module_cache_size == 0 {
|
|
||||||
return Err("module_cache_size cannot be 0".to_string());
|
|
||||||
}
|
|
||||||
if self.module_cache_size > 1000 {
|
|
||||||
return Err("module_cache_size cannot exceed 1000".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate max_body_size
|
|
||||||
if self.max_body_size == 0 {
|
|
||||||
return Err("max_body_size cannot be 0".to_string());
|
|
||||||
}
|
|
||||||
if self.max_body_size > 100 * 1024 * 1024 {
|
|
||||||
return Err("max_body_size cannot exceed 100MB".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new config with validation
|
|
||||||
pub fn new(
|
|
||||||
max_memory_pages: u32,
|
|
||||||
max_execution_time_ms: u64,
|
|
||||||
max_stack_size: usize,
|
|
||||||
thread_pool_size: usize,
|
|
||||||
module_cache_size: usize,
|
|
||||||
max_body_size: usize,
|
|
||||||
) -> Result<Self, String> {
|
|
||||||
let config = Self {
|
|
||||||
max_memory_pages,
|
|
||||||
max_execution_time_ms,
|
|
||||||
max_stack_size,
|
|
||||||
thread_pool_size,
|
|
||||||
module_cache_size,
|
|
||||||
max_body_size,
|
|
||||||
};
|
|
||||||
config.validate()?;
|
|
||||||
Ok(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the total memory size in bytes
|
|
||||||
pub fn get_total_memory_bytes(&self) -> u64 {
|
|
||||||
self.max_memory_pages as u64 * 64 * 1024 // 64KB per page
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_default_config_validation() {
|
|
||||||
let config = WasmRuntimeConfig::default();
|
|
||||||
assert!(config.validate().is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_config_new_with_validation() {
|
|
||||||
let config = WasmRuntimeConfig::new(1024, 1000, 1024 * 1024, 2, 10, 10 * 1024 * 1024);
|
|
||||||
assert!(config.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_validation_max_memory_pages_zero() {
|
|
||||||
let config = WasmRuntimeConfig {
|
|
||||||
max_memory_pages: 0,
|
|
||||||
max_execution_time_ms: 1000,
|
|
||||||
max_stack_size: 1024 * 1024,
|
|
||||||
thread_pool_size: 2,
|
|
||||||
module_cache_size: 10,
|
|
||||||
max_body_size: 10 * 1024 * 1024,
|
|
||||||
};
|
|
||||||
let result = config.validate();
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result.unwrap_err().contains("max_memory_pages cannot be 0"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_validation_max_memory_pages_too_large() {
|
|
||||||
let config = WasmRuntimeConfig {
|
|
||||||
max_memory_pages: 65537, // Exceeds 4GB limit
|
|
||||||
max_execution_time_ms: 1000,
|
|
||||||
max_stack_size: 1024 * 1024,
|
|
||||||
thread_pool_size: 2,
|
|
||||||
module_cache_size: 10,
|
|
||||||
max_body_size: 10 * 1024 * 1024,
|
|
||||||
};
|
|
||||||
let result = config.validate();
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result
|
|
||||||
.unwrap_err()
|
|
||||||
.contains("max_memory_pages cannot exceed 65536"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_validation_max_execution_time_zero() {
|
|
||||||
let config = WasmRuntimeConfig {
|
|
||||||
max_memory_pages: 1024,
|
|
||||||
max_execution_time_ms: 0,
|
|
||||||
max_stack_size: 1024 * 1024,
|
|
||||||
thread_pool_size: 2,
|
|
||||||
module_cache_size: 10,
|
|
||||||
max_body_size: 10 * 1024 * 1024,
|
|
||||||
};
|
|
||||||
let result = config.validate();
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result
|
|
||||||
.unwrap_err()
|
|
||||||
.contains("max_execution_time_ms cannot be 0"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_validation_max_execution_time_too_large() {
|
|
||||||
let config = WasmRuntimeConfig {
|
|
||||||
max_memory_pages: 1024,
|
|
||||||
max_execution_time_ms: 300001, // Exceeds 5 minutes
|
|
||||||
max_stack_size: 1024 * 1024,
|
|
||||||
thread_pool_size: 2,
|
|
||||||
module_cache_size: 10,
|
|
||||||
max_body_size: 10 * 1024 * 1024,
|
|
||||||
};
|
|
||||||
let result = config.validate();
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result
|
|
||||||
.unwrap_err()
|
|
||||||
.contains("max_execution_time_ms cannot exceed 300000ms"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_validation_max_stack_size_too_small() {
|
|
||||||
let config = WasmRuntimeConfig {
|
|
||||||
max_memory_pages: 1024,
|
|
||||||
max_execution_time_ms: 1000,
|
|
||||||
max_stack_size: 32 * 1024, // Less than 64KB
|
|
||||||
thread_pool_size: 2,
|
|
||||||
module_cache_size: 10,
|
|
||||||
max_body_size: 10 * 1024 * 1024,
|
|
||||||
};
|
|
||||||
let result = config.validate();
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result
|
|
||||||
.unwrap_err()
|
|
||||||
.contains("max_stack_size must be at least 64KB"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_validation_max_stack_size_too_large() {
|
|
||||||
let config = WasmRuntimeConfig {
|
|
||||||
max_memory_pages: 1024,
|
|
||||||
max_execution_time_ms: 1000,
|
|
||||||
max_stack_size: 17 * 1024 * 1024, // Exceeds 16MB
|
|
||||||
thread_pool_size: 2,
|
|
||||||
module_cache_size: 10,
|
|
||||||
max_body_size: 10 * 1024 * 1024,
|
|
||||||
};
|
|
||||||
let result = config.validate();
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result
|
|
||||||
.unwrap_err()
|
|
||||||
.contains("max_stack_size cannot exceed 16MB"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_validation_thread_pool_size_zero() {
|
|
||||||
let config = WasmRuntimeConfig {
|
|
||||||
max_memory_pages: 1024,
|
|
||||||
max_execution_time_ms: 1000,
|
|
||||||
max_stack_size: 1024 * 1024,
|
|
||||||
thread_pool_size: 0,
|
|
||||||
module_cache_size: 10,
|
|
||||||
max_body_size: 10 * 1024 * 1024,
|
|
||||||
};
|
|
||||||
let result = config.validate();
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result.unwrap_err().contains("thread_pool_size cannot be 0"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_validation_thread_pool_size_too_large() {
|
|
||||||
let config = WasmRuntimeConfig {
|
|
||||||
max_memory_pages: 1024,
|
|
||||||
max_execution_time_ms: 1000,
|
|
||||||
max_stack_size: 1024 * 1024,
|
|
||||||
thread_pool_size: 129, // Exceeds 128
|
|
||||||
module_cache_size: 10,
|
|
||||||
max_body_size: 10 * 1024 * 1024,
|
|
||||||
};
|
|
||||||
let result = config.validate();
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result
|
|
||||||
.unwrap_err()
|
|
||||||
.contains("thread_pool_size cannot exceed 128"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_validation_module_cache_size_zero() {
|
|
||||||
let config = WasmRuntimeConfig {
|
|
||||||
max_memory_pages: 1024,
|
|
||||||
max_execution_time_ms: 1000,
|
|
||||||
max_stack_size: 1024 * 1024,
|
|
||||||
thread_pool_size: 2,
|
|
||||||
module_cache_size: 0,
|
|
||||||
max_body_size: 10 * 1024 * 1024,
|
|
||||||
};
|
|
||||||
let result = config.validate();
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result
|
|
||||||
.unwrap_err()
|
|
||||||
.contains("module_cache_size cannot be 0"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_validation_module_cache_size_too_large() {
|
|
||||||
let config = WasmRuntimeConfig {
|
|
||||||
max_memory_pages: 1024,
|
|
||||||
max_execution_time_ms: 1000,
|
|
||||||
max_stack_size: 1024 * 1024,
|
|
||||||
thread_pool_size: 2,
|
|
||||||
module_cache_size: 1001, // Exceeds 1000
|
|
||||||
max_body_size: 10 * 1024 * 1024,
|
|
||||||
};
|
|
||||||
let result = config.validate();
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result
|
|
||||||
.unwrap_err()
|
|
||||||
.contains("module_cache_size cannot exceed 1000"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_total_memory_bytes() {
|
|
||||||
let config = WasmRuntimeConfig {
|
|
||||||
max_memory_pages: 1024,
|
|
||||||
max_execution_time_ms: 1000,
|
|
||||||
max_stack_size: 1024 * 1024,
|
|
||||||
thread_pool_size: 2,
|
|
||||||
module_cache_size: 10,
|
|
||||||
max_body_size: 10 * 1024 * 1024,
|
|
||||||
};
|
|
||||||
// 1024 pages * 64KB = 64MB
|
|
||||||
assert_eq!(config.get_total_memory_bytes(), 64 * 1024 * 1024);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_validation_max_body_size_zero() {
|
|
||||||
let config = WasmRuntimeConfig {
|
|
||||||
max_memory_pages: 1024,
|
|
||||||
max_execution_time_ms: 1000,
|
|
||||||
max_stack_size: 1024 * 1024,
|
|
||||||
thread_pool_size: 2,
|
|
||||||
module_cache_size: 10,
|
|
||||||
max_body_size: 0,
|
|
||||||
};
|
|
||||||
let result = config.validate();
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result.unwrap_err().contains("max_body_size cannot be 0"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_validation_max_body_size_too_large() {
|
|
||||||
let config = WasmRuntimeConfig {
|
|
||||||
max_memory_pages: 1024,
|
|
||||||
max_execution_time_ms: 1000,
|
|
||||||
max_stack_size: 1024 * 1024,
|
|
||||||
thread_pool_size: 2,
|
|
||||||
module_cache_size: 10,
|
|
||||||
max_body_size: 101 * 1024 * 1024, // Exceeds 100MB
|
|
||||||
};
|
|
||||||
let result = config.validate();
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result
|
|
||||||
.unwrap_err()
|
|
||||||
.contains("max_body_size cannot exceed 100MB"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
//! WASM Error Types
|
|
||||||
//!
|
|
||||||
//! Defines comprehensive error types for the WASM subsystem,
|
|
||||||
//! including module, manager, and runtime errors.
|
|
||||||
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, WasmError>;
|
|
||||||
|
|
||||||
/// SHA256 hash wrapper for display purposes
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct Sha256Hash(pub [u8; 32]);
|
|
||||||
|
|
||||||
impl fmt::Display for Sha256Hash {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
let hex_string: String = self.0.iter().map(|b| format!("{:02x}", b)).collect();
|
|
||||||
write!(f, "{}", hex_string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<[u8; 32]> for Sha256Hash {
|
|
||||||
fn from(hash: [u8; 32]) -> Self {
|
|
||||||
Sha256Hash(hash)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum WasmError {
|
|
||||||
#[error(transparent)]
|
|
||||||
Module(#[from] WasmModuleError),
|
|
||||||
|
|
||||||
#[error(transparent)]
|
|
||||||
Manager(#[from] WasmManagerError),
|
|
||||||
|
|
||||||
#[error(transparent)]
|
|
||||||
Runtime(#[from] WasmRuntimeError),
|
|
||||||
|
|
||||||
#[error(transparent)]
|
|
||||||
Io(#[from] std::io::Error),
|
|
||||||
|
|
||||||
#[error("{0}")]
|
|
||||||
Other(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum WasmModuleError {
|
|
||||||
#[error("invalid module descriptor: {0}")]
|
|
||||||
InvalidDescriptor(String),
|
|
||||||
|
|
||||||
#[error("module with same sha256 already exists: {0}")]
|
|
||||||
DuplicateSha256(Sha256Hash),
|
|
||||||
|
|
||||||
#[error("module not found: {0}")]
|
|
||||||
NotFound(uuid::Uuid),
|
|
||||||
|
|
||||||
#[error("failed to read module file: {0}")]
|
|
||||||
FileRead(String),
|
|
||||||
|
|
||||||
#[error("validation failed: {0}")]
|
|
||||||
ValidationFailed(String),
|
|
||||||
|
|
||||||
#[error("attach point missing: {0}")]
|
|
||||||
AttachPointMissing(String),
|
|
||||||
|
|
||||||
#[error("invalid function for attach point: {0}")]
|
|
||||||
AttachPointFunctionInvalid(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum WasmManagerError {
|
|
||||||
#[error("failed to acquire lock: {0}")]
|
|
||||||
LockFailed(String),
|
|
||||||
|
|
||||||
#[error("module add failed: {0}")]
|
|
||||||
ModuleAddFailed(String),
|
|
||||||
|
|
||||||
#[error("module remove failed: {0}")]
|
|
||||||
ModuleRemoveFailed(String),
|
|
||||||
|
|
||||||
#[error("runtime unavailable")]
|
|
||||||
RuntimeUnavailable,
|
|
||||||
|
|
||||||
#[error("execution failed: {0}")]
|
|
||||||
ExecutionFailed(String),
|
|
||||||
|
|
||||||
#[error("module {0} not found")]
|
|
||||||
ModuleNotFound(uuid::Uuid),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum WasmRuntimeError {
|
|
||||||
#[error("failed to create engine: {0}")]
|
|
||||||
EngineCreateFailed(String),
|
|
||||||
|
|
||||||
#[error("failed to compile module: {0}")]
|
|
||||||
CompileFailed(String),
|
|
||||||
|
|
||||||
#[error("failed to create instance: {0}")]
|
|
||||||
InstanceCreateFailed(String),
|
|
||||||
|
|
||||||
#[error("function not found: {0}")]
|
|
||||||
FunctionNotFound(String),
|
|
||||||
|
|
||||||
#[error("execution timeout after {0}ms")]
|
|
||||||
Timeout(u64),
|
|
||||||
|
|
||||||
#[error("execution failed: {0}")]
|
|
||||||
CallFailed(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<wasmtime::Error> for WasmError {
|
|
||||||
fn from(value: wasmtime::Error) -> Self {
|
|
||||||
WasmError::Runtime(WasmRuntimeError::CallFailed(value.to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package sgl:model-gateway;
|
|
||||||
|
|
||||||
interface middleware-types {
|
|
||||||
record header { name: string, value: string }
|
|
||||||
|
|
||||||
// onRequest
|
|
||||||
record request {
|
|
||||||
method: string,
|
|
||||||
path: string,
|
|
||||||
query: string,
|
|
||||||
headers: list<header>,
|
|
||||||
body: list<u8>,
|
|
||||||
request-id: string,
|
|
||||||
now-epoch-ms: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
// onResponse
|
|
||||||
record response {
|
|
||||||
status: u16,
|
|
||||||
headers: list<header>,
|
|
||||||
body: list<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// modify action
|
|
||||||
record modify-action {
|
|
||||||
status: option<u16>,
|
|
||||||
headers-set: list<header>,
|
|
||||||
headers-add: list<header>,
|
|
||||||
headers-remove: list<string>,
|
|
||||||
body-replace: option<list<u8>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// return actions
|
|
||||||
variant action {
|
|
||||||
continue,
|
|
||||||
reject(u16), // status code
|
|
||||||
modify(modify-action),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface middleware-on-request {
|
|
||||||
use middleware-types.{request, action};
|
|
||||||
on-request: func(req: request) -> action;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface middleware-on-response {
|
|
||||||
use middleware-types.{response, action};
|
|
||||||
on-response: func(resp: response) -> action;
|
|
||||||
}
|
|
||||||
|
|
||||||
world sgl-model-gateway {
|
|
||||||
export middleware-on-request;
|
|
||||||
export middleware-on-response;
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
//! WebAssembly (WASM) module support for sgl-model-gateway
|
|
||||||
//!
|
|
||||||
//! This module provides WASM component execution capabilities using the WebAssembly Component Model.
|
|
||||||
//! It supports middleware execution at various attach points (OnRequest, OnResponse) with async support.
|
|
||||||
|
|
||||||
pub mod config;
|
|
||||||
pub mod errors;
|
|
||||||
pub mod module;
|
|
||||||
pub mod module_manager;
|
|
||||||
pub mod route;
|
|
||||||
pub mod runtime;
|
|
||||||
pub mod spec;
|
|
||||||
pub mod types;
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
//! WASM Module Data Structures and Types
|
|
||||||
//!
|
|
||||||
//! This module defines the core data structures for managing WebAssembly components:
|
|
||||||
//! - Module metadata (UUID, name, file path, hash, timestamps, metrics)
|
|
||||||
//! - Module types and attachment points (Middleware hooks: OnRequest, OnResponse, OnError)
|
|
||||||
//! - API request/response types for module management
|
|
||||||
//! - Execution metrics and statistics
|
|
||||||
//!
|
|
||||||
//! The module provides custom serialization for:
|
|
||||||
//! - SHA256 hashes (hex string representation)
|
|
||||||
//! - Timestamps (ISO 8601 format for JSON output)
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize, Serializer};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
/// Serialize [u8; 32] as hex string
|
|
||||||
fn serialize_sha256_hash<S>(hash: &[u8; 32], serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
let hex_string = hash
|
|
||||||
.iter()
|
|
||||||
.map(|b| format!("{:02x}", b))
|
|
||||||
.collect::<String>();
|
|
||||||
serializer.serialize_str(&hex_string)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deserialize hex string to [u8; 32]
|
|
||||||
fn deserialize_sha256_hash<'de, D>(deserializer: D) -> Result<[u8; 32], D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
use serde::Deserialize;
|
|
||||||
let hex_string = String::deserialize(deserializer)?;
|
|
||||||
|
|
||||||
// Parse hex string to bytes
|
|
||||||
if hex_string.len() != 64 {
|
|
||||||
return Err(serde::de::Error::custom(format!(
|
|
||||||
"Invalid SHA256 hash length: expected 64 hex characters, got {}",
|
|
||||||
hex_string.len()
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut hash = [0u8; 32];
|
|
||||||
for (i, chunk) in hex_string.as_bytes().chunks(2).enumerate() {
|
|
||||||
if chunk.len() != 2 {
|
|
||||||
return Err(serde::de::Error::custom("Invalid hex string format"));
|
|
||||||
}
|
|
||||||
let byte_str = std::str::from_utf8(chunk)
|
|
||||||
.map_err(|e| serde::de::Error::custom(format!("Invalid UTF-8: {}", e)))?;
|
|
||||||
hash[i] = u8::from_str_radix(byte_str, 16)
|
|
||||||
.map_err(|e| serde::de::Error::custom(format!("Invalid hex digit: {}", e)))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize u64 timestamp (nanoseconds since epoch) as ISO 8601 string
|
|
||||||
fn serialize_timestamp<S>(timestamp: &u64, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
|
|
||||||
// Convert nanoseconds to seconds and remaining nanoseconds
|
|
||||||
let secs = (*timestamp / 1_000_000_000) as i64;
|
|
||||||
let nanos = (*timestamp % 1_000_000_000) as u32;
|
|
||||||
|
|
||||||
match DateTime::<Utc>::from_timestamp(secs, nanos) {
|
|
||||||
Some(dt) => {
|
|
||||||
let s = dt.to_rfc3339_opts(chrono::SecondsFormat::Nanos, true);
|
|
||||||
serializer.serialize_str(&s)
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// Fallback: format manually if timestamp is out of range
|
|
||||||
let s = format!("{}", timestamp);
|
|
||||||
serializer.serialize_str(&s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deserialize ISO 8601 string to u64 timestamp (nanoseconds since epoch)
|
|
||||||
fn deserialize_timestamp<'de, D>(deserializer: D) -> Result<u64, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
let timestamp_str = String::deserialize(deserializer)?;
|
|
||||||
|
|
||||||
// Try to parse as ISO 8601 datetime (RFC 3339)
|
|
||||||
match DateTime::parse_from_rfc3339(×tamp_str) {
|
|
||||||
Ok(dt) => {
|
|
||||||
// Convert to UTC and then to nanoseconds since epoch
|
|
||||||
let dt_utc = dt.with_timezone(&Utc);
|
|
||||||
let secs = dt_utc.timestamp();
|
|
||||||
let nanos = dt_utc.timestamp_subsec_nanos();
|
|
||||||
Ok((secs as u64) * 1_000_000_000 + (nanos as u64))
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
// Fallback: try to parse as u64 directly
|
|
||||||
timestamp_str
|
|
||||||
.parse::<u64>()
|
|
||||||
.map_err(|e| serde::de::Error::custom(format!("Invalid timestamp format: {}", e)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct WasmModule {
|
|
||||||
// unique identifier for the module
|
|
||||||
pub module_uuid: Uuid,
|
|
||||||
pub module_meta: WasmModuleMeta,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub enum WasmModuleAddResult {
|
|
||||||
Success(Uuid),
|
|
||||||
Error(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct WasmModuleDescriptor {
|
|
||||||
pub name: String,
|
|
||||||
pub file_path: String,
|
|
||||||
pub module_type: WasmModuleType,
|
|
||||||
pub attach_points: Vec<WasmModuleAttachPoint>,
|
|
||||||
pub add_result: Option<WasmModuleAddResult>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct WasmModuleMeta {
|
|
||||||
// module name provided by the user
|
|
||||||
pub name: String,
|
|
||||||
// path to the module file
|
|
||||||
pub file_path: String,
|
|
||||||
// sha256 hash of the module file
|
|
||||||
#[serde(
|
|
||||||
serialize_with = "serialize_sha256_hash",
|
|
||||||
deserialize_with = "deserialize_sha256_hash"
|
|
||||||
)]
|
|
||||||
pub sha256_hash: [u8; 32],
|
|
||||||
// size of the module file in bytes
|
|
||||||
pub size_bytes: u64,
|
|
||||||
// timestamp of when the module was created (nanoseconds since epoch)
|
|
||||||
#[serde(
|
|
||||||
serialize_with = "serialize_timestamp",
|
|
||||||
deserialize_with = "deserialize_timestamp"
|
|
||||||
)]
|
|
||||||
pub created_at: u64,
|
|
||||||
// timestamp of when the module was last accessed (nanoseconds since epoch)
|
|
||||||
#[serde(
|
|
||||||
serialize_with = "serialize_timestamp",
|
|
||||||
deserialize_with = "deserialize_timestamp"
|
|
||||||
)]
|
|
||||||
pub last_accessed_at: u64,
|
|
||||||
// number of times the module was accessed
|
|
||||||
pub access_count: u64,
|
|
||||||
// attach points for the module
|
|
||||||
pub attach_points: Vec<WasmModuleAttachPoint>,
|
|
||||||
// Pre-loaded WASM component bytes (loaded into memory for faster execution)
|
|
||||||
#[serde(skip)]
|
|
||||||
pub wasm_bytes: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
|
|
||||||
pub enum WasmModuleType {
|
|
||||||
Middleware,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
|
|
||||||
pub enum MiddlewareAttachPoint {
|
|
||||||
OnRequest,
|
|
||||||
OnResponse,
|
|
||||||
OnError,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
|
|
||||||
pub enum WasmModuleAttachPoint {
|
|
||||||
Middleware(MiddlewareAttachPoint),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct WasmModuleAddRequest {
|
|
||||||
pub modules: Vec<WasmModuleDescriptor>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct WasmModuleAddResponse {
|
|
||||||
pub modules: Vec<WasmModuleDescriptor>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct WasmModuleListResponse {
|
|
||||||
pub modules: Vec<WasmModule>,
|
|
||||||
pub metrics: WasmMetrics,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct WasmMetrics {
|
|
||||||
pub total_executions: u64,
|
|
||||||
pub successful_executions: u64,
|
|
||||||
pub failed_executions: u64,
|
|
||||||
pub total_execution_time_ms: u64,
|
|
||||||
pub max_execution_time_ms: u64,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub average_execution_time_ms: Option<f64>,
|
|
||||||
}
|
|
||||||
@@ -1,267 +0,0 @@
|
|||||||
//! WASM Module Manager
|
|
||||||
|
|
||||||
use std::{
|
|
||||||
collections::HashMap,
|
|
||||||
sync::{
|
|
||||||
atomic::{AtomicU64, Ordering},
|
|
||||||
Arc, RwLock,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::wasm::{
|
|
||||||
config::WasmRuntimeConfig,
|
|
||||||
errors::{Result, WasmError, WasmManagerError, WasmModuleError},
|
|
||||||
module::{WasmModule, WasmModuleAttachPoint},
|
|
||||||
runtime::WasmRuntime,
|
|
||||||
types::{WasmComponentInput, WasmComponentOutput},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct WasmModuleManager {
|
|
||||||
modules: Arc<RwLock<HashMap<Uuid, WasmModule>>>,
|
|
||||||
runtime: Arc<WasmRuntime>,
|
|
||||||
// Metrics
|
|
||||||
total_executions: AtomicU64,
|
|
||||||
successful_executions: AtomicU64,
|
|
||||||
failed_executions: AtomicU64,
|
|
||||||
total_execution_time_ms: AtomicU64,
|
|
||||||
max_execution_time_ms: AtomicU64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WasmModuleManager {
|
|
||||||
pub fn new(config: WasmRuntimeConfig) -> Result<Self> {
|
|
||||||
let runtime = Arc::new(WasmRuntime::new(config)?);
|
|
||||||
Ok(Self {
|
|
||||||
modules: Arc::new(RwLock::new(HashMap::new())),
|
|
||||||
runtime,
|
|
||||||
total_executions: AtomicU64::new(0),
|
|
||||||
successful_executions: AtomicU64::new(0),
|
|
||||||
failed_executions: AtomicU64::new(0),
|
|
||||||
total_execution_time_ms: AtomicU64::new(0),
|
|
||||||
max_execution_time_ms: AtomicU64::new(0),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_default_config() -> Result<Self> {
|
|
||||||
Self::new(WasmRuntimeConfig::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register a module (for workflow steps)
|
|
||||||
pub(crate) fn register_module_internal(&self, module: WasmModule) -> Result<()> {
|
|
||||||
let mut modules = self
|
|
||||||
.modules
|
|
||||||
.write()
|
|
||||||
.map_err(|e| WasmManagerError::LockFailed(e.to_string()))?;
|
|
||||||
modules.insert(module.module_uuid, module);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove a module (for workflow steps)
|
|
||||||
pub(crate) fn remove_module_internal(&self, module_uuid: Uuid) -> Result<()> {
|
|
||||||
let mut modules = self
|
|
||||||
.modules
|
|
||||||
.write()
|
|
||||||
.map_err(|e| WasmManagerError::LockFailed(e.to_string()))?;
|
|
||||||
if !modules.contains_key(&module_uuid) {
|
|
||||||
return Err(WasmManagerError::ModuleNotFound(module_uuid).into());
|
|
||||||
}
|
|
||||||
modules.remove(&module_uuid);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn check_duplicate_sha256_hash(&self, sha256_hash: &[u8; 32]) -> Result<()> {
|
|
||||||
let modules = self
|
|
||||||
.modules
|
|
||||||
.read()
|
|
||||||
.map_err(|e| WasmManagerError::LockFailed(e.to_string()))?;
|
|
||||||
if modules
|
|
||||||
.values()
|
|
||||||
.any(|module: &WasmModule| module.module_meta.sha256_hash == *sha256_hash)
|
|
||||||
{
|
|
||||||
return Err(WasmModuleError::DuplicateSha256((*sha256_hash).into()).into());
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_all_modules(&self) -> Result<Vec<WasmModule>> {
|
|
||||||
let modules = self
|
|
||||||
.modules
|
|
||||||
.read()
|
|
||||||
.map_err(|e| WasmManagerError::LockFailed(e.to_string()))?;
|
|
||||||
Ok(modules.values().cloned().collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_module(&self, module_uuid: Uuid) -> Result<Option<WasmModule>> {
|
|
||||||
let modules = self
|
|
||||||
.modules
|
|
||||||
.read()
|
|
||||||
.map_err(|e| WasmManagerError::LockFailed(e.to_string()))?;
|
|
||||||
Ok(modules.get(&module_uuid).cloned())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_modules(&self) -> Result<Vec<WasmModule>> {
|
|
||||||
let modules = self
|
|
||||||
.modules
|
|
||||||
.read()
|
|
||||||
.map_err(|e| WasmManagerError::LockFailed(e.to_string()))?;
|
|
||||||
Ok(modules.values().cloned().collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// get modules by attach point
|
|
||||||
pub fn get_modules_by_attach_point(
|
|
||||||
&self,
|
|
||||||
attach_point: WasmModuleAttachPoint,
|
|
||||||
) -> Result<Vec<WasmModule>> {
|
|
||||||
let modules = self
|
|
||||||
.modules
|
|
||||||
.read()
|
|
||||||
.map_err(|e| WasmManagerError::LockFailed(e.to_string()))?;
|
|
||||||
Ok(modules
|
|
||||||
.values()
|
|
||||||
.filter(|module| module.module_meta.attach_points.contains(&attach_point))
|
|
||||||
.cloned()
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_runtime(&self) -> &Arc<WasmRuntime> {
|
|
||||||
&self.runtime
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the configured maximum body size for HTTP request/response processing
|
|
||||||
pub fn get_max_body_size(&self) -> usize {
|
|
||||||
self.runtime.get_config().max_body_size
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Execute WASM module using WebAssembly component model based on attach_point
|
|
||||||
pub async fn execute_module_interface(
|
|
||||||
&self,
|
|
||||||
module_uuid: Uuid,
|
|
||||||
attach_point: WasmModuleAttachPoint,
|
|
||||||
input: WasmComponentInput,
|
|
||||||
) -> Result<WasmComponentOutput> {
|
|
||||||
let start_time = std::time::Instant::now();
|
|
||||||
|
|
||||||
// First, get the WASM bytes with a read lock (faster)
|
|
||||||
let (wasm_bytes, wasm_hash) = {
|
|
||||||
let modules = self
|
|
||||||
.modules
|
|
||||||
.read()
|
|
||||||
.map_err(|e| WasmManagerError::LockFailed(e.to_string()))?;
|
|
||||||
let module = modules
|
|
||||||
.get(&module_uuid)
|
|
||||||
.ok_or_else(|| WasmError::from(WasmManagerError::ModuleNotFound(module_uuid)))?;
|
|
||||||
|
|
||||||
// Clone the pre-loaded WASM bytes (already in memory, no file I/O)
|
|
||||||
(
|
|
||||||
module.module_meta.wasm_bytes.clone(),
|
|
||||||
module.module_meta.sha256_hash,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut modules = self
|
|
||||||
.modules
|
|
||||||
.write()
|
|
||||||
.map_err(|e| WasmManagerError::LockFailed(e.to_string()))?;
|
|
||||||
if let Some(module) = modules.get_mut(&module_uuid) {
|
|
||||||
// SystemTime::duration_since only fails if the system time is before UNIX_EPOCH,
|
|
||||||
// which should never happen in normal operation. If it does, use current time as fallback.
|
|
||||||
let now = std::time::SystemTime::now()
|
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
|
||||||
.unwrap_or_else(|_| {
|
|
||||||
// Fallback to a reasonable timestamp if system time is invalid
|
|
||||||
// This should never occur in practice, but provides a safe fallback
|
|
||||||
std::time::Duration::from_nanos(0)
|
|
||||||
})
|
|
||||||
.as_nanos() as u64;
|
|
||||||
module.module_meta.last_accessed_at = now;
|
|
||||||
module.module_meta.access_count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = self
|
|
||||||
.runtime
|
|
||||||
.execute_component_async(wasm_bytes, wasm_hash, attach_point, input)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Record metrics
|
|
||||||
let execution_time_ms = start_time.elapsed().as_millis() as u64;
|
|
||||||
self.total_executions.fetch_add(1, Ordering::Relaxed);
|
|
||||||
self.total_execution_time_ms
|
|
||||||
.fetch_add(execution_time_ms, Ordering::Relaxed);
|
|
||||||
// Update max execution time
|
|
||||||
self.max_execution_time_ms
|
|
||||||
.fetch_max(execution_time_ms, Ordering::Relaxed);
|
|
||||||
|
|
||||||
if result.is_ok() {
|
|
||||||
self.successful_executions.fetch_add(1, Ordering::Relaxed);
|
|
||||||
} else {
|
|
||||||
self.failed_executions.fetch_add(1, Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Execute WASM module using WebAssembly component model (sync version)
|
|
||||||
pub fn execute_module_interface_sync(
|
|
||||||
&self,
|
|
||||||
module_uuid: Uuid,
|
|
||||||
attach_point: WasmModuleAttachPoint,
|
|
||||||
input: WasmComponentInput,
|
|
||||||
) -> Result<WasmComponentOutput> {
|
|
||||||
let handle = tokio::runtime::Handle::current();
|
|
||||||
handle.block_on(self.execute_module_interface(module_uuid, attach_point, input))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get current metrics
|
|
||||||
pub fn get_metrics(&self) -> (u64, u64, u64, u64, u64) {
|
|
||||||
(
|
|
||||||
self.total_executions.load(Ordering::Relaxed),
|
|
||||||
self.successful_executions.load(Ordering::Relaxed),
|
|
||||||
self.failed_executions.load(Ordering::Relaxed),
|
|
||||||
self.total_execution_time_ms.load(Ordering::Relaxed),
|
|
||||||
self.max_execution_time_ms.load(Ordering::Relaxed),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Execute a WASM module for a given attach point
|
|
||||||
/// Returns the Action if successful, or None if execution failed
|
|
||||||
///
|
|
||||||
/// This is a convenience method that wraps execute_module_interface and handles
|
|
||||||
/// error logging automatically.
|
|
||||||
pub async fn execute_module_for_attach_point(
|
|
||||||
&self,
|
|
||||||
module: &WasmModule,
|
|
||||||
attach_point: WasmModuleAttachPoint,
|
|
||||||
input: WasmComponentInput,
|
|
||||||
) -> Option<crate::wasm::spec::sgl::model_gateway::middleware_types::Action> {
|
|
||||||
use tracing::error;
|
|
||||||
|
|
||||||
let action_result = self
|
|
||||||
.execute_module_interface(module.module_uuid, attach_point, input)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match action_result {
|
|
||||||
Ok(output) => match output {
|
|
||||||
WasmComponentOutput::MiddlewareAction(action) => Some(action),
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
error!(
|
|
||||||
"Failed to execute WASM module {}: {}",
|
|
||||||
module.module_meta.name, e
|
|
||||||
);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for WasmModuleManager {
|
|
||||||
fn default() -> Self {
|
|
||||||
// with_default_config() should always succeed with default configuration.
|
|
||||||
// If it fails, it indicates a critical system configuration error.
|
|
||||||
Self::with_default_config()
|
|
||||||
.expect("Failed to create WasmModuleManager with default config. This should never happen with valid default configuration.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
//! WASM HTTP API Routes
|
|
||||||
//!
|
|
||||||
//! Provides REST API endpoints for managing WASM modules:
|
|
||||||
//! - POST /wasm - Add modules
|
|
||||||
//! - DELETE /wasm/:uuid - Remove a module
|
|
||||||
//! - GET /wasm - List all modules with metrics
|
|
||||||
|
|
||||||
use std::{sync::Arc, time::Duration};
|
|
||||||
|
|
||||||
use axum::{
|
|
||||||
extract::{Json, Path, State},
|
|
||||||
http::StatusCode,
|
|
||||||
response::{IntoResponse, Response},
|
|
||||||
};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
core::{job_queue::Job, steps::WasmModuleConfigRequest},
|
|
||||||
server::AppState,
|
|
||||||
wasm::module::{
|
|
||||||
WasmMetrics, WasmModuleAddRequest, WasmModuleAddResponse, WasmModuleAddResult,
|
|
||||||
WasmModuleListResponse,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Wait for job completion by polling job status
|
|
||||||
/// Returns the job result message if successful
|
|
||||||
async fn wait_for_job_completion(
|
|
||||||
job_queue: &crate::core::job_queue::JobQueue,
|
|
||||||
status_key: &str,
|
|
||||||
timeout_duration: Duration,
|
|
||||||
) -> Result<String, String> {
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
let mut poll_interval = Duration::from_millis(100);
|
|
||||||
let max_poll_interval = Duration::from_millis(2000);
|
|
||||||
let poll_backoff = Duration::from_millis(200);
|
|
||||||
|
|
||||||
loop {
|
|
||||||
if start.elapsed() > timeout_duration {
|
|
||||||
return Err(format!("Job timeout after {}s", timeout_duration.as_secs()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(job_status) = job_queue.get_status(status_key) {
|
|
||||||
match job_status.status.as_str() {
|
|
||||||
"pending" | "processing" => {
|
|
||||||
tokio::time::sleep(poll_interval).await;
|
|
||||||
poll_interval = (poll_interval + poll_backoff).min(max_poll_interval);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
"failed" => {
|
|
||||||
let error_msg = job_status
|
|
||||||
.message
|
|
||||||
.unwrap_or_else(|| "Unknown error".to_string());
|
|
||||||
job_queue.remove_status(status_key);
|
|
||||||
return Err(error_msg);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// Should not happen, but handle gracefully
|
|
||||||
job_queue.remove_status(status_key);
|
|
||||||
return Err("Unexpected job status".to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Job completed successfully (status was removed by record_job_completion)
|
|
||||||
// We need to get the result from the job execution
|
|
||||||
// Since job queue removes status on success, we can't get the result here
|
|
||||||
// We'll need to query the wasm manager to find the module by name
|
|
||||||
// For now, return a success message and let caller extract UUID from manager
|
|
||||||
return Ok("Job completed successfully".to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn add_wasm_module(
|
|
||||||
State(state): State<Arc<AppState>>,
|
|
||||||
Json(config): Json<WasmModuleAddRequest>,
|
|
||||||
) -> Response {
|
|
||||||
let Some(_) = state.context.wasm_manager.as_ref() else {
|
|
||||||
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(job_queue) = state.context.worker_job_queue.get() else {
|
|
||||||
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut status = StatusCode::OK;
|
|
||||||
let mut modules = config.modules.clone();
|
|
||||||
|
|
||||||
for module in modules.iter_mut() {
|
|
||||||
let wasm_config = WasmModuleConfigRequest {
|
|
||||||
descriptor: module.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let job = Job::AddWasmModule {
|
|
||||||
config: Box::new(wasm_config),
|
|
||||||
};
|
|
||||||
|
|
||||||
let worker_url = job.worker_url().to_string();
|
|
||||||
|
|
||||||
// Submit job to queue
|
|
||||||
match job_queue.submit(job).await {
|
|
||||||
Ok(_) => {
|
|
||||||
// Wait for job completion (timeout: 5 minutes)
|
|
||||||
let timeout = Duration::from_secs(300);
|
|
||||||
match wait_for_job_completion(job_queue, &worker_url, timeout).await {
|
|
||||||
Ok(_) => {
|
|
||||||
// Job completed successfully, but we need to get the UUID
|
|
||||||
// Since job queue removes status on success, we need to query
|
|
||||||
// the workflow engine or wasm manager to get the UUID
|
|
||||||
// For now, let's try to get it from the wasm manager by name
|
|
||||||
if let Some(wasm_manager) = state.context.wasm_manager.as_ref() {
|
|
||||||
if let Ok(all_modules) = wasm_manager.get_modules() {
|
|
||||||
if let Some(registered_module) = all_modules
|
|
||||||
.iter()
|
|
||||||
.find(|m| m.module_meta.name == module.name)
|
|
||||||
{
|
|
||||||
module.add_result = Some(WasmModuleAddResult::Success(
|
|
||||||
registered_module.module_uuid,
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
module.add_result = Some(WasmModuleAddResult::Error(
|
|
||||||
"Module registered but UUID not found".to_string(),
|
|
||||||
));
|
|
||||||
status = StatusCode::BAD_REQUEST;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
module.add_result = Some(WasmModuleAddResult::Error(
|
|
||||||
"Failed to query registered modules".to_string(),
|
|
||||||
));
|
|
||||||
status = StatusCode::BAD_REQUEST;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
module.add_result = Some(WasmModuleAddResult::Error(
|
|
||||||
"WASM manager not available".to_string(),
|
|
||||||
));
|
|
||||||
status = StatusCode::BAD_REQUEST;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
module.add_result = Some(WasmModuleAddResult::Error(e));
|
|
||||||
status = StatusCode::BAD_REQUEST;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
module.add_result = Some(WasmModuleAddResult::Error(format!(
|
|
||||||
"Failed to submit job: {}",
|
|
||||||
e
|
|
||||||
)));
|
|
||||||
status = StatusCode::BAD_REQUEST;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = WasmModuleAddResponse { modules };
|
|
||||||
(status, Json(response)).into_response()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn remove_wasm_module(
|
|
||||||
State(state): State<Arc<AppState>>,
|
|
||||||
Path(module_uuid_str): Path<String>,
|
|
||||||
) -> Response {
|
|
||||||
let Ok(module_uuid) = Uuid::parse_str(&module_uuid_str) else {
|
|
||||||
return StatusCode::BAD_REQUEST.into_response();
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(_) = state.context.wasm_manager.as_ref() else {
|
|
||||||
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(job_queue) = state.context.worker_job_queue.get() else {
|
|
||||||
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::core::steps::WasmModuleRemovalRequest;
|
|
||||||
|
|
||||||
let removal_request = WasmModuleRemovalRequest::new(module_uuid);
|
|
||||||
|
|
||||||
let job = Job::RemoveWasmModule {
|
|
||||||
request: Box::new(removal_request),
|
|
||||||
};
|
|
||||||
|
|
||||||
let worker_url = job.worker_url().to_string();
|
|
||||||
|
|
||||||
// Submit job to queue
|
|
||||||
match job_queue.submit(job).await {
|
|
||||||
Ok(_) => {
|
|
||||||
// Wait for job completion (timeout: 1 minute)
|
|
||||||
let timeout = Duration::from_secs(60);
|
|
||||||
match wait_for_job_completion(job_queue, &worker_url, timeout).await {
|
|
||||||
Ok(_) => (StatusCode::OK, "Module removed successfully").into_response(),
|
|
||||||
Err(e) => (StatusCode::BAD_REQUEST, e).into_response(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => (
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
format!("Failed to submit job: {}", e),
|
|
||||||
)
|
|
||||||
.into_response(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn list_wasm_modules(State(state): State<Arc<AppState>>) -> Response {
|
|
||||||
let Some(wasm_manager) = state.context.wasm_manager.as_ref() else {
|
|
||||||
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
|
||||||
};
|
|
||||||
let modules = wasm_manager.get_modules();
|
|
||||||
if let Ok(modules) = modules {
|
|
||||||
let (total, success, failed, total_time_ms, max_time_ms) = wasm_manager.get_metrics();
|
|
||||||
let average_execution_time_ms = if total > 0 {
|
|
||||||
Some(total_time_ms as f64 / total as f64)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let metrics = WasmMetrics {
|
|
||||||
total_executions: total,
|
|
||||||
successful_executions: success,
|
|
||||||
failed_executions: failed,
|
|
||||||
total_execution_time_ms: total_time_ms,
|
|
||||||
max_execution_time_ms: max_time_ms,
|
|
||||||
average_execution_time_ms,
|
|
||||||
};
|
|
||||||
let response = WasmModuleListResponse { modules, metrics };
|
|
||||||
(StatusCode::OK, Json(response)).into_response()
|
|
||||||
} else {
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,615 +0,0 @@
|
|||||||
//! WASM Runtime
|
|
||||||
//!
|
|
||||||
//! Manages WASM component execution using wasmtime with async support.
|
|
||||||
//! Provides a thread pool for concurrent WASM execution and metrics tracking.
|
|
||||||
|
|
||||||
use std::{
|
|
||||||
num::NonZeroUsize,
|
|
||||||
sync::{
|
|
||||||
atomic::{AtomicU64, Ordering},
|
|
||||||
Arc,
|
|
||||||
},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use lru::LruCache;
|
|
||||||
use tokio::sync::oneshot;
|
|
||||||
use tracing::{debug, error};
|
|
||||||
use wasmtime::{
|
|
||||||
component::{Component, Linker, ResourceTable},
|
|
||||||
Config, Engine, InstanceAllocationStrategy, PoolingAllocationConfig, Store, StoreLimitsBuilder,
|
|
||||||
UpdateDeadline,
|
|
||||||
};
|
|
||||||
use wasmtime_wasi::WasiCtx;
|
|
||||||
|
|
||||||
/// Epoch increment interval in milliseconds.
|
|
||||||
/// Epochs are used for cooperative timeout enforcement in WASM execution.
|
|
||||||
/// A smaller interval gives finer-grained timeout control but slightly more overhead.
|
|
||||||
const EPOCH_INTERVAL_MS: u64 = 100;
|
|
||||||
|
|
||||||
use crate::wasm::{
|
|
||||||
config::WasmRuntimeConfig,
|
|
||||||
errors::{Result, WasmError, WasmRuntimeError},
|
|
||||||
module::{MiddlewareAttachPoint, WasmModuleAttachPoint},
|
|
||||||
spec::SglModelGateway,
|
|
||||||
types::{WasiState, WasmComponentInput, WasmComponentOutput},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct WasmRuntime {
|
|
||||||
config: WasmRuntimeConfig,
|
|
||||||
thread_pool: Arc<WasmThreadPool>,
|
|
||||||
// Metrics
|
|
||||||
total_executions: AtomicU64,
|
|
||||||
successful_executions: AtomicU64,
|
|
||||||
failed_executions: AtomicU64,
|
|
||||||
total_execution_time_ms: AtomicU64,
|
|
||||||
max_execution_time_ms: AtomicU64,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct WasmThreadPool {
|
|
||||||
sender: async_channel::Sender<WasmTask>,
|
|
||||||
receiver: async_channel::Receiver<WasmTask>,
|
|
||||||
workers: Vec<std::thread::JoinHandle<()>>,
|
|
||||||
// Metrics
|
|
||||||
total_tasks: AtomicU64,
|
|
||||||
completed_tasks: AtomicU64,
|
|
||||||
failed_tasks: AtomicU64,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum WasmTask {
|
|
||||||
ExecuteComponent {
|
|
||||||
wasm_bytes: Vec<u8>,
|
|
||||||
wasm_hash: [u8; 32],
|
|
||||||
attach_point: WasmModuleAttachPoint,
|
|
||||||
input: WasmComponentInput,
|
|
||||||
response: oneshot::Sender<Result<WasmComponentOutput>>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WasmRuntime {
|
|
||||||
pub fn new(config: WasmRuntimeConfig) -> Result<Self> {
|
|
||||||
let thread_pool = Arc::new(WasmThreadPool::new(config.clone())?);
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
config,
|
|
||||||
thread_pool,
|
|
||||||
total_executions: AtomicU64::new(0),
|
|
||||||
successful_executions: AtomicU64::new(0),
|
|
||||||
failed_executions: AtomicU64::new(0),
|
|
||||||
total_execution_time_ms: AtomicU64::new(0),
|
|
||||||
max_execution_time_ms: AtomicU64::new(0),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_default_config() -> Result<Self> {
|
|
||||||
Self::new(WasmRuntimeConfig::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_config(&self) -> &WasmRuntimeConfig {
|
|
||||||
&self.config
|
|
||||||
}
|
|
||||||
|
|
||||||
/// get available cpu count and max recommended cpu count
|
|
||||||
pub fn get_cpu_info() -> (usize, usize) {
|
|
||||||
let cpu_count = std::thread::available_parallelism()
|
|
||||||
.map(|n| n.get())
|
|
||||||
.unwrap_or(4);
|
|
||||||
let max_recommended = cpu_count.max(1);
|
|
||||||
(cpu_count, max_recommended)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// get current thread pool status
|
|
||||||
pub fn get_thread_pool_info(&self) -> (usize, usize) {
|
|
||||||
let (_cpu_count, max_recommended) = Self::get_cpu_info();
|
|
||||||
let current_workers = self.thread_pool.workers.len();
|
|
||||||
(current_workers, max_recommended)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Execute WASM component using WASM interface based on attach_point
|
|
||||||
pub async fn execute_component_async(
|
|
||||||
&self,
|
|
||||||
wasm_bytes: Vec<u8>,
|
|
||||||
wasm_hash: [u8; 32],
|
|
||||||
attach_point: WasmModuleAttachPoint,
|
|
||||||
input: WasmComponentInput,
|
|
||||||
) -> Result<WasmComponentOutput> {
|
|
||||||
let start_time = std::time::Instant::now();
|
|
||||||
let (response_tx, response_rx) = oneshot::channel();
|
|
||||||
|
|
||||||
let task = WasmTask::ExecuteComponent {
|
|
||||||
wasm_bytes,
|
|
||||||
wasm_hash,
|
|
||||||
attach_point,
|
|
||||||
input,
|
|
||||||
response: response_tx,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.thread_pool.sender.send(task).await.map_err(|e| {
|
|
||||||
WasmRuntimeError::CallFailed(format!("Failed to send task to thread pool: {}", e))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let result = response_rx.await.map_err(|e| {
|
|
||||||
WasmRuntimeError::CallFailed(format!(
|
|
||||||
"Failed to receive response from thread pool: {}",
|
|
||||||
e
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let execution_time_ms = start_time.elapsed().as_millis() as u64;
|
|
||||||
self.total_executions.fetch_add(1, Ordering::Relaxed);
|
|
||||||
self.total_execution_time_ms
|
|
||||||
.fetch_add(execution_time_ms, Ordering::Relaxed);
|
|
||||||
// Update max execution time
|
|
||||||
self.max_execution_time_ms
|
|
||||||
.fetch_max(execution_time_ms, Ordering::Relaxed);
|
|
||||||
|
|
||||||
if result.is_ok() {
|
|
||||||
self.successful_executions.fetch_add(1, Ordering::Relaxed);
|
|
||||||
} else {
|
|
||||||
self.failed_executions.fetch_add(1, Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get current metrics
|
|
||||||
pub fn get_metrics(&self) -> (u64, u64, u64, u64, u64) {
|
|
||||||
(
|
|
||||||
self.total_executions.load(Ordering::Relaxed),
|
|
||||||
self.successful_executions.load(Ordering::Relaxed),
|
|
||||||
self.failed_executions.load(Ordering::Relaxed),
|
|
||||||
self.total_execution_time_ms.load(Ordering::Relaxed),
|
|
||||||
self.max_execution_time_ms.load(Ordering::Relaxed),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Maps a wasmtime error to a WasmError, detecting epoch interruption (timeout) traps.
|
|
||||||
fn map_wasm_error(e: wasmtime::Error, timeout_ms: u64) -> WasmError {
|
|
||||||
// Use proper trap code detection instead of brittle string matching.
|
|
||||||
// Wasmtime uses Trap::Interrupt for epoch-based interruptions.
|
|
||||||
if e.downcast_ref::<wasmtime::Trap>() == Some(&wasmtime::Trap::Interrupt) {
|
|
||||||
WasmError::from(WasmRuntimeError::Timeout(timeout_ms))
|
|
||||||
} else {
|
|
||||||
WasmError::from(WasmRuntimeError::CallFailed(e.to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WasmThreadPool {
|
|
||||||
pub fn new(config: WasmRuntimeConfig) -> Result<Self> {
|
|
||||||
let (sender, receiver) = async_channel::unbounded();
|
|
||||||
|
|
||||||
let mut workers = Vec::new();
|
|
||||||
// set thread pool size based on cpu count
|
|
||||||
let max_workers = std::thread::available_parallelism()
|
|
||||||
.map(|n| n.get())
|
|
||||||
.unwrap_or(4)
|
|
||||||
.max(1);
|
|
||||||
let num_workers = config.thread_pool_size.clamp(1, max_workers);
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
target: "smg::wasm::runtime",
|
|
||||||
"Initializing WASM runtime with {} workers",
|
|
||||||
num_workers
|
|
||||||
);
|
|
||||||
|
|
||||||
for worker_id in 0..num_workers {
|
|
||||||
let receiver = receiver.clone();
|
|
||||||
let config = config.clone();
|
|
||||||
|
|
||||||
let worker = std::thread::spawn(move || {
|
|
||||||
// create independent tokio runtime for this thread
|
|
||||||
let rt = match tokio::runtime::Runtime::new() {
|
|
||||||
Ok(rt) => rt,
|
|
||||||
Err(e) => {
|
|
||||||
error!(
|
|
||||||
target: "smg::wasm::runtime",
|
|
||||||
worker_id = worker_id,
|
|
||||||
"Failed to create tokio runtime: {}",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
rt.block_on(async {
|
|
||||||
Self::worker_loop(worker_id, receiver, config).await;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
workers.push(worker);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
sender,
|
|
||||||
receiver,
|
|
||||||
workers,
|
|
||||||
total_tasks: AtomicU64::new(0),
|
|
||||||
completed_tasks: AtomicU64::new(0),
|
|
||||||
failed_tasks: AtomicU64::new(0),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get current thread pool metrics
|
|
||||||
pub fn get_metrics(&self) -> (u64, u64, u64) {
|
|
||||||
(
|
|
||||||
self.total_tasks.load(Ordering::Relaxed),
|
|
||||||
self.completed_tasks.load(Ordering::Relaxed),
|
|
||||||
self.failed_tasks.load(Ordering::Relaxed),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn worker_loop(
|
|
||||||
worker_id: usize,
|
|
||||||
receiver: async_channel::Receiver<WasmTask>,
|
|
||||||
config: WasmRuntimeConfig,
|
|
||||||
) {
|
|
||||||
debug!(
|
|
||||||
target: "smg::wasm::runtime",
|
|
||||||
worker_id = worker_id,
|
|
||||||
thread_id = ?std::thread::current().id(),
|
|
||||||
"Worker started"
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut pool_config = PoolingAllocationConfig::default();
|
|
||||||
let max_memory_bytes = (config.max_memory_pages as usize) * 65536;
|
|
||||||
|
|
||||||
// Since this thread handles tasks sequentially, we don't need a large pool per thread.
|
|
||||||
// A pool size of 20 allows for efficient reuse without hogging memory.
|
|
||||||
pool_config.total_core_instances(20);
|
|
||||||
pool_config.max_memory_size(max_memory_bytes);
|
|
||||||
pool_config.max_component_instance_size(max_memory_bytes);
|
|
||||||
pool_config.max_tables_per_component(5);
|
|
||||||
|
|
||||||
let mut wasmtime_config = Config::new();
|
|
||||||
wasmtime_config.allocation_strategy(InstanceAllocationStrategy::Pooling(pool_config));
|
|
||||||
|
|
||||||
wasmtime_config.async_stack_size(config.max_stack_size);
|
|
||||||
wasmtime_config.async_support(true);
|
|
||||||
wasmtime_config.wasm_component_model(true); // Enable component model
|
|
||||||
wasmtime_config.epoch_interruption(true); // Enable epoch-based timeout interruption
|
|
||||||
|
|
||||||
let engine = match Engine::new(&wasmtime_config) {
|
|
||||||
Ok(engine) => engine,
|
|
||||||
Err(e) => {
|
|
||||||
error!(
|
|
||||||
target: "smg::wasm::runtime",
|
|
||||||
worker_id = worker_id,
|
|
||||||
"Failed to create engine: {}",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let cache_capacity =
|
|
||||||
NonZeroUsize::new(config.module_cache_size).unwrap_or(NonZeroUsize::new(10).unwrap());
|
|
||||||
let mut component_cache: LruCache<[u8; 32], Component> = LruCache::new(cache_capacity);
|
|
||||||
|
|
||||||
// Start epoch incrementer for timeout enforcement.
|
|
||||||
// The engine's epoch counter is incremented periodically, and each Store
|
|
||||||
// can set a deadline (number of epochs). When the deadline is reached,
|
|
||||||
// WASM execution is interrupted with a trap.
|
|
||||||
let engine_for_epoch = engine.clone();
|
|
||||||
let epoch_handle = tokio::spawn(async move {
|
|
||||||
let mut interval = tokio::time::interval(Duration::from_millis(EPOCH_INTERVAL_MS));
|
|
||||||
loop {
|
|
||||||
interval.tick().await;
|
|
||||||
engine_for_epoch.increment_epoch();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
target: "smg::wasm::runtime",
|
|
||||||
worker_id = worker_id,
|
|
||||||
epoch_interval_ms = EPOCH_INTERVAL_MS,
|
|
||||||
"Epoch incrementer started for timeout enforcement"
|
|
||||||
);
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let task = match receiver.recv().await {
|
|
||||||
Ok(task) => task,
|
|
||||||
Err(_) => {
|
|
||||||
debug!(
|
|
||||||
target: "smg::wasm::runtime",
|
|
||||||
worker_id = worker_id,
|
|
||||||
"Worker shutting down"
|
|
||||||
);
|
|
||||||
epoch_handle.abort(); // Stop the epoch incrementer
|
|
||||||
break; // channel closed, exit loop
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match task {
|
|
||||||
WasmTask::ExecuteComponent {
|
|
||||||
wasm_bytes,
|
|
||||||
wasm_hash,
|
|
||||||
attach_point,
|
|
||||||
input,
|
|
||||||
response,
|
|
||||||
} => {
|
|
||||||
let result = Self::execute_component_in_worker(
|
|
||||||
&engine,
|
|
||||||
&mut component_cache, // Pass the cache
|
|
||||||
wasm_bytes,
|
|
||||||
wasm_hash,
|
|
||||||
attach_point,
|
|
||||||
input,
|
|
||||||
&config,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let _ = response.send(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute_component_in_worker(
|
|
||||||
engine: &Engine,
|
|
||||||
cache: &mut LruCache<[u8; 32], Component>, // cache argument
|
|
||||||
wasm_bytes: Vec<u8>,
|
|
||||||
wasm_hash: [u8; 32],
|
|
||||||
attach_point: WasmModuleAttachPoint,
|
|
||||||
input: WasmComponentInput,
|
|
||||||
config: &WasmRuntimeConfig,
|
|
||||||
) -> Result<WasmComponentOutput> {
|
|
||||||
// Compile component from bytes OR retrieve from cache
|
|
||||||
// Note: The WASM file must be in component format (not plain WASM module)
|
|
||||||
let component = if let Some(comp) = cache.get(&wasm_hash) {
|
|
||||||
comp.clone() // Component is just a handle (cheap clone)
|
|
||||||
} else {
|
|
||||||
// Compile new component
|
|
||||||
let comp = Component::new(engine, &wasm_bytes).map_err(|e| {
|
|
||||||
WasmError::Runtime(WasmRuntimeError::CompileFailed(format!(
|
|
||||||
"failed to parse WebAssembly component: {}. \
|
|
||||||
Hint: The WASM file must be in component format. \
|
|
||||||
If you're using wit-bindgen, use 'wasm-tools component new' to wrap the WASM module into a component.",
|
|
||||||
e
|
|
||||||
)))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
cache.push(wasm_hash, comp.clone());
|
|
||||||
comp
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut linker = Linker::<WasiState>::new(engine);
|
|
||||||
wasmtime_wasi::p2::add_to_linker_async(&mut linker)?;
|
|
||||||
let mut builder = WasiCtx::builder();
|
|
||||||
|
|
||||||
// Create memory limits from config.
|
|
||||||
// Use the config helper to get total bytes, then safely convert to usize.
|
|
||||||
let memory_limit_bytes =
|
|
||||||
usize::try_from(config.get_total_memory_bytes()).map_err(|_| {
|
|
||||||
WasmError::from(WasmRuntimeError::CallFailed(
|
|
||||||
"Configured WASM memory limit exceeds addressable space on this platform."
|
|
||||||
.to_string(),
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
let limits = StoreLimitsBuilder::new()
|
|
||||||
.memory_size(memory_limit_bytes)
|
|
||||||
.trap_on_grow_failure(true) // Trap instead of returning -1 for easier debugging
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let mut store = Store::new(
|
|
||||||
engine,
|
|
||||||
WasiState {
|
|
||||||
ctx: builder.build(),
|
|
||||||
table: ResourceTable::new(),
|
|
||||||
limits,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Apply resource limits to the store.
|
|
||||||
// This enforces max_memory_pages by preventing memory.grow beyond the limit.
|
|
||||||
store.limiter(|state| &mut state.limits);
|
|
||||||
|
|
||||||
// Set epoch deadline for timeout enforcement.
|
|
||||||
// The deadline is the number of epoch ticks before execution is interrupted.
|
|
||||||
// With EPOCH_INTERVAL_MS=100ms and max_execution_time_ms=1000ms, deadline=10 epochs.
|
|
||||||
let deadline_epochs = (config.max_execution_time_ms / EPOCH_INTERVAL_MS).max(1);
|
|
||||||
store.set_epoch_deadline(deadline_epochs);
|
|
||||||
|
|
||||||
// Configure what happens when the deadline is reached during async yields
|
|
||||||
store.epoch_deadline_callback(|_store| Ok(UpdateDeadline::Yield(1)));
|
|
||||||
|
|
||||||
let output = match attach_point {
|
|
||||||
WasmModuleAttachPoint::Middleware(MiddlewareAttachPoint::OnRequest) => {
|
|
||||||
let request = match input {
|
|
||||||
WasmComponentInput::MiddlewareRequest(req) => req,
|
|
||||||
_ => {
|
|
||||||
return Err(WasmError::from(WasmRuntimeError::CallFailed(
|
|
||||||
"Expected MiddlewareRequest input for OnRequest attach point"
|
|
||||||
.to_string(),
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Instantiate component (must use async instantiation when async support is enabled)
|
|
||||||
let bindings = SglModelGateway::instantiate_async(&mut store, &component, &linker)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
WasmError::from(WasmRuntimeError::InstanceCreateFailed(e.to_string()))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Call on-request (async call when async support is enabled)
|
|
||||||
let action_result = bindings
|
|
||||||
.sgl_model_gateway_middleware_on_request()
|
|
||||||
.call_on_request(&mut store, &request)
|
|
||||||
.await
|
|
||||||
.map_err(|e| map_wasm_error(e, config.max_execution_time_ms))?;
|
|
||||||
|
|
||||||
WasmComponentOutput::MiddlewareAction(action_result)
|
|
||||||
}
|
|
||||||
WasmModuleAttachPoint::Middleware(MiddlewareAttachPoint::OnResponse) => {
|
|
||||||
// Extract Response input
|
|
||||||
let response = match input {
|
|
||||||
WasmComponentInput::MiddlewareResponse(resp) => resp,
|
|
||||||
_ => {
|
|
||||||
return Err(WasmError::from(WasmRuntimeError::CallFailed(
|
|
||||||
"Expected MiddlewareResponse input for OnResponse attach point"
|
|
||||||
.to_string(),
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Instantiate component (must use async instantiation when async support is enabled)
|
|
||||||
let bindings = SglModelGateway::instantiate_async(&mut store, &component, &linker)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
WasmError::from(WasmRuntimeError::InstanceCreateFailed(e.to_string()))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Call on-response (async call when async support is enabled)
|
|
||||||
let action_result = bindings
|
|
||||||
.sgl_model_gateway_middleware_on_response()
|
|
||||||
.call_on_response(&mut store, &response)
|
|
||||||
.await
|
|
||||||
.map_err(|e| map_wasm_error(e, config.max_execution_time_ms))?;
|
|
||||||
|
|
||||||
WasmComponentOutput::MiddlewareAction(action_result)
|
|
||||||
}
|
|
||||||
WasmModuleAttachPoint::Middleware(MiddlewareAttachPoint::OnError) => {
|
|
||||||
return Err(WasmError::from(WasmRuntimeError::CallFailed(
|
|
||||||
"OnError attach point not yet implemented".to_string(),
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for WasmThreadPool {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
// close sender and receiver
|
|
||||||
self.sender.close();
|
|
||||||
self.receiver.close();
|
|
||||||
|
|
||||||
// wait for all workers to complete
|
|
||||||
for worker in self.workers.drain(..) {
|
|
||||||
let _ = worker.join();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use std::{num::NonZeroUsize, time::Instant};
|
|
||||||
|
|
||||||
use lru::LruCache;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::wasm::config::WasmRuntimeConfig;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_cpu_info() {
|
|
||||||
let (cpu_count, max_recommended) = WasmRuntime::get_cpu_info();
|
|
||||||
assert!(cpu_count > 0);
|
|
||||||
assert!(max_recommended > 0);
|
|
||||||
assert!(max_recommended >= cpu_count);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_config_default_values() {
|
|
||||||
let config = WasmRuntimeConfig::default();
|
|
||||||
|
|
||||||
assert_eq!(config.max_memory_pages, 1024);
|
|
||||||
assert_eq!(config.max_execution_time_ms, 1000);
|
|
||||||
assert_eq!(config.max_stack_size, 1024 * 1024);
|
|
||||||
assert!(config.thread_pool_size > 0);
|
|
||||||
assert_eq!(config.module_cache_size, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_config_clone() {
|
|
||||||
let config = WasmRuntimeConfig::default();
|
|
||||||
let cloned_config = config.clone();
|
|
||||||
|
|
||||||
assert_eq!(config.max_memory_pages, cloned_config.max_memory_pages);
|
|
||||||
assert_eq!(
|
|
||||||
config.max_execution_time_ms,
|
|
||||||
cloned_config.max_execution_time_ms
|
|
||||||
);
|
|
||||||
assert_eq!(config.max_stack_size, cloned_config.max_stack_size);
|
|
||||||
assert_eq!(config.thread_pool_size, cloned_config.thread_pool_size);
|
|
||||||
assert_eq!(config.module_cache_size, cloned_config.module_cache_size);
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn test_wasm_instantiation_performance_threshold() -> Result<()> {
|
|
||||||
// A simple WASM module forcing memory allocation
|
|
||||||
const WASM_WAT: &str = r#"
|
|
||||||
(module
|
|
||||||
(memory (export "memory") 1)
|
|
||||||
(func (export "run") (param i32 i32) (result i32)
|
|
||||||
local.get 0
|
|
||||||
local.get 1
|
|
||||||
i32.add)
|
|
||||||
)
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let iterations = 1000;
|
|
||||||
|
|
||||||
// Scenario A: Baseline (No Pool, No Cache)
|
|
||||||
let engine_standard = Engine::default();
|
|
||||||
let start_standard = Instant::now();
|
|
||||||
for _ in 0..iterations {
|
|
||||||
// Simulate compilation + instantiation overhead
|
|
||||||
let module = wasmtime::Module::new(&engine_standard, WASM_WAT).unwrap();
|
|
||||||
let mut store = Store::new(&engine_standard, ());
|
|
||||||
let instance = wasmtime::Instance::new(&mut store, &module, &[]).unwrap();
|
|
||||||
let run_func = instance
|
|
||||||
.get_typed_func::<(i32, i32), i32>(&mut store, "run")
|
|
||||||
.unwrap();
|
|
||||||
let _ = run_func.call(&mut store, (10, 20)).unwrap();
|
|
||||||
}
|
|
||||||
let duration_standard = start_standard.elapsed();
|
|
||||||
|
|
||||||
// --- Scenario B: Optimized (Pool + Cache)
|
|
||||||
let mut pool_config = PoolingAllocationConfig::default();
|
|
||||||
|
|
||||||
pool_config.total_core_instances(100);
|
|
||||||
|
|
||||||
let mut config = Config::new();
|
|
||||||
config.allocation_strategy(InstanceAllocationStrategy::Pooling(pool_config));
|
|
||||||
|
|
||||||
let engine_pooled = Engine::new(&config).unwrap();
|
|
||||||
|
|
||||||
// Setup LRU Cache
|
|
||||||
let cache_capacity = NonZeroUsize::new(100).unwrap();
|
|
||||||
let mut cache: LruCache<Vec<u8>, wasmtime::Module> = LruCache::new(cache_capacity);
|
|
||||||
|
|
||||||
// Pre-warm cache (simulating the "cached" state)
|
|
||||||
let key = WASM_WAT.as_bytes().to_vec();
|
|
||||||
let module_compiled = wasmtime::Module::new(&engine_pooled, WASM_WAT).unwrap();
|
|
||||||
cache.push(key.clone(), module_compiled);
|
|
||||||
|
|
||||||
let start_pooled = Instant::now();
|
|
||||||
for _ in 0..iterations {
|
|
||||||
let module = cache.get(&key).unwrap().clone();
|
|
||||||
let mut store = Store::new(&engine_pooled, ());
|
|
||||||
let instance = wasmtime::Instance::new(&mut store, &module, &[]).unwrap();
|
|
||||||
let run_func = instance
|
|
||||||
.get_typed_func::<(i32, i32), i32>(&mut store, "run")
|
|
||||||
.unwrap();
|
|
||||||
let _ = run_func.call(&mut store, (10, 20)).unwrap();
|
|
||||||
}
|
|
||||||
let duration_pooled = start_pooled.elapsed();
|
|
||||||
|
|
||||||
// Verify Speedup
|
|
||||||
let standard_secs = duration_standard.as_secs_f64();
|
|
||||||
let pooled_secs = duration_pooled.as_secs_f64();
|
|
||||||
|
|
||||||
if pooled_secs > 0.0 {
|
|
||||||
let speedup = standard_secs / pooled_secs;
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
speedup > 5.0,
|
|
||||||
"Optimization regression: Pooling+Caching was only {:.2}x faster",
|
|
||||||
speedup
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
//! WebAssembly Interface Bindings and Type Conversions
|
|
||||||
//!
|
|
||||||
//! Contains wasmtime component bindings generated from interface definitions,
|
|
||||||
//! and helper functions to convert between Axum HTTP types and interface types.
|
|
||||||
|
|
||||||
use axum::http::{header, HeaderMap, HeaderValue};
|
|
||||||
|
|
||||||
wasmtime::component::bindgen!({
|
|
||||||
path: "src/wasm/interface",
|
|
||||||
world: "sgl-model-gateway",
|
|
||||||
imports: { default: async | trappable },
|
|
||||||
exports: { default: async },
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Build WebAssembly headers from Axum HeaderMap
|
|
||||||
pub fn build_wasm_headers_from_axum_headers(
|
|
||||||
headers: &HeaderMap,
|
|
||||||
) -> Vec<sgl::model_gateway::middleware_types::Header> {
|
|
||||||
let mut wasm_headers = Vec::new();
|
|
||||||
for (name, value) in headers.iter() {
|
|
||||||
if let Ok(value_str) = value.to_str() {
|
|
||||||
wasm_headers.push(sgl::model_gateway::middleware_types::Header {
|
|
||||||
name: name.as_str().to_string(),
|
|
||||||
value: value_str.to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wasm_headers
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply ModifyAction header modifications to Axum HeaderMap
|
|
||||||
pub fn apply_modify_action_to_headers(
|
|
||||||
headers: &mut HeaderMap,
|
|
||||||
modify: &sgl::model_gateway::middleware_types::ModifyAction,
|
|
||||||
) {
|
|
||||||
// Apply headers_set
|
|
||||||
for header_mod in &modify.headers_set {
|
|
||||||
if let (Ok(name), Ok(value)) = (
|
|
||||||
header_mod.name.parse::<header::HeaderName>(),
|
|
||||||
header_mod.value.parse::<HeaderValue>(),
|
|
||||||
) {
|
|
||||||
headers.insert(name, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Apply headers_add
|
|
||||||
for header_mod in &modify.headers_add {
|
|
||||||
if let (Ok(name), Ok(value)) = (
|
|
||||||
header_mod.name.parse::<header::HeaderName>(),
|
|
||||||
header_mod.value.parse::<HeaderValue>(),
|
|
||||||
) {
|
|
||||||
headers.append(name, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Apply headers_remove
|
|
||||||
for name_str in &modify.headers_remove {
|
|
||||||
if let Ok(name) = name_str.parse::<header::HeaderName>() {
|
|
||||||
headers.remove(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
//! WASM Component Type System
|
|
||||||
//!
|
|
||||||
//! Provides generic input/output types for WASM component execution
|
|
||||||
//! based on attach points.
|
|
||||||
|
|
||||||
use wasmtime::{component::ResourceTable, StoreLimits};
|
|
||||||
use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView};
|
|
||||||
|
|
||||||
use crate::wasm::{
|
|
||||||
module::{MiddlewareAttachPoint, WasmModuleAttachPoint},
|
|
||||||
spec::sgl::model_gateway::middleware_types,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Generic input type for WASM component execution
|
|
||||||
///
|
|
||||||
/// This enum represents all possible input types that can be passed
|
|
||||||
/// to a WASM component, determined by the attach_point.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum WasmComponentInput {
|
|
||||||
/// Middleware OnRequest input
|
|
||||||
MiddlewareRequest(middleware_types::Request),
|
|
||||||
/// Middleware OnResponse input
|
|
||||||
MiddlewareResponse(middleware_types::Response),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generic output type from WASM component execution
|
|
||||||
///
|
|
||||||
/// This enum represents all possible output types that can be returned
|
|
||||||
/// from a WASM component, determined by the attach_point.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum WasmComponentOutput {
|
|
||||||
/// Middleware Action output
|
|
||||||
MiddlewareAction(middleware_types::Action),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WasmComponentInput {
|
|
||||||
/// Create input based on attach_point and raw data
|
|
||||||
///
|
|
||||||
/// This helper function validates that the attach_point matches
|
|
||||||
/// the expected input type.
|
|
||||||
pub fn from_attach_point(attach_point: &WasmModuleAttachPoint) -> Result<Self, String> {
|
|
||||||
match attach_point {
|
|
||||||
WasmModuleAttachPoint::Middleware(MiddlewareAttachPoint::OnRequest) => {
|
|
||||||
// OnRequest expects a Request type, but we can't construct it here
|
|
||||||
// The caller should use MiddlewareRequest variant directly
|
|
||||||
Err("OnRequest requires MiddlewareRequest input. Use WasmComponentInput::MiddlewareRequest directly.".to_string())
|
|
||||||
}
|
|
||||||
WasmModuleAttachPoint::Middleware(MiddlewareAttachPoint::OnResponse) => {
|
|
||||||
// OnResponse expects a Response type
|
|
||||||
Err("OnResponse requires MiddlewareResponse input. Use WasmComponentInput::MiddlewareResponse directly.".to_string())
|
|
||||||
}
|
|
||||||
WasmModuleAttachPoint::Middleware(MiddlewareAttachPoint::OnError) => {
|
|
||||||
Err("OnError attach point not yet implemented".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the expected attach_point for this input type
|
|
||||||
pub fn expected_attach_point(&self) -> WasmModuleAttachPoint {
|
|
||||||
match self {
|
|
||||||
WasmComponentInput::MiddlewareRequest(_) => {
|
|
||||||
WasmModuleAttachPoint::Middleware(MiddlewareAttachPoint::OnRequest)
|
|
||||||
}
|
|
||||||
WasmComponentInput::MiddlewareResponse(_) => {
|
|
||||||
WasmModuleAttachPoint::Middleware(MiddlewareAttachPoint::OnResponse)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WasmComponentOutput {
|
|
||||||
/// Get the attach_point that produced this output type
|
|
||||||
pub fn from_attach_point(attach_point: &WasmModuleAttachPoint) -> Result<Self, String> {
|
|
||||||
match attach_point {
|
|
||||||
WasmModuleAttachPoint::Middleware(MiddlewareAttachPoint::OnRequest) => {
|
|
||||||
// This would be set after execution
|
|
||||||
Err("Cannot create output before execution".to_string())
|
|
||||||
}
|
|
||||||
WasmModuleAttachPoint::Middleware(MiddlewareAttachPoint::OnResponse) => {
|
|
||||||
Err("Cannot create output before execution".to_string())
|
|
||||||
}
|
|
||||||
WasmModuleAttachPoint::Middleware(MiddlewareAttachPoint::OnError) => {
|
|
||||||
Err("OnError attach point not yet implemented".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct WasiState {
|
|
||||||
pub ctx: WasiCtx,
|
|
||||||
pub table: ResourceTable,
|
|
||||||
pub limits: StoreLimits,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WasiView for WasiState {
|
|
||||||
fn ctx(&mut self) -> WasiCtxView<'_> {
|
|
||||||
WasiCtxView {
|
|
||||||
ctx: &mut self.ctx,
|
|
||||||
table: &mut self.table,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -744,7 +744,7 @@ async fn test_wasm_module_execution() {
|
|||||||
|
|
||||||
// Execute the module
|
// Execute the module
|
||||||
use smg::wasm::{
|
use smg::wasm::{
|
||||||
spec::sgl::model_gateway::middleware_types,
|
spec::smg::gateway::middleware_types,
|
||||||
types::{WasmComponentInput, WasmComponentOutput},
|
types::{WasmComponentInput, WasmComponentOutput},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user