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:
Maofeng
2026-07-29 19:37:17 +08:00
parent 8d8d136510
commit a325c60421
4 changed files with 416 additions and 10 deletions
+79 -7
View File
@@ -1,10 +1,14 @@
//! Toasty models and transactional libSQL snapshots for Console control data.
//!
//! Component bytes and WIT packages remain filesystem artifacts. This module
//! persists only service manifests, deployments, metrics, and the bounded
//! event history.
//! persists service manifests, deployments, metrics, the bounded event
//! 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_driver_turso::Turso;
@@ -40,9 +44,19 @@ pub(crate) struct StoredDeployment {
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.
#[derive(Clone)]
pub(crate) struct Persistence {
db: Mutex<Db>,
db: Arc<Mutex<Db>>,
}
impl Persistence {
@@ -53,7 +67,8 @@ impl Persistence {
.models(toasty::models!(
StoredService,
StoredEvent,
StoredDeployment
StoredDeployment,
StoredKvEntry
))
.build(Turso::file(path))
.await?;
@@ -74,9 +89,24 @@ impl Persistence {
)
.exec(&mut db)
.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.
@@ -139,4 +169,46 @@ impl Persistence {
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()
}