Compare commits
5 Commits
72c901c478
...
71654a8bcf
| Author | SHA1 | Date | |
|---|---|---|---|
| 71654a8bcf | |||
| a325c60421 | |||
| 8d8d136510 | |||
| 044b64a9c7 | |||
| 5edf185c90 |
Generated
+7
@@ -1995,6 +1995,13 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kv-probe-component"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen 0.41.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ members = [
|
|||||||
"components/fault",
|
"components/fault",
|
||||||
"components/spin",
|
"components/spin",
|
||||||
"components/clock-probe",
|
"components/clock-probe",
|
||||||
|
"components/kv-probe",
|
||||||
"components/wasi-clock-probe",
|
"components/wasi-clock-probe",
|
||||||
]
|
]
|
||||||
default-members = ["crates/wasmeld-runtime"]
|
default-members = ["crates/wasmeld-runtime"]
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ Runtime 内置版本化 Host Capability Registry。注册 Component 时会直接
|
|||||||
imports,只链接实际请求且版本完全匹配的 Host 能力;未知能力会被拒绝。能力不需要在
|
imports,只链接实际请求且版本完全匹配的 Host 能力;未知能力会被拒绝。能力不需要在
|
||||||
`.wasmpkg` 中重复声明,管理面会展示每个服务版本解析出的完整接口标识。
|
`.wasmpkg` 中重复声明,管理面会展示每个服务版本解析出的完整接口标识。
|
||||||
|
|
||||||
|
当前 Host 能力:
|
||||||
|
|
||||||
|
- `wasmeld:clock/monotonic-clock@0.1.0`:Actor 内单调时钟
|
||||||
|
- `wasmeld:kv/store@0.1.0`:按服务隔离、跨 Revision 共享的持久化二进制 KV
|
||||||
|
|
||||||
## 结构
|
## 结构
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -58,6 +63,16 @@ cargo run -p wasmeld-package --bin wasmeld -- \
|
|||||||
pack components/echo/Cargo.toml --locked
|
pack components/echo/Cargo.toml --locked
|
||||||
```
|
```
|
||||||
|
|
||||||
|
KV 示例组件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run -p wasmeld-package --bin wasmeld -- \
|
||||||
|
pack components/kv-probe/Cargo.toml --locked
|
||||||
|
```
|
||||||
|
|
||||||
|
`kv-probe` 接受 `set:<key>:<value>`、`get:<key>` 和 `delete:<key>`,用于验证 Host KV
|
||||||
|
能力;它不是公开 Gateway 的业务协议。
|
||||||
|
|
||||||
发布 WIT Package:
|
发布 WIT Package:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "kv-probe-component"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[package.metadata.wasmeld]
|
||||||
|
id = "kv-probe"
|
||||||
|
world = "component:kv-probe/kv-probe-component@0.1.0"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
wit-bindgen.workspace = true
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
mod bindings {
|
||||||
|
wit_bindgen::generate!({
|
||||||
|
path: "wit",
|
||||||
|
world: "kv-probe-component",
|
||||||
|
generate_all,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
use bindings::wasmeld::kv::store;
|
||||||
|
|
||||||
|
struct KvProbe;
|
||||||
|
|
||||||
|
impl bindings::Guest for KvProbe {
|
||||||
|
fn init(_config: Vec<u8>) -> Result<(), bindings::ServiceError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invoke(input: Vec<u8>) -> Result<Vec<u8>, bindings::ServiceError> {
|
||||||
|
if let Some(key) = input.strip_prefix(b"get:") {
|
||||||
|
let key = parse_key(key)?;
|
||||||
|
return store::get(&key)
|
||||||
|
.map(|value| value.unwrap_or_default())
|
||||||
|
.map_err(kv_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(command) = input.strip_prefix(b"set:") {
|
||||||
|
let separator = command
|
||||||
|
.iter()
|
||||||
|
.position(|byte| *byte == b':')
|
||||||
|
.ok_or_else(|| {
|
||||||
|
bindings::ServiceError::CallFailed(
|
||||||
|
"set command must be set:<key>:<value>".to_owned(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let key = parse_key(&command[..separator])?;
|
||||||
|
let value = command[separator + 1..].to_vec();
|
||||||
|
store::set(&key, &value).map_err(kv_error)?;
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(key) = input.strip_prefix(b"delete:") {
|
||||||
|
let key = parse_key(key)?;
|
||||||
|
store::delete(&key).map_err(kv_error)?;
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(bindings::ServiceError::CallFailed(
|
||||||
|
"expected get:<key>, set:<key>:<value>, or delete:<key>".to_owned(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_key(bytes: &[u8]) -> Result<String, bindings::ServiceError> {
|
||||||
|
core::str::from_utf8(bytes)
|
||||||
|
.map(str::to_owned)
|
||||||
|
.map_err(|_| bindings::ServiceError::CallFailed("key must be valid UTF-8".to_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kv_error(error: store::KvError) -> bindings::ServiceError {
|
||||||
|
bindings::ServiceError::CallFailed(format!("KV operation failed: {error:?}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
bindings::export!(KvProbe with_types_in bindings);
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
schema_version = 1
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
"wasmeld:kv" = "0.1.0"
|
||||||
|
"wasmeld:service" = "0.1.0"
|
||||||
|
|
||||||
|
[replace."wasmeld:kv"]
|
||||||
|
path = "../../wit/kv"
|
||||||
|
|
||||||
|
[replace."wasmeld:service"]
|
||||||
|
path = "../../wit/service"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
schema_version = 1
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasmeld:kv"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "path+../../wit/kv"
|
||||||
|
sha256 = "6dc3a84dc03c7104aa8b305518466c95e4440e1031c4bef3b59843f40f24d0fb"
|
||||||
|
replaced = true
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasmeld:service"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "path+../../wit/service"
|
||||||
|
sha256 = "d5497307bbcd1e159f7707f385b488a6f2e26362d5256d5bc1080ac603a51305"
|
||||||
|
replaced = true
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package component:kv-probe@0.1.0;
|
||||||
|
|
||||||
|
world kv-probe-component {
|
||||||
|
include wasmeld:service/service-component@0.1.0;
|
||||||
|
import wasmeld:kv/store@0.1.0;
|
||||||
|
}
|
||||||
@@ -8,11 +8,14 @@
|
|||||||
- 启动、停止和重启常驻 Actor
|
- 启动、停止和重启常驻 Actor
|
||||||
- 管理服务 ID 到活动版本的 Deployment
|
- 管理服务 ID 到活动版本的 Deployment
|
||||||
- 通过独立 Gateway 转发原始二进制调用
|
- 通过独立 Gateway 转发原始二进制调用
|
||||||
|
- 为导入 `wasmeld:kv/store@0.1.0` 的 Component 提供持久化 KV
|
||||||
- 暴露调用计数、状态和最近事件
|
- 暴露调用计数、状态和最近事件
|
||||||
|
|
||||||
Wasm 制品保存在本地文件系统。服务 manifest、调用计数和最近 256 条事件通过
|
Wasm 制品保存在本地文件系统。服务 manifest、调用计数和最近 256 条事件通过
|
||||||
[Toasty](https://github.com/tokio-rs/toasty) 写入本地 libSQL 数据库,默认路径是
|
[Toasty](https://github.com/tokio-rs/toasty) 写入本地 libSQL 数据库,默认路径是
|
||||||
`var/wasmeld/console.db`。Deployment 也存储在同一数据库中。
|
`var/wasmeld/console.db`。Deployment 也存储在同一数据库中。
|
||||||
|
Host KV 使用 `(service_id, key)` 复合主键存储在该数据库中:同一服务的 Revision
|
||||||
|
共享数据,不同服务互相隔离,Runtime 或 Console 重启不会清空 KV。
|
||||||
|
|
||||||
`wasmeld-console` 启动时会在同一进程内创建 `wasmeld-runtime`、加载 Wasmtime Engine,
|
`wasmeld-console` 启动时会在同一进程内创建 `wasmeld-runtime`、加载 Wasmtime Engine,
|
||||||
并重新注册已保存的 Component。Runtime 停止后 Console HTTP 和数据库仍然在线;
|
并重新注册已保存的 Component。Runtime 停止后 Console HTTP 和数据库仍然在线;
|
||||||
@@ -95,6 +98,12 @@ curl http://127.0.0.1:8081/v1/services/echo/invoke \
|
|||||||
`wasmeld:clock/monotonic-clock@0.1.0`。Console 不接受手工能力声明,Runtime 只会链接
|
`wasmeld:clock/monotonic-clock@0.1.0`。Console 不接受手工能力声明,Runtime 只会链接
|
||||||
注册表中存在且版本完全匹配的接口。
|
注册表中存在且版本完全匹配的接口。
|
||||||
|
|
||||||
|
## Host KV
|
||||||
|
|
||||||
|
`wasmeld:kv/store@0.1.0` 提供 `get`、`set` 和 `delete`。Key 必须是非空 UTF-8,
|
||||||
|
最大 256 B;Value 最大 64 KiB。数据库命令通过有界专用工作线程执行,Host 等待时间
|
||||||
|
不会超过服务的 `deadline_ms`。v0.1.0 不提供 TTL、遍历、事务或 CAS。
|
||||||
|
|
||||||
组件包的构建与格式说明见
|
组件包的构建与格式说明见
|
||||||
[`docs/design/wasmeld-component-package.md`](../../docs/design/wasmeld-component-package.md)。
|
[`docs/design/wasmeld-component-package.md`](../../docs/design/wasmeld-component-package.md)。
|
||||||
WIT 依赖、Registry 和本地 replace 说明见
|
WIT 依赖、Registry 和本地 replace 说明见
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
//! Bounded bridge between synchronous Component Host calls and async Toasty.
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
fmt, io,
|
||||||
|
sync::{
|
||||||
|
Arc, Mutex,
|
||||||
|
mpsc::{self, Receiver, SyncSender, TrySendError},
|
||||||
|
},
|
||||||
|
thread,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use wasmeld_runtime::{KvBackend, KvBackendError};
|
||||||
|
|
||||||
|
use crate::persistence::Persistence;
|
||||||
|
|
||||||
|
const COMMAND_CAPACITY: usize = 64;
|
||||||
|
const MAX_RESPONSE_TIMEOUT: Duration = Duration::from_millis(400);
|
||||||
|
|
||||||
|
pub(crate) struct ToastyKvBackend {
|
||||||
|
inner: Arc<BackendInner>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BackendInner {
|
||||||
|
sender: SyncSender<KvCommand>,
|
||||||
|
worker: Mutex<Option<thread::JoinHandle<()>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum KvCommand {
|
||||||
|
Get {
|
||||||
|
service_id: String,
|
||||||
|
key: String,
|
||||||
|
response: SyncSender<Result<Option<Vec<u8>>, KvBackendError>>,
|
||||||
|
},
|
||||||
|
Set {
|
||||||
|
service_id: String,
|
||||||
|
key: String,
|
||||||
|
value: Vec<u8>,
|
||||||
|
response: SyncSender<Result<(), KvBackendError>>,
|
||||||
|
},
|
||||||
|
Delete {
|
||||||
|
service_id: String,
|
||||||
|
key: String,
|
||||||
|
response: SyncSender<Result<(), KvBackendError>>,
|
||||||
|
},
|
||||||
|
Shutdown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToastyKvBackend {
|
||||||
|
pub(crate) fn start(persistence: Persistence) -> io::Result<Self> {
|
||||||
|
let (sender, receiver) = mpsc::sync_channel(COMMAND_CAPACITY);
|
||||||
|
let (ready_sender, ready_receiver) = mpsc::sync_channel(1);
|
||||||
|
let worker = thread::Builder::new()
|
||||||
|
.name("wasmeld-kv-backend".to_owned())
|
||||||
|
.spawn(move || run_worker(persistence, receiver, ready_sender))?;
|
||||||
|
|
||||||
|
match ready_receiver.recv() {
|
||||||
|
Ok(Ok(())) => Ok(Self {
|
||||||
|
inner: Arc::new(BackendInner {
|
||||||
|
sender,
|
||||||
|
worker: Mutex::new(Some(worker)),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
Ok(Err(error)) => {
|
||||||
|
let _ = worker.join();
|
||||||
|
Err(error)
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
let _ = worker.join();
|
||||||
|
Err(io::Error::other("KV backend worker stopped during startup"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request<T>(
|
||||||
|
&self,
|
||||||
|
timeout: Duration,
|
||||||
|
build: impl FnOnce(SyncSender<Result<T, KvBackendError>>) -> KvCommand,
|
||||||
|
) -> Result<T, KvBackendError> {
|
||||||
|
let (response, receiver) = mpsc::sync_channel(1);
|
||||||
|
match self.inner.sender.try_send(build(response)) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(TrySendError::Full(_)) => return Err(KvBackendError::Busy),
|
||||||
|
Err(TrySendError::Disconnected(_)) => {
|
||||||
|
return Err(KvBackendError::Operation(
|
||||||
|
"backend worker is unavailable".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
receiver
|
||||||
|
.recv_timeout(timeout.min(MAX_RESPONSE_TIMEOUT))
|
||||||
|
.map_err(|_| KvBackendError::Timeout)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for ToastyKvBackend {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Arc::clone(&self.inner),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for ToastyKvBackend {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
formatter
|
||||||
|
.debug_struct("ToastyKvBackend")
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KvBackend for ToastyKvBackend {
|
||||||
|
fn get(
|
||||||
|
&self,
|
||||||
|
service_id: &str,
|
||||||
|
key: &str,
|
||||||
|
timeout: Duration,
|
||||||
|
) -> Result<Option<Vec<u8>>, KvBackendError> {
|
||||||
|
self.request(timeout, |response| KvCommand::Get {
|
||||||
|
service_id: service_id.to_owned(),
|
||||||
|
key: key.to_owned(),
|
||||||
|
response,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set(
|
||||||
|
&self,
|
||||||
|
service_id: &str,
|
||||||
|
key: &str,
|
||||||
|
value: &[u8],
|
||||||
|
timeout: Duration,
|
||||||
|
) -> Result<(), KvBackendError> {
|
||||||
|
self.request(timeout, |response| KvCommand::Set {
|
||||||
|
service_id: service_id.to_owned(),
|
||||||
|
key: key.to_owned(),
|
||||||
|
value: value.to_vec(),
|
||||||
|
response,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete(&self, service_id: &str, key: &str, timeout: Duration) -> Result<(), KvBackendError> {
|
||||||
|
self.request(timeout, |response| KvCommand::Delete {
|
||||||
|
service_id: service_id.to_owned(),
|
||||||
|
key: key.to_owned(),
|
||||||
|
response,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for BackendInner {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = self.sender.send(KvCommand::Shutdown);
|
||||||
|
if let Ok(worker) = self.worker.get_mut()
|
||||||
|
&& let Some(worker) = worker.take()
|
||||||
|
{
|
||||||
|
let _ = worker.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_worker(
|
||||||
|
persistence: Persistence,
|
||||||
|
receiver: Receiver<KvCommand>,
|
||||||
|
ready: SyncSender<io::Result<()>>,
|
||||||
|
) {
|
||||||
|
let runtime = match tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
{
|
||||||
|
Ok(runtime) => runtime,
|
||||||
|
Err(error) => {
|
||||||
|
let _ = ready.send(Err(error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let _ = ready.send(Ok(()));
|
||||||
|
|
||||||
|
while let Ok(command) = receiver.recv() {
|
||||||
|
match command {
|
||||||
|
KvCommand::Get {
|
||||||
|
service_id,
|
||||||
|
key,
|
||||||
|
response,
|
||||||
|
} => {
|
||||||
|
let result = runtime
|
||||||
|
.block_on(persistence.kv_get(&service_id, &key))
|
||||||
|
.map_err(operation_failed);
|
||||||
|
let _ = response.send(result);
|
||||||
|
}
|
||||||
|
KvCommand::Set {
|
||||||
|
service_id,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
response,
|
||||||
|
} => {
|
||||||
|
let result = runtime
|
||||||
|
.block_on(persistence.kv_set(service_id, key, value))
|
||||||
|
.map_err(operation_failed);
|
||||||
|
let _ = response.send(result);
|
||||||
|
}
|
||||||
|
KvCommand::Delete {
|
||||||
|
service_id,
|
||||||
|
key,
|
||||||
|
response,
|
||||||
|
} => {
|
||||||
|
let result = runtime
|
||||||
|
.block_on(persistence.kv_delete(&service_id, &key))
|
||||||
|
.map_err(operation_failed);
|
||||||
|
let _ = response.send(result);
|
||||||
|
}
|
||||||
|
KvCommand::Shutdown => return,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operation_failed(error: toasty::Error) -> KvBackendError {
|
||||||
|
tracing::error!(%error, "persistent KV operation failed");
|
||||||
|
KvBackendError::Operation("persistent storage failure".to_owned())
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
//! package Registry. Actor memory remains process-local and is never presented
|
//! package Registry. Actor memory remains process-local and is never presented
|
||||||
//! as durable state.
|
//! as durable state.
|
||||||
|
|
||||||
|
mod kv_backend;
|
||||||
mod persistence;
|
mod persistence;
|
||||||
mod wit_registry;
|
mod wit_registry;
|
||||||
|
|
||||||
@@ -43,6 +44,7 @@ use wasmeld_runtime::{
|
|||||||
ServiceManifest,
|
ServiceManifest,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use kv_backend::ToastyKvBackend;
|
||||||
use persistence::{Persistence, StoredDeployment, StoredEvent, StoredService};
|
use persistence::{Persistence, StoredDeployment, StoredEvent, StoredService};
|
||||||
use wit_registry::WitRegistry;
|
use wit_registry::WitRegistry;
|
||||||
pub use wit_registry::WitRegistryError;
|
pub use wit_registry::WitRegistryError;
|
||||||
@@ -374,6 +376,9 @@ pub enum ConsoleError {
|
|||||||
#[error("database operation failed: {0}")]
|
#[error("database operation failed: {0}")]
|
||||||
Database(#[from] toasty::Error),
|
Database(#[from] toasty::Error),
|
||||||
|
|
||||||
|
#[error("failed to start persistent KV backend: {0}")]
|
||||||
|
KvBackendStart(#[source] io::Error),
|
||||||
|
|
||||||
#[error("database contains invalid console data: {0}")]
|
#[error("database contains invalid console data: {0}")]
|
||||||
InvalidDatabaseData(String),
|
InvalidDatabaseData(String),
|
||||||
|
|
||||||
@@ -410,6 +415,13 @@ impl Console {
|
|||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
let persistence = Persistence::open(&config.database_path).await?;
|
let persistence = Persistence::open(&config.database_path).await?;
|
||||||
|
let mut runtime_config = config.runtime;
|
||||||
|
if runtime_config.kv_backend.is_none() {
|
||||||
|
runtime_config.kv_backend = Some(Arc::new(
|
||||||
|
ToastyKvBackend::start(persistence.clone())
|
||||||
|
.map_err(ConsoleError::KvBackendStart)?,
|
||||||
|
));
|
||||||
|
}
|
||||||
let wit_registry =
|
let wit_registry =
|
||||||
WitRegistry::open(&config.wit_registry_dir, config.max_wit_package_bytes)?;
|
WitRegistry::open(&config.wit_registry_dir, config.max_wit_package_bytes)?;
|
||||||
let (stored_services, mut stored_events, stored_deployments) = persistence.load().await?;
|
let (stored_services, mut stored_events, stored_deployments) = persistence.load().await?;
|
||||||
@@ -424,10 +436,10 @@ impl Console {
|
|||||||
.map(EventView::try_from)
|
.map(EventView::try_from)
|
||||||
.collect::<Result<VecDeque<_>, _>>()?;
|
.collect::<Result<VecDeque<_>, _>>()?;
|
||||||
|
|
||||||
let runtime = Runtime::new(config.runtime.clone())?;
|
let runtime = Runtime::new(runtime_config.clone())?;
|
||||||
let console = Self {
|
let console = Self {
|
||||||
runtime: RwLock::new(Some(runtime)),
|
runtime: RwLock::new(Some(runtime)),
|
||||||
runtime_config: config.runtime,
|
runtime_config,
|
||||||
artifact_dir,
|
artifact_dir,
|
||||||
wit_registry,
|
wit_registry,
|
||||||
max_artifact_bytes: config.max_artifact_bytes,
|
max_artifact_bytes: config.max_artifact_bytes,
|
||||||
@@ -1759,6 +1771,7 @@ impl IntoResponse for GatewayError {
|
|||||||
| ConsoleError::Storage { .. }
|
| ConsoleError::Storage { .. }
|
||||||
| ConsoleError::ManifestSerialize(_)
|
| ConsoleError::ManifestSerialize(_)
|
||||||
| ConsoleError::Database(_)
|
| ConsoleError::Database(_)
|
||||||
|
| ConsoleError::KvBackendStart(_)
|
||||||
| ConsoleError::InvalidDatabaseData(_)
|
| ConsoleError::InvalidDatabaseData(_)
|
||||||
| ConsoleError::Runtime(_)
|
| ConsoleError::Runtime(_)
|
||||||
| ConsoleError::Join(_)
|
| ConsoleError::Join(_)
|
||||||
@@ -1849,6 +1862,7 @@ impl IntoResponse for ApiError {
|
|||||||
ConsoleError::Storage { .. }
|
ConsoleError::Storage { .. }
|
||||||
| ConsoleError::ManifestSerialize(_)
|
| ConsoleError::ManifestSerialize(_)
|
||||||
| ConsoleError::Database(_)
|
| ConsoleError::Database(_)
|
||||||
|
| ConsoleError::KvBackendStart(_)
|
||||||
| ConsoleError::InvalidDatabaseData(_)
|
| ConsoleError::InvalidDatabaseData(_)
|
||||||
| ConsoleError::Runtime(_)
|
| ConsoleError::Runtime(_)
|
||||||
| ConsoleError::WitRegistry(WitRegistryError::InvalidStorage(_))
|
| ConsoleError::WitRegistry(WitRegistryError::InvalidStorage(_))
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
//! Toasty models and transactional libSQL snapshots for Console control data.
|
//! Toasty models and transactional libSQL snapshots for Console control data.
|
||||||
//!
|
//!
|
||||||
//! Component bytes and WIT packages remain filesystem artifacts. This module
|
//! Component bytes and WIT packages remain filesystem artifacts. This module
|
||||||
//! persists only service manifests, deployments, metrics, and the bounded
|
//! persists service manifests, deployments, metrics, the bounded event
|
||||||
//! event history.
|
//! history, and service-scoped Host KV entries.
|
||||||
|
|
||||||
use std::path::Path;
|
use std::{
|
||||||
|
path::Path,
|
||||||
|
sync::Arc,
|
||||||
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
|
||||||
use toasty::Db;
|
use toasty::Db;
|
||||||
use toasty_driver_turso::Turso;
|
use toasty_driver_turso::Turso;
|
||||||
@@ -40,9 +44,19 @@ pub(crate) struct StoredDeployment {
|
|||||||
pub updated_at_ms: u64,
|
pub updated_at_ms: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, toasty::Model)]
|
||||||
|
#[key(service_id, entry_key)]
|
||||||
|
pub(crate) struct StoredKvEntry {
|
||||||
|
pub service_id: String,
|
||||||
|
pub entry_key: String,
|
||||||
|
pub value: Vec<u8>,
|
||||||
|
pub updated_at_ms: u64,
|
||||||
|
}
|
||||||
|
|
||||||
/// Serialized access to the Toasty database connection.
|
/// Serialized access to the Toasty database connection.
|
||||||
|
#[derive(Clone)]
|
||||||
pub(crate) struct Persistence {
|
pub(crate) struct Persistence {
|
||||||
db: Mutex<Db>,
|
db: Arc<Mutex<Db>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Persistence {
|
impl Persistence {
|
||||||
@@ -53,7 +67,8 @@ impl Persistence {
|
|||||||
.models(toasty::models!(
|
.models(toasty::models!(
|
||||||
StoredService,
|
StoredService,
|
||||||
StoredEvent,
|
StoredEvent,
|
||||||
StoredDeployment
|
StoredDeployment,
|
||||||
|
StoredKvEntry
|
||||||
))
|
))
|
||||||
.build(Turso::file(path))
|
.build(Turso::file(path))
|
||||||
.await?;
|
.await?;
|
||||||
@@ -74,9 +89,24 @@ impl Persistence {
|
|||||||
)
|
)
|
||||||
.exec(&mut db)
|
.exec(&mut db)
|
||||||
.await?;
|
.await?;
|
||||||
return Ok(Self { db: Mutex::new(db) });
|
toasty::sql::statement(
|
||||||
|
r#"CREATE TABLE IF NOT EXISTS "stored_kv_entries" (
|
||||||
|
"service_id" TEXT NOT NULL,
|
||||||
|
"entry_key" TEXT NOT NULL,
|
||||||
|
"value" BLOB NOT NULL,
|
||||||
|
"updated_at_ms" INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY ("service_id", "entry_key")
|
||||||
|
)"#,
|
||||||
|
)
|
||||||
|
.exec(&mut db)
|
||||||
|
.await?;
|
||||||
|
return Ok(Self {
|
||||||
|
db: Arc::new(Mutex::new(db)),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Ok(Self { db: Mutex::new(db) })
|
Ok(Self {
|
||||||
|
db: Arc::new(Mutex::new(db)),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads the complete service and deployment sets plus retained events.
|
/// Loads the complete service and deployment sets plus retained events.
|
||||||
@@ -139,4 +169,46 @@ impl Persistence {
|
|||||||
|
|
||||||
tx.commit().await
|
tx.commit().await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn kv_get(
|
||||||
|
&self,
|
||||||
|
service_id: &str,
|
||||||
|
entry_key: &str,
|
||||||
|
) -> toasty::Result<Option<Vec<u8>>> {
|
||||||
|
let mut db = self.db.lock().await;
|
||||||
|
Ok(
|
||||||
|
StoredKvEntry::filter_by_service_id_and_entry_key(service_id, entry_key)
|
||||||
|
.first()
|
||||||
|
.exec(&mut *db)
|
||||||
|
.await?
|
||||||
|
.map(|entry| entry.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn kv_set(
|
||||||
|
&self,
|
||||||
|
service_id: String,
|
||||||
|
entry_key: String,
|
||||||
|
value: Vec<u8>,
|
||||||
|
) -> toasty::Result<()> {
|
||||||
|
let mut db = self.db.lock().await;
|
||||||
|
StoredKvEntry::upsert_by_service_id_and_entry_key(service_id, entry_key)
|
||||||
|
.value(value)
|
||||||
|
.updated_at_ms(unix_time_ms())
|
||||||
|
.exec(&mut *db)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn kv_delete(&self, service_id: &str, entry_key: &str) -> toasty::Result<()> {
|
||||||
|
let mut db = self.db.lock().await;
|
||||||
|
StoredKvEntry::delete_by_service_id_and_entry_key(&mut *db, service_id, entry_key).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unix_time_ms() -> u64 {
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|duration| u64::try_from(duration.as_millis()).unwrap_or(u64::MAX))
|
||||||
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -369,6 +369,80 @@ async fn reloads_manifests_without_restoring_actor_memory() {
|
|||||||
assert!(body["events"].as_array().unwrap().len() >= 3);
|
assert!(body["events"].as_array().unwrap().len() >= 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn persists_service_scoped_kv_across_revisions_and_restarts() {
|
||||||
|
const KV_WORLD: &str = "component:kv-probe/kv-probe-component@0.1.0";
|
||||||
|
|
||||||
|
let artifact_dir = TempDir::new().expect("temporary artifact directory");
|
||||||
|
let component = fs::read(component_artifact("kv_probe_component.wasm"))
|
||||||
|
.expect("KV probe component should be readable");
|
||||||
|
|
||||||
|
{
|
||||||
|
let application = test_app(artifact_dir.path()).await;
|
||||||
|
for (id, revision) in [
|
||||||
|
("shared-kv", "0.1.0"),
|
||||||
|
("shared-kv", "0.2.0"),
|
||||||
|
("isolated-kv", "0.1.0"),
|
||||||
|
] {
|
||||||
|
let response = application
|
||||||
|
.clone()
|
||||||
|
.oneshot(package_request_with_world(
|
||||||
|
id, revision, KV_WORLD, &component,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.expect("KV component registration should complete");
|
||||||
|
assert_eq!(response.status(), StatusCode::CREATED);
|
||||||
|
let registered = response_json(response).await;
|
||||||
|
assert_eq!(
|
||||||
|
registered["capabilities"][0]["interface"],
|
||||||
|
"wasmeld:kv/store@0.1.0"
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = application
|
||||||
|
.clone()
|
||||||
|
.oneshot(empty_post(&format!(
|
||||||
|
"/api/v1/services/{id}/{revision}/start"
|
||||||
|
)))
|
||||||
|
.await
|
||||||
|
.expect("KV actor start should complete");
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
invoke_service(&application, "shared-kv", "0.1.0", b"set:answer:forty-two").await,
|
||||||
|
b""
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
invoke_service(&application, "shared-kv", "0.2.0", b"get:answer").await,
|
||||||
|
b"forty-two"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
invoke_service(&application, "isolated-kv", "0.1.0", b"get:answer").await,
|
||||||
|
b""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let application = test_app(artifact_dir.path()).await;
|
||||||
|
let response = application
|
||||||
|
.clone()
|
||||||
|
.oneshot(empty_post("/api/v1/services/shared-kv/0.2.0/start"))
|
||||||
|
.await
|
||||||
|
.expect("restored KV actor start should complete");
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
assert_eq!(
|
||||||
|
invoke_service(&application, "shared-kv", "0.2.0", b"get:answer").await,
|
||||||
|
b"forty-two"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
invoke_service(&application, "shared-kv", "0.2.0", b"delete:answer").await,
|
||||||
|
b""
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
invoke_service(&application, "shared-kv", "0.2.0", b"get:answer").await,
|
||||||
|
b""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn manages_the_embedded_runtime_lifecycle() {
|
async fn manages_the_embedded_runtime_lifecycle() {
|
||||||
let artifact_dir = TempDir::new().expect("temporary artifact directory");
|
let artifact_dir = TempDir::new().expect("temporary artifact directory");
|
||||||
@@ -719,7 +793,6 @@ async fn test_apps(artifact_dir: &Path) -> (Router, Router) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn package_request(id: &str, revision: &str, component: &[u8]) -> Request<Body> {
|
fn package_request(id: &str, revision: &str, component: &[u8]) -> Request<Body> {
|
||||||
let mut package = Cursor::new(Vec::new());
|
|
||||||
let world = if id == "counter" {
|
let world = if id == "counter" {
|
||||||
"component:counter/counter-component@0.1.0"
|
"component:counter/counter-component@0.1.0"
|
||||||
} else if id == "clock-probe" {
|
} else if id == "clock-probe" {
|
||||||
@@ -727,6 +800,16 @@ fn package_request(id: &str, revision: &str, component: &[u8]) -> Request<Body>
|
|||||||
} else {
|
} else {
|
||||||
"component:echo/echo-component@0.1.0"
|
"component:echo/echo-component@0.1.0"
|
||||||
};
|
};
|
||||||
|
package_request_with_world(id, revision, world, component)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn package_request_with_world(
|
||||||
|
id: &str,
|
||||||
|
revision: &str,
|
||||||
|
world: &str,
|
||||||
|
component: &[u8],
|
||||||
|
) -> Request<Body> {
|
||||||
|
let mut package = Cursor::new(Vec::new());
|
||||||
write_package(&mut package, id, revision, world, component)
|
write_package(&mut package, id, revision, world, component)
|
||||||
.expect("test package should be valid");
|
.expect("test package should be valid");
|
||||||
package_bytes_request(package.get_ref())
|
package_bytes_request(package.get_ref())
|
||||||
@@ -816,6 +899,23 @@ async fn response_json(response: axum::response::Response) -> Value {
|
|||||||
serde_json::from_slice(&bytes).expect("response should be JSON")
|
serde_json::from_slice(&bytes).expect("response should be JSON")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn invoke_service(application: &Router, id: &str, revision: &str, input: &[u8]) -> Vec<u8> {
|
||||||
|
let response = application
|
||||||
|
.clone()
|
||||||
|
.oneshot(json_request(
|
||||||
|
&format!("/api/v1/services/{id}/{revision}/invoke"),
|
||||||
|
json!({ "input_base64": BASE64.encode(input) }),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.expect("component invocation should complete");
|
||||||
|
let status = response.status();
|
||||||
|
let body = response_json(response).await;
|
||||||
|
assert_eq!(status, StatusCode::OK, "{body}");
|
||||||
|
BASE64
|
||||||
|
.decode(body["output_base64"].as_str().unwrap())
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
fn echo_component() -> PathBuf {
|
fn echo_component() -> PathBuf {
|
||||||
component_artifact("echo_component.wasm")
|
component_artifact("echo_component.wasm")
|
||||||
}
|
}
|
||||||
@@ -826,6 +926,7 @@ fn component_artifact(name: &str) -> PathBuf {
|
|||||||
"components/echo/Cargo.toml",
|
"components/echo/Cargo.toml",
|
||||||
"components/counter/Cargo.toml",
|
"components/counter/Cargo.toml",
|
||||||
"components/clock-probe/Cargo.toml",
|
"components/clock-probe/Cargo.toml",
|
||||||
|
"components/kv-probe/Cargo.toml",
|
||||||
] {
|
] {
|
||||||
let status = Command::new("rustup")
|
let status = Command::new("rustup")
|
||||||
.args([
|
.args([
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
//! `wasmeld:kv/store@0.1.0` host implementation and backend boundary.
|
||||||
|
|
||||||
|
use std::{fmt, sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
use wasmtime::component::Linker;
|
||||||
|
|
||||||
|
use crate::{RuntimeError, runtime::HostState};
|
||||||
|
|
||||||
|
wasmtime::component::bindgen!({
|
||||||
|
path: "../../wit/kv",
|
||||||
|
world: "kv-host",
|
||||||
|
});
|
||||||
|
|
||||||
|
pub(crate) const INTERFACE: &str = "wasmeld:kv/store@0.1.0";
|
||||||
|
pub(crate) const PACKAGE: &str = "wasmeld:kv";
|
||||||
|
pub(crate) const NAME: &str = "store";
|
||||||
|
pub(crate) const VERSION: &str = "0.1.0";
|
||||||
|
|
||||||
|
/// Maximum UTF-8 byte length accepted for one KV key.
|
||||||
|
pub const KV_MAX_KEY_BYTES: usize = 256;
|
||||||
|
/// Maximum byte length accepted for one KV value.
|
||||||
|
pub const KV_MAX_VALUE_BYTES: usize = 64 * 1024;
|
||||||
|
|
||||||
|
/// Synchronous storage boundary used by the Wasmtime host implementation.
|
||||||
|
///
|
||||||
|
/// Implementations must bound their own blocking time because calls execute on
|
||||||
|
/// the Component Actor thread.
|
||||||
|
pub trait KvBackend: fmt::Debug + Send + Sync {
|
||||||
|
/// Returns the value stored under a service-scoped key.
|
||||||
|
fn get(
|
||||||
|
&self,
|
||||||
|
service_id: &str,
|
||||||
|
key: &str,
|
||||||
|
timeout: Duration,
|
||||||
|
) -> Result<Option<Vec<u8>>, KvBackendError>;
|
||||||
|
/// Creates or replaces a service-scoped value.
|
||||||
|
fn set(
|
||||||
|
&self,
|
||||||
|
service_id: &str,
|
||||||
|
key: &str,
|
||||||
|
value: &[u8],
|
||||||
|
timeout: Duration,
|
||||||
|
) -> Result<(), KvBackendError>;
|
||||||
|
/// Removes a service-scoped value if it exists.
|
||||||
|
fn delete(&self, service_id: &str, key: &str, timeout: Duration) -> Result<(), KvBackendError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Storage failures returned by a [`KvBackend`].
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum KvBackendError {
|
||||||
|
#[error("KV backend is busy")]
|
||||||
|
Busy,
|
||||||
|
#[error("KV backend timed out")]
|
||||||
|
Timeout,
|
||||||
|
#[error("KV backend operation failed: {0}")]
|
||||||
|
Operation(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct KvState {
|
||||||
|
service_id: String,
|
||||||
|
backend: Arc<dyn KvBackend>,
|
||||||
|
timeout: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KvState {
|
||||||
|
pub(crate) fn new(service_id: &str, backend: Arc<dyn KvBackend>, timeout: Duration) -> Self {
|
||||||
|
Self {
|
||||||
|
service_id: service_id.to_owned(),
|
||||||
|
backend,
|
||||||
|
timeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self, key: &str) -> Result<Option<Vec<u8>>, wasmeld::kv::store::KvError> {
|
||||||
|
validate_key(key)?;
|
||||||
|
self.backend
|
||||||
|
.get(&self.service_id, key, self.timeout)
|
||||||
|
.map_err(operation_failed)
|
||||||
|
.and_then(|value| match value {
|
||||||
|
Some(value) if value.len() > KV_MAX_VALUE_BYTES => {
|
||||||
|
Err(wasmeld::kv::store::KvError::ValueTooLarge(
|
||||||
|
u64::try_from(value.len()).unwrap_or(u64::MAX),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
value => Ok(value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set(&self, key: &str, value: &[u8]) -> Result<(), wasmeld::kv::store::KvError> {
|
||||||
|
validate_key(key)?;
|
||||||
|
if value.len() > KV_MAX_VALUE_BYTES {
|
||||||
|
return Err(wasmeld::kv::store::KvError::ValueTooLarge(
|
||||||
|
u64::try_from(value.len()).unwrap_or(u64::MAX),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.backend
|
||||||
|
.set(&self.service_id, key, value, self.timeout)
|
||||||
|
.map_err(operation_failed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete(&self, key: &str) -> Result<(), wasmeld::kv::store::KvError> {
|
||||||
|
validate_key(key)?;
|
||||||
|
self.backend
|
||||||
|
.delete(&self.service_id, key, self.timeout)
|
||||||
|
.map_err(operation_failed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl wasmeld::kv::store::Host for HostState {
|
||||||
|
fn get(&mut self, key: String) -> Result<Option<Vec<u8>>, wasmeld::kv::store::KvError> {
|
||||||
|
self.capabilities.kv().get(&key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set(&mut self, key: String, value: Vec<u8>) -> Result<(), wasmeld::kv::store::KvError> {
|
||||||
|
self.capabilities.kv().set(&key, &value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete(&mut self, key: String) -> Result<(), wasmeld::kv::store::KvError> {
|
||||||
|
self.capabilities.kv().delete(&key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn add_to_linker(linker: &mut Linker<HostState>) -> Result<(), RuntimeError> {
|
||||||
|
wasmeld::kv::store::add_to_linker::<HostState, HostState>(linker, |state| state)
|
||||||
|
.map_err(|error| RuntimeError::ActorInitialization(error.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_key(key: &str) -> Result<(), wasmeld::kv::store::KvError> {
|
||||||
|
if key.is_empty() {
|
||||||
|
return Err(wasmeld::kv::store::KvError::InvalidKey(
|
||||||
|
"key must not be empty".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if key.len() > KV_MAX_KEY_BYTES {
|
||||||
|
return Err(wasmeld::kv::store::KvError::InvalidKey(format!(
|
||||||
|
"key exceeds the {KV_MAX_KEY_BYTES}-byte limit"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operation_failed(error: KvBackendError) -> wasmeld::kv::store::KvError {
|
||||||
|
wasmeld::kv::store::KvError::OperationFailed(error.to_string())
|
||||||
|
}
|
||||||
@@ -1,22 +1,37 @@
|
|||||||
//! Versioned host capabilities available to WebAssembly Components.
|
//! Versioned host capabilities available to WebAssembly Components.
|
||||||
|
|
||||||
mod clock;
|
mod clock;
|
||||||
|
mod kv;
|
||||||
mod registry;
|
mod registry;
|
||||||
|
|
||||||
|
pub use kv::{KV_MAX_KEY_BYTES, KV_MAX_VALUE_BYTES, KvBackend, KvBackendError};
|
||||||
pub use registry::CapabilityDescriptor;
|
pub use registry::CapabilityDescriptor;
|
||||||
pub(crate) use registry::{Capability, CapabilityRegistry};
|
pub(crate) use registry::{Capability, CapabilityRegistry};
|
||||||
|
|
||||||
/// Per-Actor state owned by enabled host capabilities.
|
/// Per-Actor state owned by enabled host capabilities.
|
||||||
pub(crate) struct HostCapabilities {
|
pub(crate) struct HostCapabilities {
|
||||||
clock: Option<clock::ClockState>,
|
clock: Option<clock::ClockState>,
|
||||||
|
kv: Option<kv::KvState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HostCapabilities {
|
impl HostCapabilities {
|
||||||
pub(crate) fn new(capabilities: &[Capability]) -> Self {
|
pub(crate) fn new(
|
||||||
|
capabilities: &[Capability],
|
||||||
|
service_id: &str,
|
||||||
|
kv_backend: Option<std::sync::Arc<dyn KvBackend>>,
|
||||||
|
deadline: std::time::Duration,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
clock: capabilities
|
clock: capabilities
|
||||||
.contains(&Capability::MonotonicClock)
|
.contains(&Capability::MonotonicClock)
|
||||||
.then(clock::ClockState::new),
|
.then(clock::ClockState::new),
|
||||||
|
kv: capabilities.contains(&Capability::KvStore).then(|| {
|
||||||
|
kv::KvState::new(
|
||||||
|
service_id,
|
||||||
|
kv_backend.expect("KV Components are rejected when no backend is configured"),
|
||||||
|
deadline,
|
||||||
|
)
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,4 +41,10 @@ impl HostCapabilities {
|
|||||||
.expect("the Clock interface is linked only when its state is enabled")
|
.expect("the Clock interface is linked only when its state is enabled")
|
||||||
.now()
|
.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn kv(&self) -> &kv::KvState {
|
||||||
|
self.kv
|
||||||
|
.as_ref()
|
||||||
|
.expect("the KV interface is linked only when its state is enabled")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use wasmtime::component::Linker;
|
use wasmtime::component::Linker;
|
||||||
|
|
||||||
use super::clock;
|
use super::{clock, kv};
|
||||||
use crate::{RuntimeError, runtime::HostState};
|
use crate::{RuntimeError, runtime::HostState};
|
||||||
|
|
||||||
/// Stable description of one versioned WIT interface used by a Component.
|
/// Stable description of one versioned WIT interface used by a Component.
|
||||||
@@ -53,6 +53,7 @@ impl CapabilityDescriptor {
|
|||||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||||
pub(crate) enum Capability {
|
pub(crate) enum Capability {
|
||||||
MonotonicClock,
|
MonotonicClock,
|
||||||
|
KvStore,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Capability {
|
impl Capability {
|
||||||
@@ -64,12 +65,16 @@ impl Capability {
|
|||||||
clock::NAME,
|
clock::NAME,
|
||||||
clock::VERSION,
|
clock::VERSION,
|
||||||
),
|
),
|
||||||
|
Self::KvStore => {
|
||||||
|
CapabilityDescriptor::new(kv::INTERFACE, kv::PACKAGE, kv::NAME, kv::VERSION)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn add_to_linker(self, linker: &mut Linker<HostState>) -> Result<(), RuntimeError> {
|
pub(crate) fn add_to_linker(self, linker: &mut Linker<HostState>) -> Result<(), RuntimeError> {
|
||||||
match self {
|
match self {
|
||||||
Self::MonotonicClock => clock::add_to_linker(linker),
|
Self::MonotonicClock => clock::add_to_linker(linker),
|
||||||
|
Self::KvStore => kv::add_to_linker(linker),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,6 +87,7 @@ impl CapabilityRegistry {
|
|||||||
pub(crate) fn resolve(interface: &str) -> Option<Capability> {
|
pub(crate) fn resolve(interface: &str) -> Option<Capability> {
|
||||||
match interface {
|
match interface {
|
||||||
clock::INTERFACE => Some(Capability::MonotonicClock),
|
clock::INTERFACE => Some(Capability::MonotonicClock),
|
||||||
|
kv::INTERFACE => Some(Capability::KvStore),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,5 +122,10 @@ mod tests {
|
|||||||
CapabilityRegistry::resolve("wasmeld:clock/other@0.1.0"),
|
CapabilityRegistry::resolve("wasmeld:clock/other@0.1.0"),
|
||||||
None
|
None
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
CapabilityRegistry::resolve("wasmeld:kv/store@0.1.0"),
|
||||||
|
Some(Capability::KvStore)
|
||||||
|
);
|
||||||
|
assert_eq!(CapabilityRegistry::resolve("wasmeld:kv/store@0.2.0"), None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ pub enum RuntimeError {
|
|||||||
#[error("component imports unsupported capability {0}")]
|
#[error("component imports unsupported capability {0}")]
|
||||||
UnsupportedImport(String),
|
UnsupportedImport(String),
|
||||||
|
|
||||||
|
#[error("component requires unavailable capability {0}")]
|
||||||
|
CapabilityUnavailable(String),
|
||||||
|
|
||||||
#[error("runtime creation failed: {0}")]
|
#[error("runtime creation failed: {0}")]
|
||||||
RuntimeCreation(#[source] wasmtime::Error),
|
RuntimeCreation(#[source] wasmtime::Error),
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ mod error;
|
|||||||
mod manifest;
|
mod manifest;
|
||||||
mod runtime;
|
mod runtime;
|
||||||
|
|
||||||
pub use capability::CapabilityDescriptor;
|
pub use capability::{
|
||||||
|
CapabilityDescriptor, KV_MAX_KEY_BYTES, KV_MAX_VALUE_BYTES, KvBackend, KvBackendError,
|
||||||
|
};
|
||||||
pub use error::RuntimeError;
|
pub use error::RuntimeError;
|
||||||
pub use manifest::{ResourceLimits, ServiceKey, ServiceManifest};
|
pub use manifest::{ResourceLimits, ServiceKey, ServiceManifest};
|
||||||
pub use runtime::{ActorHandle, Runtime, RuntimeConfig, RuntimeStats};
|
pub use runtime::{ActorHandle, Runtime, RuntimeConfig, RuntimeStats};
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ use wasmtime_wasi::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
CapabilityDescriptor, RuntimeError, ServiceKey, ServiceManifest,
|
CapabilityDescriptor, KvBackend, RuntimeError, ServiceKey, ServiceManifest,
|
||||||
bindings::{ServiceComponent, ServiceError},
|
bindings::{ServiceComponent, ServiceError},
|
||||||
capability::{Capability, CapabilityRegistry, HostCapabilities},
|
capability::{Capability, CapabilityRegistry, HostCapabilities},
|
||||||
manifest::ResourceLimits,
|
manifest::ResourceLimits,
|
||||||
@@ -61,6 +61,8 @@ pub struct RuntimeConfig {
|
|||||||
pub epoch_tick: Duration,
|
pub epoch_tick: Duration,
|
||||||
/// Maximum native stack reservation for WebAssembly execution.
|
/// Maximum native stack reservation for WebAssembly execution.
|
||||||
pub max_wasm_stack: usize,
|
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 {
|
impl Default for RuntimeConfig {
|
||||||
@@ -68,6 +70,7 @@ impl Default for RuntimeConfig {
|
|||||||
Self {
|
Self {
|
||||||
epoch_tick: DEFAULT_EPOCH_TICK,
|
epoch_tick: DEFAULT_EPOCH_TICK,
|
||||||
max_wasm_stack: DEFAULT_MAX_WASM_STACK,
|
max_wasm_stack: DEFAULT_MAX_WASM_STACK,
|
||||||
|
kv_backend: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,6 +120,7 @@ struct RuntimeInner {
|
|||||||
actors: Mutex<HashMap<ServiceKey, ActorHandle>>,
|
actors: Mutex<HashMap<ServiceKey, ActorHandle>>,
|
||||||
lifecycle: Mutex<()>,
|
lifecycle: Mutex<()>,
|
||||||
ticker: Arc<EpochTicker>,
|
ticker: Arc<EpochTicker>,
|
||||||
|
kv_backend: Option<Arc<dyn KvBackend>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct EpochTicker {
|
struct EpochTicker {
|
||||||
@@ -189,6 +193,7 @@ impl Runtime {
|
|||||||
actors: Mutex::new(HashMap::new()),
|
actors: Mutex::new(HashMap::new()),
|
||||||
lifecycle: Mutex::new(()),
|
lifecycle: Mutex::new(()),
|
||||||
ticker,
|
ticker,
|
||||||
|
kv_backend: config.kv_backend,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -207,6 +212,11 @@ impl Runtime {
|
|||||||
let component = Component::new(&self.inner.engine, component_bytes.as_ref())
|
let component = Component::new(&self.inner.engine, component_bytes.as_ref())
|
||||||
.map_err(RuntimeError::ComponentCompilation)?;
|
.map_err(RuntimeError::ComponentCompilation)?;
|
||||||
let capabilities = validate_component_imports(&component, &self.inner.engine)?;
|
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
|
let mut services = self
|
||||||
.inner
|
.inner
|
||||||
@@ -298,6 +308,7 @@ impl Runtime {
|
|||||||
key.clone(),
|
key.clone(),
|
||||||
service,
|
service,
|
||||||
init_config,
|
init_config,
|
||||||
|
self.inner.kv_backend.clone(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
self.inner
|
self.inner
|
||||||
@@ -580,7 +591,12 @@ pub(crate) struct HostState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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()
|
let store_limits = StoreLimitsBuilder::new()
|
||||||
.memory_size(limits.memory_bytes)
|
.memory_size(limits.memory_bytes)
|
||||||
.table_elements(ACTOR_TABLE_ELEMENT_LIMIT)
|
.table_elements(ACTOR_TABLE_ELEMENT_LIMIT)
|
||||||
@@ -600,7 +616,12 @@ impl HostState {
|
|||||||
store_limits,
|
store_limits,
|
||||||
wasi: wasi.build(),
|
wasi: wasi.build(),
|
||||||
wasi_table: ResourceTable::new(),
|
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,
|
key: ServiceKey,
|
||||||
service: RegisteredService,
|
service: RegisteredService,
|
||||||
init_config: Vec<u8>,
|
init_config: Vec<u8>,
|
||||||
|
kv_backend: Option<Arc<dyn KvBackend>>,
|
||||||
) -> Result<Self, RuntimeError> {
|
) -> Result<Self, RuntimeError> {
|
||||||
let limits = Arc::new(service.manifest.limits.clone());
|
let limits = Arc::new(service.manifest.limits.clone());
|
||||||
if init_config.len() > limits.max_input_bytes {
|
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);
|
store.limiter(|state| &mut state.store_limits);
|
||||||
|
|
||||||
let mut linker = Linker::new(&engine);
|
let mut linker = Linker::new(&engine);
|
||||||
@@ -797,6 +822,7 @@ fn spawn_actor(
|
|||||||
key: ServiceKey,
|
key: ServiceKey,
|
||||||
service: RegisteredService,
|
service: RegisteredService,
|
||||||
init_config: Vec<u8>,
|
init_config: Vec<u8>,
|
||||||
|
kv_backend: Option<Arc<dyn KvBackend>>,
|
||||||
) -> Result<ActorHandle, RuntimeError> {
|
) -> Result<ActorHandle, RuntimeError> {
|
||||||
let limits = Arc::new(service.manifest.limits.clone());
|
let limits = Arc::new(service.manifest.limits.clone());
|
||||||
let (sender, receiver) = mpsc::sync_channel(limits.mailbox_capacity);
|
let (sender, receiver) = mpsc::sync_channel(limits.mailbox_capacity);
|
||||||
@@ -811,7 +837,7 @@ fn spawn_actor(
|
|||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
let _ticker = worker_ticker;
|
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) => {
|
Ok(mut worker) => {
|
||||||
worker_status.mark_ready();
|
worker_status.mark_ready();
|
||||||
let _ = ready_sender.send(Ok(()));
|
let _ = ready_sender.send(Ok(()));
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
use std::{
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
fs,
|
fs,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::Command,
|
process::Command,
|
||||||
sync::OnceLock,
|
sync::{Arc, Mutex, OnceLock},
|
||||||
thread,
|
thread,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
use wasmeld_runtime::{ResourceLimits, Runtime, RuntimeConfig, RuntimeError, ServiceManifest};
|
use wasmeld_runtime::{
|
||||||
|
KV_MAX_VALUE_BYTES, KvBackend, KvBackendError, ResourceLimits, Runtime, RuntimeConfig,
|
||||||
|
RuntimeError, ServiceManifest,
|
||||||
|
};
|
||||||
|
|
||||||
static COMPONENTS_BUILT: OnceLock<()> = OnceLock::new();
|
static COMPONENTS_BUILT: OnceLock<()> = OnceLock::new();
|
||||||
|
|
||||||
@@ -188,6 +192,97 @@ fn explicit_clock_capability_is_linked() {
|
|||||||
runtime.stop(&key).unwrap();
|
runtime.stop(&key).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn kv_capability_is_service_scoped_and_shared_across_revisions() {
|
||||||
|
let backend = Arc::new(MemoryKvBackend::default());
|
||||||
|
let runtime = Runtime::new(RuntimeConfig {
|
||||||
|
kv_backend: Some(backend),
|
||||||
|
..RuntimeConfig::default()
|
||||||
|
})
|
||||||
|
.expect("runtime should start");
|
||||||
|
let first_key = register_component_revision(
|
||||||
|
&runtime,
|
||||||
|
"shared-kv",
|
||||||
|
"0.1.0",
|
||||||
|
"kv_probe_component.wasm",
|
||||||
|
test_limits(),
|
||||||
|
);
|
||||||
|
let second_key = register_component_revision(
|
||||||
|
&runtime,
|
||||||
|
"shared-kv",
|
||||||
|
"0.2.0",
|
||||||
|
"kv_probe_component.wasm",
|
||||||
|
test_limits(),
|
||||||
|
);
|
||||||
|
let isolated_key = register_component_revision(
|
||||||
|
&runtime,
|
||||||
|
"isolated-kv",
|
||||||
|
"0.1.0",
|
||||||
|
"kv_probe_component.wasm",
|
||||||
|
test_limits(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let first = runtime.start(&first_key, Vec::new()).unwrap();
|
||||||
|
let second = runtime.start(&second_key, Vec::new()).unwrap();
|
||||||
|
let isolated = runtime.start(&isolated_key, Vec::new()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(first.invoke(b"set:answer:forty-two".to_vec()).unwrap(), b"");
|
||||||
|
assert_eq!(second.invoke(b"get:answer".to_vec()).unwrap(), b"forty-two");
|
||||||
|
assert_eq!(isolated.invoke(b"get:answer".to_vec()).unwrap(), b"");
|
||||||
|
assert_eq!(
|
||||||
|
runtime.capabilities(&first_key).unwrap()[0].interface(),
|
||||||
|
"wasmeld:kv/store@0.1.0"
|
||||||
|
);
|
||||||
|
|
||||||
|
runtime.stop_all().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn kv_capability_requires_a_backend_and_enforces_limits() {
|
||||||
|
let artifact = component_artifact("kv_probe_component.wasm");
|
||||||
|
let manifest = ServiceManifest {
|
||||||
|
id: "kv-without-backend".to_owned(),
|
||||||
|
revision: "0.1.0".to_owned(),
|
||||||
|
component: artifact.display().to_string(),
|
||||||
|
world: component_world("kv_probe_component.wasm").to_owned(),
|
||||||
|
limits: test_limits(),
|
||||||
|
};
|
||||||
|
let bytes = fs::read(&artifact).unwrap();
|
||||||
|
let runtime = Runtime::new(RuntimeConfig::default()).unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
runtime.register(manifest, &bytes),
|
||||||
|
Err(RuntimeError::CapabilityUnavailable(interface))
|
||||||
|
if interface == "wasmeld:kv/store@0.1.0"
|
||||||
|
));
|
||||||
|
|
||||||
|
let runtime = Runtime::new(RuntimeConfig {
|
||||||
|
kv_backend: Some(Arc::new(MemoryKvBackend::default())),
|
||||||
|
..RuntimeConfig::default()
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let mut limits = test_limits();
|
||||||
|
limits.max_input_bytes = 128 * 1024;
|
||||||
|
let key = register_component(&runtime, "kv-limits", "kv_probe_component.wasm", limits);
|
||||||
|
let actor = runtime.start(&key, Vec::new()).unwrap();
|
||||||
|
|
||||||
|
let long_key = format!("get:{}", "k".repeat(257));
|
||||||
|
assert!(matches!(
|
||||||
|
actor.invoke(long_key.into_bytes()),
|
||||||
|
Err(RuntimeError::ComponentError { message, .. })
|
||||||
|
if message.contains("InvalidKey")
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut oversized = b"set:key:".to_vec();
|
||||||
|
oversized.resize(oversized.len() + KV_MAX_VALUE_BYTES + 1, b'x');
|
||||||
|
assert!(matches!(
|
||||||
|
actor.invoke(oversized),
|
||||||
|
Err(RuntimeError::ComponentError { message, .. })
|
||||||
|
if message.contains("ValueTooLarge")
|
||||||
|
));
|
||||||
|
|
||||||
|
runtime.stop(&key).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn registration_rejects_non_whitelisted_wasi_imports() {
|
fn registration_rejects_non_whitelisted_wasi_imports() {
|
||||||
let runtime = Runtime::new(RuntimeConfig::default()).expect("runtime should start");
|
let runtime = Runtime::new(RuntimeConfig::default()).expect("runtime should start");
|
||||||
@@ -225,11 +320,21 @@ fn register_component(
|
|||||||
service_id: &str,
|
service_id: &str,
|
||||||
artifact_name: &str,
|
artifact_name: &str,
|
||||||
limits: ResourceLimits,
|
limits: ResourceLimits,
|
||||||
|
) -> wasmeld_runtime::ServiceKey {
|
||||||
|
register_component_revision(runtime, service_id, "0.1.0", artifact_name, limits)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_component_revision(
|
||||||
|
runtime: &Runtime,
|
||||||
|
service_id: &str,
|
||||||
|
revision: &str,
|
||||||
|
artifact_name: &str,
|
||||||
|
limits: ResourceLimits,
|
||||||
) -> wasmeld_runtime::ServiceKey {
|
) -> wasmeld_runtime::ServiceKey {
|
||||||
let artifact = component_artifact(artifact_name);
|
let artifact = component_artifact(artifact_name);
|
||||||
let manifest = ServiceManifest {
|
let manifest = ServiceManifest {
|
||||||
id: service_id.to_owned(),
|
id: service_id.to_owned(),
|
||||||
revision: "0.1.0".to_owned(),
|
revision: revision.to_owned(),
|
||||||
component: artifact.display().to_string(),
|
component: artifact.display().to_string(),
|
||||||
world: component_world(artifact_name).to_owned(),
|
world: component_world(artifact_name).to_owned(),
|
||||||
limits,
|
limits,
|
||||||
@@ -256,6 +361,7 @@ fn build_components_once() {
|
|||||||
"components/fault/Cargo.toml",
|
"components/fault/Cargo.toml",
|
||||||
"components/spin/Cargo.toml",
|
"components/spin/Cargo.toml",
|
||||||
"components/clock-probe/Cargo.toml",
|
"components/clock-probe/Cargo.toml",
|
||||||
|
"components/kv-probe/Cargo.toml",
|
||||||
"components/wasi-clock-probe/Cargo.toml",
|
"components/wasi-clock-probe/Cargo.toml",
|
||||||
] {
|
] {
|
||||||
let status = Command::new("rustup")
|
let status = Command::new("rustup")
|
||||||
@@ -323,9 +429,58 @@ fn component_world(artifact_name: &str) -> &'static str {
|
|||||||
"fault_component.wasm" => "component:fault/fault-component@0.1.0",
|
"fault_component.wasm" => "component:fault/fault-component@0.1.0",
|
||||||
"spin_component.wasm" => "component:spin/spin-component@0.1.0",
|
"spin_component.wasm" => "component:spin/spin-component@0.1.0",
|
||||||
"clock_probe_component.wasm" => "component:clock-probe/clock-probe-component@0.1.0",
|
"clock_probe_component.wasm" => "component:clock-probe/clock-probe-component@0.1.0",
|
||||||
|
"kv_probe_component.wasm" => "component:kv-probe/kv-probe-component@0.1.0",
|
||||||
"wasi_clock_probe_component.wasm" => {
|
"wasi_clock_probe_component.wasm" => {
|
||||||
"component:wasi-clock-probe/wasi-clock-probe-component@0.1.0"
|
"component:wasi-clock-probe/wasi-clock-probe-component@0.1.0"
|
||||||
}
|
}
|
||||||
other => panic!("no WIT world registered for {other}"),
|
other => panic!("no WIT world registered for {other}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct MemoryKvBackend {
|
||||||
|
entries: Mutex<HashMap<(String, String), Vec<u8>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KvBackend for MemoryKvBackend {
|
||||||
|
fn get(
|
||||||
|
&self,
|
||||||
|
service_id: &str,
|
||||||
|
key: &str,
|
||||||
|
_timeout: Duration,
|
||||||
|
) -> Result<Option<Vec<u8>>, KvBackendError> {
|
||||||
|
Ok(self
|
||||||
|
.entries
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.get(&(service_id.to_owned(), key.to_owned()))
|
||||||
|
.cloned())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set(
|
||||||
|
&self,
|
||||||
|
service_id: &str,
|
||||||
|
key: &str,
|
||||||
|
value: &[u8],
|
||||||
|
_timeout: Duration,
|
||||||
|
) -> Result<(), KvBackendError> {
|
||||||
|
self.entries
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert((service_id.to_owned(), key.to_owned()), value.to_vec());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete(
|
||||||
|
&self,
|
||||||
|
service_id: &str,
|
||||||
|
key: &str,
|
||||||
|
_timeout: Duration,
|
||||||
|
) -> Result<(), KvBackendError> {
|
||||||
|
self.entries
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.remove(&(service_id.to_owned(), key.to_owned()));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package wasmeld:kv@0.1.0;
|
||||||
|
|
||||||
|
interface store {
|
||||||
|
variant kv-error {
|
||||||
|
invalid-key(string),
|
||||||
|
value-too-large(u64),
|
||||||
|
operation-failed(string),
|
||||||
|
}
|
||||||
|
|
||||||
|
get: func(key: string) -> result<option<list<u8>>, kv-error>;
|
||||||
|
set: func(key: string, value: list<u8>) -> result<_, kv-error>;
|
||||||
|
delete: func(key: string) -> result<_, kv-error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
world kv-host {
|
||||||
|
import store;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user