From 2287476ecc8cf503e2f137380e1645ef87124e84 Mon Sep 17 00:00:00 2001 From: Kim Altintop Date: Wed, 11 Mar 2026 22:43:32 +0100 Subject: [PATCH 1/3] core: Attempt to repair databases with wrong host type Prior to #4549 the host type in `st_module` was always set to wasm. We now correctly use the host type from the database, but the module may in fact be a JS module. So if launching it as a wasm module fails, try JS instead. If this succeeds, the module is definitely a JS module, so attempt to repair `st_module` in this case. --- crates/core/src/db/relational_db.rs | 2 +- crates/core/src/host/host_controller.rs | 105 +++++++++++++++++++----- crates/datastore/src/traits.rs | 1 + 3 files changed, 88 insertions(+), 20 deletions(-) diff --git a/crates/core/src/db/relational_db.rs b/crates/core/src/db/relational_db.rs index 9f0b992855b..294003548be 100644 --- a/crates/core/src/db/relational_db.rs +++ b/crates/core/src/db/relational_db.rs @@ -464,7 +464,7 @@ impl RelationalDB { /// The caller must ensure that: /// /// - `program.hash` is the [`Hash`] over `program.bytes`. - /// - `program.bytes` is a valid module acc. to `host_type`. + /// - `program.bytes` is a valid module acc. to `program.host_type`. /// - the schema updates contained in the module have been applied within /// the transactional context `tx`. /// - the `__init__` reducer contained in the module has been executed diff --git a/crates/core/src/host/host_controller.rs b/crates/core/src/host/host_controller.rs index 2fc41070ca4..9738de0f95f 100644 --- a/crates/core/src/host/host_controller.rs +++ b/crates/core/src/host/host_controller.rs @@ -30,6 +30,8 @@ use spacetimedb_data_structures::error_stream::ErrorStream; use spacetimedb_data_structures::map::{IntMap, IntSet}; use spacetimedb_datastore::db_metrics::data_size::DATA_SIZE_METRICS; use spacetimedb_datastore::db_metrics::DB_METRICS; +use spacetimedb_datastore::execution_context::Workload; +use spacetimedb_datastore::system_tables::ModuleKind; use spacetimedb_datastore::traits::Program; use spacetimedb_durability::{self as durability}; use spacetimedb_lib::{AlgebraicValue, Identity, Timestamp}; @@ -898,7 +900,6 @@ impl Host { bsatn_rlb_pool, .. } = host_controller; - let on_panic = host_controller.unregister_fn(replica_id); let replica_dir = data_dir.replica(replica_id); let (tx_metrics_queue, tx_metrics_recorder_task) = spawn_tx_metrics_recorder(); @@ -947,7 +948,7 @@ impl Host { (db, clients) } }; - let (program, program_needs_init) = match db.program()? { + let (mut program, program_needs_init) = match db.program()? { // Launch module with program from existing database. Some(program) => { info!( @@ -974,23 +975,89 @@ impl Host { } }; - let (program, launched) = ModuleLauncher { - database, - replica_id, - program, - on_panic, - relational_db: Arc::new(db), - energy_monitor: energy_monitor.clone(), - module_logs: match config.storage { - db::Storage::Memory => None, - db::Storage::Disk => Some(replica_dir.module_logs()), - }, - runtimes: runtimes.clone(), - core: host_controller.db_cores.take(), - bsatn_rlb_pool: bsatn_rlb_pool.clone(), - } - .launch_module() - .await?; + let relational_db = Arc::new(db); + let (program, launched) = match HostType::from(program.kind) { + HostType::Js => { + ModuleLauncher { + database, + replica_id, + program, + on_panic: host_controller.unregister_fn(replica_id), + relational_db, + energy_monitor: energy_monitor.clone(), + module_logs: match config.storage { + db::Storage::Memory => None, + db::Storage::Disk => Some(replica_dir.module_logs()), + }, + runtimes: runtimes.clone(), + core: host_controller.db_cores.take(), + bsatn_rlb_pool: bsatn_rlb_pool.clone(), + } + .launch_module() + .await? + } + HostType::Wasm => { + // Prior to https://github.com/clockworklabs/SpacetimeDB/pull/4549 + // the host type in `st_module` was always set to wasm. + // We now correctly use the host type from the database, but the + // module may in fact be a JS module. + // So if launching it as a wasm module fails, try JS instead. + // If this succeeds, the module is definitely a JS module, so + // attempt to repair `st_module` in this case. + // + // TODO: This code should eventually be removed once all + // databases have been repaired. + let launch_wasm_result = ModuleLauncher { + database: database.clone(), + replica_id, + program: program.clone(), + on_panic: host_controller.unregister_fn(replica_id), + relational_db: relational_db.clone(), + energy_monitor: energy_monitor.clone(), + module_logs: match config.storage { + db::Storage::Memory => None, + db::Storage::Disk => Some(replica_dir.clone().module_logs()), + }, + runtimes: runtimes.clone(), + core: host_controller.db_cores.take(), + bsatn_rlb_pool: bsatn_rlb_pool.clone(), + } + .launch_module() + .await; + match launch_wasm_result { + Ok(program_and_module_host) => program_and_module_host, + Err(e) => { + warn!("failed to launch wasm module, trying js: {e:#}"); + + program.kind = ModuleKind::JS; + let res = ModuleLauncher { + database, + replica_id, + program: program.clone(), + on_panic: host_controller.unregister_fn(replica_id), + relational_db: relational_db.clone(), + energy_monitor: energy_monitor.clone(), + module_logs: match config.storage { + db::Storage::Memory => None, + db::Storage::Disk => Some(replica_dir.module_logs()), + }, + runtimes: runtimes.clone(), + core: host_controller.db_cores.take(), + bsatn_rlb_pool: bsatn_rlb_pool.clone(), + } + .launch_module() + .await; + + if res.is_ok() { + let _ = relational_db + .with_auto_commit(Workload::Internal, |tx| relational_db.update_program(tx, program)); + } + + res? + } + } + } + }; if program_needs_init { let call_result = launched.module_host.init_database(program).await?; diff --git a/crates/datastore/src/traits.rs b/crates/datastore/src/traits.rs index 998cdb859e1..e1b99825ae2 100644 --- a/crates/datastore/src/traits.rs +++ b/crates/datastore/src/traits.rs @@ -500,6 +500,7 @@ pub struct Metadata { } /// Program associated with a database. +#[derive(Clone)] pub struct Program { /// Hash over the program's bytes. pub hash: Hash, From 2a0eb39092aa81daa25a6e67d1b59a46294d07b8 Mon Sep 17 00:00:00 2001 From: Kim Altintop Date: Thu, 12 Mar 2026 10:08:51 +0100 Subject: [PATCH 2/3] core: Fix unused import warning --- crates/core/src/db/relational_db.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/core/src/db/relational_db.rs b/crates/core/src/db/relational_db.rs index 294003548be..2819dbe366a 100644 --- a/crates/core/src/db/relational_db.rs +++ b/crates/core/src/db/relational_db.rs @@ -1,7 +1,6 @@ use crate::db::durability::DurabilityWorker; use crate::db::MetricsRecorderQueue; use crate::error::{DBError, RestoreSnapshotError}; -use crate::messages::control_db::HostType; use crate::subscription::ExecutionCounters; use crate::util::asyncify; use crate::worker_metrics::WORKER_METRICS; @@ -1861,6 +1860,7 @@ fn default_row_count_fn(db: Identity) -> RowCountFn { pub mod tests_utils { use crate::db::snapshot; use crate::db::snapshot::SnapshotWorker; + use crate::messages::control_db::HostType; use super::*; use core::ops::Deref; From 1db1a83fb047132ea674fc2c4c92dc50040f6362 Mon Sep 17 00:00:00 2001 From: Kim Altintop Date: Thu, 12 Mar 2026 10:09:14 +0100 Subject: [PATCH 3/3] smoketest: Add test for host type repair --- Cargo.lock | 1 + crates/smoketests/Cargo.toml | 1 + .../tests/smoketests/change_host_type.rs | 36 +++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 679805a4700..ad148688877 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8432,6 +8432,7 @@ dependencies = [ "predicates", "regex", "serde_json", + "spacetimedb-core", "spacetimedb-guard", "tempfile", "tokio", diff --git a/crates/smoketests/Cargo.toml b/crates/smoketests/Cargo.toml index 13ddab1a980..bfa718e7daa 100644 --- a/crates/smoketests/Cargo.toml +++ b/crates/smoketests/Cargo.toml @@ -15,6 +15,7 @@ anyhow.workspace = true which = "8.0.0" [dev-dependencies] +spacetimedb-core.workspace = true cargo_metadata.workspace = true assert_cmd = "2" predicates = "3" diff --git a/crates/smoketests/tests/smoketests/change_host_type.rs b/crates/smoketests/tests/smoketests/change_host_type.rs index 33efd036dd6..b72f86cdf04 100644 --- a/crates/smoketests/tests/smoketests/change_host_type.rs +++ b/crates/smoketests/tests/smoketests/change_host_type.rs @@ -1,3 +1,4 @@ +use spacetimedb::messages::control_db::HostType; use spacetimedb_smoketests::{require_local_server, require_pnpm, Smoketest}; const TS_MODULE_BASIC: &str = r#"import { schema, t, table } from "spacetimedb/server"; @@ -83,3 +84,38 @@ fn assert_has_rows(test: &Smoketest, names: &[&str], context: &str) { "{context}: expected all of {names:?} to be in result: {output}" ) } + +/// Tests that a legacy database that has a wrong host type in `st_module` is +/// auto-repaired upon startup. +/// +/// NOTE: The repair mechanism shall be removed eventually, and so shall this +/// test (which will fail when the mechanism is sunset). +/// +/// This test restarts the server. +#[test] +fn test_repair_host_type() { + require_pnpm!(); + require_local_server!(); + + let mut test = Smoketest::builder().autopublish(false).build(); + + test.publish_typescript_module_source("modules-basic-ts", "basic-ts", TS_MODULE_BASIC) + .unwrap(); + assert_host_type(&test, HostType::Js); + // Set the program kind to the wrong value. + test.sql_confirmed("update st_module set program_kind=0").unwrap(); + assert_host_type(&test, HostType::Wasm); + + // After restarting, the database both comes up and has the right host type. + test.restart_server(); + assert_host_type(&test, HostType::Js); +} + +fn assert_host_type(test: &Smoketest, host_type: HostType) { + let output = test.sql_confirmed("select program_kind from st_module").unwrap(); + let rows = output.lines().skip(2).map(|s| s.trim()).collect::>(); + match host_type { + HostType::Wasm => assert_eq!(&rows, &["0"]), + HostType::Js => assert_eq!(&rows, &["1"]), + } +}