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:
Maofeng
2026-07-29 19:37:10 +08:00
parent 044b64a9c7
commit 8d8d136510
7 changed files with 374 additions and 11 deletions
+31 -5
View File
@@ -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(()));