diff --git a/backend/tests/common/mod.rs b/backend/tests/common/mod.rs index 83e7f9931a..2c569537fa 100644 --- a/backend/tests/common/mod.rs +++ b/backend/tests/common/mod.rs @@ -313,6 +313,9 @@ pub fn spawn_test_worker( conn: &Connection, port: u16, ) -> (KillpillSender, tokio::task::JoinHandle<()>) { + #[cfg(feature = "deno_core")] + windmill_runtime_nativets::setup_deno_runtime().expect("V8 init failed"); + std::fs::DirBuilder::new() .recursive(true) .create(windmill_worker::GO_BIN_CACHE_DIR) diff --git a/backend/tests/nativets_jobs.rs b/backend/tests/nativets_jobs.rs index a8fbb483ed..c654882589 100644 --- a/backend/tests/nativets_jobs.rs +++ b/backend/tests/nativets_jobs.rs @@ -28,15 +28,6 @@ use windmill_common::jobs::{JobPayload, RawCode}; #[cfg(feature = "deno_core")] use windmill_common::scripts::ScriptLang; -#[cfg(feature = "deno_core")] -fn init_nativets_runtime() { - static INIT: std::sync::Once = std::sync::Once::new(); - INIT.call_once(|| { - let _ = rustls::crypto::ring::default_provider().install_default(); - windmill_runtime_nativets::setup_deno_runtime().expect("V8 init failed"); - }); -} - #[cfg(feature = "deno_core")] fn nativets_code(content: &str) -> JobPayload { JobPayload::Code(RawCode { @@ -82,7 +73,6 @@ async fn push_and_wait( #[sqlx::test(fixtures("base"))] async fn test_nativets_jobs(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; - init_nativets_runtime(); set_jwt_secret().await; let server = ApiServer::start(db.clone()).await?; diff --git a/backend/tests/nativets_stress.rs b/backend/tests/nativets_stress.rs index 896b5e8f3d..cf3710d86c 100644 --- a/backend/tests/nativets_stress.rs +++ b/backend/tests/nativets_stress.rs @@ -260,13 +260,6 @@ async fn test_parallel_nativets_stress(db: Pool) -> anyhow::Result<()> initialize_tracing().await; set_jwt_secret().await; - // V8 and rustls must be initialized before any JsRuntime is created - static RUNTIME_INIT: std::sync::Once = std::sync::Once::new(); - RUNTIME_INIT.call_once(|| { - let _ = rustls::crypto::ring::default_provider().install_default(); - windmill_runtime_nativets::setup_deno_runtime().expect("V8 init failed"); - }); - let server = ApiServer::start(db.clone()).await?; let port = server.addr.port(); let conn = Connection::Sql(db.clone()); diff --git a/backend/windmill-runtime-nativets/Cargo.toml b/backend/windmill-runtime-nativets/Cargo.toml index cb084016a8..417ec8de9d 100644 --- a/backend/windmill-runtime-nativets/Cargo.toml +++ b/backend/windmill-runtime-nativets/Cargo.toml @@ -46,6 +46,7 @@ lazy_static.workspace = true const_format.workspace = true futures.workspace = true sqlx.workspace = true +rustls.workspace = true [build-dependencies] deno_fetch.workspace = true diff --git a/backend/windmill-runtime-nativets/src/lib.rs b/backend/windmill-runtime-nativets/src/lib.rs index 30b30dd989..4f1f88daa8 100644 --- a/backend/windmill-runtime-nativets/src/lib.rs +++ b/backend/windmill-runtime-nativets/src/lib.rs @@ -156,28 +156,43 @@ lazy_static! { // ── Public interface ───────────────────────────────────────────────── -/// Set up the deno_core/V8 runtime. Must be called once before creating any JsRuntime. +/// Set up the deno_core/V8 runtime. Idempotent — safe to call multiple times. +/// Called automatically before JsRuntime creation, but can also be called +/// eagerly at startup for predictable initialization order. pub fn setup_deno_runtime() -> anyhow::Result<()> { - let unrecognized_v8_flags = deno_core::v8_set_flags(vec![ - "--stack-size=1024".to_string(), - "--no-harmony-import-assertions".to_string(), - ]) - .into_iter() - .skip(1) - .collect::>(); + use std::sync::Once; + static INIT: Once = Once::new(); - if !unrecognized_v8_flags.is_empty() { - println!("Unrecognized V8 flags: {:?}", unrecognized_v8_flags); + let mut init_err: Option = None; + INIT.call_once(|| { + // deno_fetch requires a TLS provider; install ring as default (idempotent). + let _ = rustls::crypto::ring::default_provider().install_default(); + + let unrecognized_v8_flags = deno_core::v8_set_flags(vec![ + "--stack-size=1024".to_string(), + "--no-harmony-import-assertions".to_string(), + ]) + .into_iter() + .skip(1) + .collect::>(); + + if !unrecognized_v8_flags.is_empty() { + init_err = Some(format!("Unrecognized V8 flags: {:?}", unrecognized_v8_flags)); + } + + // Use an unprotected platform that doesn't enforce thread-isolated allocations + // via Memory Protection Keys (pkeys). The default platform requires all V8-using + // threads to be descendants of the thread that called v8::Initialize, but tokio's + // spawn_blocking pool threads don't satisfy this. Without this, V8 crashes with + // SIGSEGV in WasmCodePointerTable::AllocateUninitializedEntry() on x86_64 Linux. + // See: https://github.com/denoland/deno_core/issues/952 + let platform = deno_core::v8::new_unprotected_default_platform(0, false).make_shared(); + deno_core::JsRuntime::init_platform(Some(platform), false); + }); + + if let Some(msg) = init_err { + println!("{msg}"); } - - // Use an unprotected platform that doesn't enforce thread-isolated allocations - // via Memory Protection Keys (pkeys). The default platform requires all V8-using - // threads to be descendants of the thread that called v8::Initialize, but tokio's - // spawn_blocking pool threads don't satisfy this. Without this, V8 crashes with - // SIGSEGV in WasmCodePointerTable::AllocateUninitializedEntry() on x86_64 Linux. - // See: https://github.com/denoland/deno_core/issues/952 - let platform = deno_core::v8::new_unprotected_default_platform(0, false).make_shared(); - deno_core::JsRuntime::init_platform(Some(platform), false); Ok(()) } @@ -464,6 +479,9 @@ pub async fn eval_fetch_timeout( let (memory_limit_tx, mut memory_limit_rx) = mpsc::unbounded_channel::<()>(); + // Ensure V8 platform is initialized (idempotent, no-op if already done). + setup_deno_runtime().expect("V8 platform init failed"); + // Serialize isolate creation as extra safety net against concurrent V8 // isolate creation races. The main fix is the unprotected platform in // setup_deno_runtime(), but this provides defense in depth.