fix: make V8 runtime init idempotent and auto-initialize before isolate creation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-02-08 14:27:53 +00:00
parent ccc4806b9a
commit aa9f3da429
5 changed files with 41 additions and 36 deletions

View File

@@ -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)

View File

@@ -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<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
init_nativets_runtime();
set_jwt_secret().await;
let server = ApiServer::start(db.clone()).await?;

View File

@@ -260,13 +260,6 @@ async fn test_parallel_nativets_stress(db: Pool<Postgres>) -> 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());

View File

@@ -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

View File

@@ -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::<Vec<_>>();
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<String> = 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::<Vec<_>>();
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.