feat(console): add deployment-routed gateway

- persist service-to-revision deployments in libSQL with an additive migration
- restore active actors and atomically switch revisions
- expose an isolated raw-byte gateway on a second listener with stable errors
- cover routing, isolation, revision switching, and restart restoration
This commit is contained in:
Maofeng
2026-07-29 16:32:12 +08:00
parent a872008d44
commit 1d3fac4be8
4 changed files with 762 additions and 43 deletions
+47 -6
View File
@@ -1,7 +1,8 @@
//! Toasty models and transactional libSQL snapshots for Console control data.
//!
//! Component bytes and WIT packages remain filesystem artifacts. This module
//! persists only service manifests, metrics, and the bounded event history.
//! persists only service manifests, deployments, metrics, and the bounded
//! event history.
use std::path::Path;
@@ -31,31 +32,62 @@ pub(crate) struct StoredEvent {
pub message: String,
}
#[derive(Clone, Debug, toasty::Model)]
pub(crate) struct StoredDeployment {
#[key]
pub service_id: String,
pub active_revision: String,
pub updated_at_ms: u64,
}
/// Serialized access to the Toasty database connection.
pub(crate) struct Persistence {
db: Mutex<Db>,
}
impl Persistence {
/// Opens the database and installs the schema for a new file.
/// Opens the database and installs or advances its schema.
pub async fn open(path: &Path) -> toasty::Result<Self> {
let is_new_database = !path.exists();
let db = Db::builder()
.models(toasty::models!(StoredService, StoredEvent))
.models(toasty::models!(
StoredService,
StoredEvent,
StoredDeployment
))
.build(Turso::file(path))
.await?;
if is_new_database {
db.push_schema().await?;
} 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?;
return Ok(Self { db: Mutex::new(db) });
}
Ok(Self { db: Mutex::new(db) })
}
/// Loads the complete service set and retained event history.
pub async fn load(&self) -> toasty::Result<(Vec<StoredService>, Vec<StoredEvent>)> {
/// Loads the complete service and deployment sets plus retained events.
pub async fn load(
&self,
) -> toasty::Result<(Vec<StoredService>, Vec<StoredEvent>, Vec<StoredDeployment>)> {
let mut db = self.db.lock().await;
let services = StoredService::all().exec(&mut *db).await?;
let events = StoredEvent::all().exec(&mut *db).await?;
Ok((services, events))
let deployments = StoredDeployment::all().exec(&mut *db).await?;
Ok((services, events, deployments))
}
/// Atomically upserts a Console snapshot and prunes expired events.
@@ -63,6 +95,7 @@ impl Persistence {
&self,
services: Vec<StoredService>,
events: Vec<StoredEvent>,
deployments: Vec<StoredDeployment>,
) -> toasty::Result<()> {
let mut db = self.db.lock().await;
let mut tx = db.transaction().await?;
@@ -96,6 +129,14 @@ impl Persistence {
.await?;
}
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?;
}
tx.commit().await
}
}