feat(console): persist Host KV in libSQL
Store KV entries through Toasty with a composite service ID and key primary key, including an additive table migration for existing Console databases. Bridge synchronous Wasmtime Host calls to async persistence with a bounded worker queue and response timeout capped by the service deadline. Verify same-service revision sharing, cross-service isolation, deletion, exact capability reporting, and persistence after reopening Console.
This commit is contained in:
@@ -369,6 +369,80 @@ async fn reloads_manifests_without_restoring_actor_memory() {
|
||||
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]
|
||||
async fn manages_the_embedded_runtime_lifecycle() {
|
||||
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> {
|
||||
let mut package = Cursor::new(Vec::new());
|
||||
let world = if id == "counter" {
|
||||
"component:counter/counter-component@0.1.0"
|
||||
} else if id == "clock-probe" {
|
||||
@@ -727,6 +800,16 @@ fn package_request(id: &str, revision: &str, component: &[u8]) -> Request<Body>
|
||||
} else {
|
||||
"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)
|
||||
.expect("test package should be valid");
|
||||
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")
|
||||
}
|
||||
|
||||
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 {
|
||||
component_artifact("echo_component.wasm")
|
||||
}
|
||||
@@ -826,6 +926,7 @@ fn component_artifact(name: &str) -> PathBuf {
|
||||
"components/echo/Cargo.toml",
|
||||
"components/counter/Cargo.toml",
|
||||
"components/clock-probe/Cargo.toml",
|
||||
"components/kv-probe/Cargo.toml",
|
||||
] {
|
||||
let status = Command::new("rustup")
|
||||
.args([
|
||||
|
||||
Reference in New Issue
Block a user