feat(runtime): host service-scoped KV capability
Introduce an injectable, deadline-aware KvBackend boundary and link only the exact wasmeld:kv/store@0.1.0 import requested by a Component. Namespace entries by service ID so revisions share state while services remain isolated, reject registrations without a backend, and enforce 256-byte keys and 64 KiB values. Cover capability discovery, cross-revision sharing, service isolation, missing backends, and resource limits with Component integration tests.
This commit is contained in:
@@ -30,7 +30,7 @@ use wasmtime_wasi::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
CapabilityDescriptor, RuntimeError, ServiceKey, ServiceManifest,
|
||||
CapabilityDescriptor, KvBackend, RuntimeError, ServiceKey, ServiceManifest,
|
||||
bindings::{ServiceComponent, ServiceError},
|
||||
capability::{Capability, CapabilityRegistry, HostCapabilities},
|
||||
manifest::ResourceLimits,
|
||||
@@ -61,6 +61,8 @@ pub struct RuntimeConfig {
|
||||
pub epoch_tick: Duration,
|
||||
/// Maximum native stack reservation for WebAssembly execution.
|
||||
pub max_wasm_stack: usize,
|
||||
/// Service-scoped storage used by Components importing the KV capability.
|
||||
pub kv_backend: Option<Arc<dyn KvBackend>>,
|
||||
}
|
||||
|
||||
impl Default for RuntimeConfig {
|
||||
@@ -68,6 +70,7 @@ impl Default for RuntimeConfig {
|
||||
Self {
|
||||
epoch_tick: DEFAULT_EPOCH_TICK,
|
||||
max_wasm_stack: DEFAULT_MAX_WASM_STACK,
|
||||
kv_backend: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,6 +120,7 @@ struct RuntimeInner {
|
||||
actors: Mutex<HashMap<ServiceKey, ActorHandle>>,
|
||||
lifecycle: Mutex<()>,
|
||||
ticker: Arc<EpochTicker>,
|
||||
kv_backend: Option<Arc<dyn KvBackend>>,
|
||||
}
|
||||
|
||||
struct EpochTicker {
|
||||
@@ -189,6 +193,7 @@ impl Runtime {
|
||||
actors: Mutex::new(HashMap::new()),
|
||||
lifecycle: Mutex::new(()),
|
||||
ticker,
|
||||
kv_backend: config.kv_backend,
|
||||
}),
|
||||
})
|
||||
}
|
||||
@@ -207,6 +212,11 @@ impl Runtime {
|
||||
let component = Component::new(&self.inner.engine, component_bytes.as_ref())
|
||||
.map_err(RuntimeError::ComponentCompilation)?;
|
||||
let capabilities = validate_component_imports(&component, &self.inner.engine)?;
|
||||
if capabilities.contains(&Capability::KvStore) && self.inner.kv_backend.is_none() {
|
||||
return Err(RuntimeError::CapabilityUnavailable(
|
||||
Capability::KvStore.descriptor().interface().to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut services = self
|
||||
.inner
|
||||
@@ -298,6 +308,7 @@ impl Runtime {
|
||||
key.clone(),
|
||||
service,
|
||||
init_config,
|
||||
self.inner.kv_backend.clone(),
|
||||
)?;
|
||||
|
||||
self.inner
|
||||
@@ -580,7 +591,12 @@ pub(crate) struct HostState {
|
||||
}
|
||||
|
||||
impl HostState {
|
||||
fn new(limits: &ResourceLimits, capabilities: &[Capability]) -> Self {
|
||||
fn new(
|
||||
limits: &ResourceLimits,
|
||||
capabilities: &[Capability],
|
||||
service_id: &str,
|
||||
kv_backend: Option<Arc<dyn KvBackend>>,
|
||||
) -> Self {
|
||||
let store_limits = StoreLimitsBuilder::new()
|
||||
.memory_size(limits.memory_bytes)
|
||||
.table_elements(ACTOR_TABLE_ELEMENT_LIMIT)
|
||||
@@ -600,7 +616,12 @@ impl HostState {
|
||||
store_limits,
|
||||
wasi: wasi.build(),
|
||||
wasi_table: ResourceTable::new(),
|
||||
capabilities: HostCapabilities::new(capabilities),
|
||||
capabilities: HostCapabilities::new(
|
||||
capabilities,
|
||||
service_id,
|
||||
kv_backend,
|
||||
limits.deadline(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -639,6 +660,7 @@ impl ActorWorker {
|
||||
key: ServiceKey,
|
||||
service: RegisteredService,
|
||||
init_config: Vec<u8>,
|
||||
kv_backend: Option<Arc<dyn KvBackend>>,
|
||||
) -> Result<Self, RuntimeError> {
|
||||
let limits = Arc::new(service.manifest.limits.clone());
|
||||
if init_config.len() > limits.max_input_bytes {
|
||||
@@ -648,7 +670,10 @@ impl ActorWorker {
|
||||
});
|
||||
}
|
||||
|
||||
let mut store = Store::new(&engine, HostState::new(&limits, &service.capabilities));
|
||||
let mut store = Store::new(
|
||||
&engine,
|
||||
HostState::new(&limits, &service.capabilities, key.id(), kv_backend),
|
||||
);
|
||||
store.limiter(|state| &mut state.store_limits);
|
||||
|
||||
let mut linker = Linker::new(&engine);
|
||||
@@ -797,6 +822,7 @@ fn spawn_actor(
|
||||
key: ServiceKey,
|
||||
service: RegisteredService,
|
||||
init_config: Vec<u8>,
|
||||
kv_backend: Option<Arc<dyn KvBackend>>,
|
||||
) -> Result<ActorHandle, RuntimeError> {
|
||||
let limits = Arc::new(service.manifest.limits.clone());
|
||||
let (sender, receiver) = mpsc::sync_channel(limits.mailbox_capacity);
|
||||
@@ -811,7 +837,7 @@ fn spawn_actor(
|
||||
.spawn(move || {
|
||||
let _ticker = worker_ticker;
|
||||
|
||||
match ActorWorker::create(engine, epoch_tick, key, service, init_config) {
|
||||
match ActorWorker::create(engine, epoch_tick, key, service, init_config, kv_backend) {
|
||||
Ok(mut worker) => {
|
||||
worker_status.mark_ready();
|
||||
let _ = ready_sender.send(Ok(()));
|
||||
|
||||
Reference in New Issue
Block a user