2026-07-27 05:01:26 +08:00
|
|
|
//! Toasty models and transactional libSQL snapshots for Console control data.
|
|
|
|
|
//!
|
|
|
|
|
//! Component bytes and WIT packages remain filesystem artifacts. This module
|
2026-07-29 19:37:17 +08:00
|
|
|
//! persists service manifests, deployments, metrics, the bounded event
|
|
|
|
|
//! history, and service-scoped Host KV entries.
|
2026-07-27 05:01:26 +08:00
|
|
|
|
2026-07-29 19:37:17 +08:00
|
|
|
use std::{
|
2026-07-29 20:26:06 +08:00
|
|
|
collections::BTreeSet,
|
2026-07-29 19:37:17 +08:00
|
|
|
path::Path,
|
|
|
|
|
sync::Arc,
|
|
|
|
|
time::{SystemTime, UNIX_EPOCH},
|
|
|
|
|
};
|
2026-07-27 05:01:26 +08:00
|
|
|
|
|
|
|
|
use toasty::Db;
|
|
|
|
|
use toasty_driver_turso::Turso;
|
|
|
|
|
use tokio::sync::Mutex;
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, toasty::Model)]
|
|
|
|
|
pub(crate) struct StoredService {
|
|
|
|
|
#[key]
|
|
|
|
|
pub service_key: String,
|
|
|
|
|
pub manifest_toml: String,
|
|
|
|
|
pub updated_at_ms: u64,
|
|
|
|
|
pub calls: u64,
|
|
|
|
|
pub errors: u64,
|
|
|
|
|
pub last_latency_ms: Option<u64>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, toasty::Model)]
|
|
|
|
|
pub(crate) struct StoredEvent {
|
|
|
|
|
#[key]
|
|
|
|
|
pub id: u64,
|
|
|
|
|
pub timestamp_ms: u64,
|
|
|
|
|
pub kind: String,
|
|
|
|
|
pub service_id: Option<String>,
|
|
|
|
|
pub revision: Option<String>,
|
|
|
|
|
pub message: String,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-29 16:32:12 +08:00
|
|
|
#[derive(Clone, Debug, toasty::Model)]
|
|
|
|
|
pub(crate) struct StoredDeployment {
|
|
|
|
|
#[key]
|
|
|
|
|
pub service_id: String,
|
|
|
|
|
pub active_revision: String,
|
|
|
|
|
pub updated_at_ms: u64,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-29 19:37:17 +08:00
|
|
|
#[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,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-30 07:18:25 +08:00
|
|
|
/// One invocation's durable metric delta and event.
|
|
|
|
|
pub(crate) struct InvocationUpdate {
|
|
|
|
|
pub service_key: String,
|
|
|
|
|
pub failed: bool,
|
|
|
|
|
pub updated_at_ms: u64,
|
|
|
|
|
pub latency_ms: u64,
|
|
|
|
|
pub event: StoredEvent,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-27 05:01:26 +08:00
|
|
|
/// Serialized access to the Toasty database connection.
|
2026-07-29 19:37:17 +08:00
|
|
|
#[derive(Clone)]
|
2026-07-27 05:01:26 +08:00
|
|
|
pub(crate) struct Persistence {
|
2026-07-29 19:37:17 +08:00
|
|
|
db: Arc<Mutex<Db>>,
|
2026-07-27 05:01:26 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Persistence {
|
2026-07-29 16:32:12 +08:00
|
|
|
/// Opens the database and installs or advances its schema.
|
2026-07-27 05:01:26 +08:00
|
|
|
pub async fn open(path: &Path) -> toasty::Result<Self> {
|
|
|
|
|
let is_new_database = !path.exists();
|
|
|
|
|
let db = Db::builder()
|
2026-07-29 16:32:12 +08:00
|
|
|
.models(toasty::models!(
|
|
|
|
|
StoredService,
|
|
|
|
|
StoredEvent,
|
2026-07-29 19:37:17 +08:00
|
|
|
StoredDeployment,
|
|
|
|
|
StoredKvEntry
|
2026-07-29 16:32:12 +08:00
|
|
|
))
|
2026-07-27 05:01:26 +08:00
|
|
|
.build(Turso::file(path))
|
|
|
|
|
.await?;
|
|
|
|
|
if is_new_database {
|
|
|
|
|
db.push_schema().await?;
|
2026-07-29 16:32:12 +08:00
|
|
|
} else {
|
|
|
|
|
// This project predates Toasty's migration history. Keep the
|
|
|
|
|
// additive bootstrap explicit so existing databases can be opened
|
|
|
|
|
// without recreating the service and event tables.
|
|
|
|
|
let mut db = db;
|
|
|
|
|
toasty::sql::statement(
|
|
|
|
|
r#"CREATE TABLE IF NOT EXISTS "stored_deployments" (
|
|
|
|
|
"service_id" TEXT NOT NULL,
|
|
|
|
|
"active_revision" TEXT NOT NULL,
|
|
|
|
|
"updated_at_ms" INTEGER NOT NULL,
|
|
|
|
|
PRIMARY KEY ("service_id")
|
|
|
|
|
)"#,
|
|
|
|
|
)
|
|
|
|
|
.exec(&mut db)
|
|
|
|
|
.await?;
|
2026-07-29 19:37:17 +08:00
|
|
|
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)),
|
|
|
|
|
});
|
2026-07-27 05:01:26 +08:00
|
|
|
}
|
2026-07-29 19:37:17 +08:00
|
|
|
Ok(Self {
|
|
|
|
|
db: Arc::new(Mutex::new(db)),
|
|
|
|
|
})
|
2026-07-27 05:01:26 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-29 16:32:12 +08:00
|
|
|
/// Loads the complete service and deployment sets plus retained events.
|
|
|
|
|
pub async fn load(
|
|
|
|
|
&self,
|
|
|
|
|
) -> toasty::Result<(Vec<StoredService>, Vec<StoredEvent>, Vec<StoredDeployment>)> {
|
2026-07-27 05:01:26 +08:00
|
|
|
let mut db = self.db.lock().await;
|
|
|
|
|
let services = StoredService::all().exec(&mut *db).await?;
|
|
|
|
|
let events = StoredEvent::all().exec(&mut *db).await?;
|
2026-07-29 16:32:12 +08:00
|
|
|
let deployments = StoredDeployment::all().exec(&mut *db).await?;
|
|
|
|
|
Ok((services, events, deployments))
|
2026-07-27 05:01:26 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Atomically upserts a Console snapshot and prunes expired events.
|
|
|
|
|
pub async fn sync(
|
|
|
|
|
&self,
|
|
|
|
|
services: Vec<StoredService>,
|
|
|
|
|
events: Vec<StoredEvent>,
|
2026-07-29 16:32:12 +08:00
|
|
|
deployments: Vec<StoredDeployment>,
|
2026-07-27 05:01:26 +08:00
|
|
|
) -> toasty::Result<()> {
|
|
|
|
|
let mut db = self.db.lock().await;
|
|
|
|
|
let mut tx = db.transaction().await?;
|
|
|
|
|
|
2026-07-30 07:18:25 +08:00
|
|
|
let stored_services = StoredService::all().exec(&mut tx).await?;
|
2026-07-29 20:26:06 +08:00
|
|
|
let service_keys = services
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|service| service.service_key.clone())
|
|
|
|
|
.collect::<BTreeSet<_>>();
|
2026-07-30 07:18:25 +08:00
|
|
|
let existing_service_keys = stored_services
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|service| service.service_key.clone())
|
|
|
|
|
.collect::<BTreeSet<_>>();
|
|
|
|
|
for stored in stored_services {
|
2026-07-29 20:26:06 +08:00
|
|
|
if !service_keys.contains(&stored.service_key) {
|
|
|
|
|
StoredService::delete_by_service_key(&mut tx, &stored.service_key).await?;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-27 05:01:26 +08:00
|
|
|
for service in services {
|
2026-07-30 07:18:25 +08:00
|
|
|
if existing_service_keys.contains(&service.service_key) {
|
|
|
|
|
// Invocation metrics are written as ordered deltas by
|
|
|
|
|
// `record_invocations`; control snapshots must not overwrite
|
|
|
|
|
// or double-count those concurrent updates.
|
|
|
|
|
StoredService::filter_by_service_key(&service.service_key)
|
|
|
|
|
.update()
|
|
|
|
|
.manifest_toml(service.manifest_toml)
|
|
|
|
|
.updated_at_ms(service.updated_at_ms)
|
|
|
|
|
.exec(&mut tx)
|
|
|
|
|
.await?;
|
|
|
|
|
} else {
|
|
|
|
|
StoredService::create()
|
|
|
|
|
.service_key(service.service_key)
|
|
|
|
|
.manifest_toml(service.manifest_toml)
|
|
|
|
|
.updated_at_ms(service.updated_at_ms)
|
|
|
|
|
.calls(0)
|
|
|
|
|
.errors(0)
|
|
|
|
|
.last_latency_ms(None)
|
|
|
|
|
.exec(&mut tx)
|
|
|
|
|
.await?;
|
|
|
|
|
}
|
2026-07-27 05:01:26 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(oldest_event) = events.last() {
|
|
|
|
|
StoredEvent::filter(StoredEvent::fields().id().lt(oldest_event.id))
|
|
|
|
|
.delete()
|
|
|
|
|
.exec(&mut tx)
|
|
|
|
|
.await?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for event in events {
|
|
|
|
|
StoredEvent::upsert_by_id(event.id)
|
|
|
|
|
.timestamp_ms(event.timestamp_ms)
|
|
|
|
|
.kind(event.kind)
|
|
|
|
|
.service_id(event.service_id)
|
|
|
|
|
.revision(event.revision)
|
|
|
|
|
.message(event.message)
|
|
|
|
|
.exec(&mut tx)
|
|
|
|
|
.await?;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-29 20:26:06 +08:00
|
|
|
let deployment_ids = deployments
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|deployment| deployment.service_id.clone())
|
|
|
|
|
.collect::<BTreeSet<_>>();
|
|
|
|
|
for stored in StoredDeployment::all().exec(&mut tx).await? {
|
|
|
|
|
if !deployment_ids.contains(&stored.service_id) {
|
|
|
|
|
StoredDeployment::delete_by_service_id(&mut tx, &stored.service_id).await?;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-29 16:32:12 +08:00
|
|
|
for deployment in deployments {
|
|
|
|
|
StoredDeployment::upsert_by_service_id(deployment.service_id)
|
|
|
|
|
.active_revision(deployment.active_revision)
|
|
|
|
|
.updated_at_ms(deployment.updated_at_ms)
|
|
|
|
|
.exec(&mut tx)
|
|
|
|
|
.await?;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-27 05:01:26 +08:00
|
|
|
tx.commit().await
|
|
|
|
|
}
|
2026-07-29 19:37:17 +08:00
|
|
|
|
2026-07-30 07:18:25 +08:00
|
|
|
/// Atomically records invocation deltas and their bounded event history.
|
|
|
|
|
pub(crate) async fn record_invocations(
|
|
|
|
|
&self,
|
|
|
|
|
updates: Vec<InvocationUpdate>,
|
|
|
|
|
) -> toasty::Result<()> {
|
|
|
|
|
let mut db = self.db.lock().await;
|
|
|
|
|
let mut tx = db.transaction().await?;
|
|
|
|
|
let mut newest_event_id = 0;
|
|
|
|
|
|
|
|
|
|
for update in updates {
|
|
|
|
|
if let Some(stored) = StoredService::filter_by_service_key(&update.service_key)
|
|
|
|
|
.first()
|
|
|
|
|
.exec(&mut tx)
|
|
|
|
|
.await?
|
|
|
|
|
{
|
|
|
|
|
StoredService::filter_by_service_key(&update.service_key)
|
|
|
|
|
.update()
|
|
|
|
|
.calls(stored.calls.saturating_add(1))
|
|
|
|
|
.errors(stored.errors.saturating_add(u64::from(update.failed)))
|
|
|
|
|
.updated_at_ms(update.updated_at_ms)
|
|
|
|
|
.last_latency_ms(Some(update.latency_ms))
|
|
|
|
|
.exec(&mut tx)
|
|
|
|
|
.await?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
newest_event_id = newest_event_id.max(update.event.id);
|
|
|
|
|
StoredEvent::upsert_by_id(update.event.id)
|
|
|
|
|
.timestamp_ms(update.event.timestamp_ms)
|
|
|
|
|
.kind(update.event.kind)
|
|
|
|
|
.service_id(update.event.service_id)
|
|
|
|
|
.revision(update.event.revision)
|
|
|
|
|
.message(update.event.message)
|
|
|
|
|
.exec(&mut tx)
|
|
|
|
|
.await?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let oldest_retained_event_id = newest_event_id.saturating_sub(255);
|
|
|
|
|
if oldest_retained_event_id > 0 {
|
|
|
|
|
StoredEvent::filter(StoredEvent::fields().id().lt(oldest_retained_event_id))
|
|
|
|
|
.delete()
|
|
|
|
|
.exec(&mut tx)
|
|
|
|
|
.await?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tx.commit().await
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-29 19:37:17 +08:00
|
|
|
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()
|
2026-07-27 05:01:26 +08:00
|
|
|
}
|