deno_core
deno_core copied to clipboard
Multiple `JSRuntime` in a single tokio runtime
I was experimenting with deno
and deno_core
to implement a runtime similar to what workerd has. Basically for different script it creates different isolates.
At first i tried creating a single tokio runtime and create worker like this:
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Creating tokio runtime for Runner");
localset.block_on(&rt, async move {
for script in scripts {
let worker = Worker::new(script); // which creates MainWorker
localset.spawn_local(move async {
worker.start().await; // which runs the event loop
});
}
// ...
wait_for_messages()
})
it works fine while running or adding new scripts, but if the MainWorker
drops, i get the following segfault:
x00005b5de44db978 in v8::internal::Isolate::Exit() () at ../../../../v8/src/execution/isolate.cc:4705
4705 ../../../../v8/src/execution/isolate.cc: No such file or directory.
(gdb) bt
#0 0x00005b5de44db978 in v8::internal::Isolate::Exit() () at ../../../../v8/src/execution/isolate.cc:4705
#1 0x00005b5de444514d in v8::Isolate::New(v8::Isolate::CreateParams const&) () at ../../../../v8/src/api/api.cc:9519
#2 0x00005b5de440b06a in v8::isolate::Isolate::new (params=...) at src/isolate.rs:576
#3 0x00005b5de41b9166 in deno_core::runtime::jsruntime::JsRuntime::new_inner (options=..., will_snapshot=false, maybe_load_callback=...) at runtime/jsruntime.rs:635
#4 0x00005b5de41b738f in deno_core::runtime::jsruntime::JsRuntime::new (options=...) at runtime/jsruntime.rs:458
#5 0x00005b5de22cad03 in deno_runtime::worker::MainWorker::from_options (main_module=..., permissions=..., options=...) at worker.rs:322
#6 0x00005b5de22c8df5 in deno_runtime::worker::MainWorker::bootstrap_from_options (main_module=..., permissions=..., options=...) at worker.rs:193
#7 0x00005b5de1b5bbad in workers_server::worker::Worker::create_worker (config=0x7441bfffe0a0, definition=...) at src/workers/workers-server/src/worker/mod.rs:192
#8 0x00005b5de1b59fd2 in workers_server::worker::Worker::new (config=0x7441bfffe0a0, definition=<error reading variable: Cannot access memory at address 0x74477c2b0239>)
at src/workers/workers-server/src/worker/mod.rs:37
and the output of the debug version of v8 is:
#
# Fatal error in ../../../../v8/src/execution/isolate.cc, line 4693
# Debug check failed: CurrentPerIsolateThreadData()->isolate_ == this.
#
#
#
#FailureMessage Object: 0x719dc7ffbbb0
==== C stack trace ===============================
after some investigation i saw the comment in deno code that "we enter isolates on creation" and "exit on drop". I assumed this would conflict a lot with the setup i had so i decided to create a new thread per script with the following setup:
std::thread::Builder::new().spawn(move || {
let rt: tokio::runtime::Runtime =
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Creating tokio runtime for Runner");
let localset = tokio::task::LocalSet::new();
localset.block_on(&rt, async move {
let w = match Worker::new(def, rx).unwrap();
w.start(shutdown_signal_clone).await.unwrap();
});
);
which resulted in the following segfault:
0x00006056c2a950a5 in v8::internal::wasm::WasmCodeManager::HasMemoryProtectionKeySupport() () at ../../../../v8/src/wasm/wasm-code-manager.cc:2067
2067 ../../../../v8/src/wasm/wasm-code-manager.cc: No such file or directory.
(gdb) bt
#0 0x00006056c2a950a5 in v8::internal::wasm::WasmCodeManager::HasMemoryProtectionKeySupport() () at ../../../../v8/src/wasm/wasm-code-manager.cc:2067
#1 0x00006056c22e5f5f in v8::internal::(anonymous namespace)::Invoke(v8::internal::Isolate*, v8::internal::(anonymous namespace)::InvokeParams const&) ()
at ../../../../v8/src/execution/execution.cc:285
#2 0x00006056c22e7939 in v8::internal::Execution::CallScript(v8::internal::Isolate*, v8::internal::Handle<v8::internal::JSFunction>, v8::internal::Handle<v8::internal::Object>, v8::internal::Handle<v8::internal::Object>) () at ../../../../v8/src/execution/execution.cc:540
#3 0x00006056c218a941 in v8::Script::Run(v8::Local<v8::Context>, v8::Local<v8::Data>) () at ../../../../v8/src/api/api.cc:2320
#4 0x00006056c20f7814 in v8__Script__Run () at ../../../../src/binding.cc:2341
#5 0x00006056c200df03 in v8::script::{impl#0}::run::{closure#0} (sd=0x77ff1c0a7a80) at /root/.cargo/registry/src/index.crates.io-6f17d22bba15001f/v8-0.74.3/src/script.rs:96
#6 0x00006056c1eeed3a in v8::scope::HandleScope<()>::cast_local<v8::data::Value, v8::script::{impl#0}::run::{closure_env#0}> (self=0x77ff225eeca8, f=...)
at /root/.cargo/registry/src/index.crates.io-6f17d22bba15001f/v8-0.74.3/src/scope.rs:239
#7 v8::data::Script::run (self=0x77ff1c088e08, scope=0x77ff225eeca8) at /root/.cargo/registry/src/index.crates.io-6f17d22bba15001f/v8-0.74.3/src/script.rs:96
#8 0x00006056c1d4348a in deno_core::runtime::bindings::initialize_context (scope=0x77ff225eeca8, context=..., op_ctxs=..., init_mode=deno_core::runtime::jsruntime::InitMode::FromSnapshot)
at runtime/bindings.rs:147
#9 0x00006056c1e931d0 in deno_core::runtime::jsruntime::JsRuntime::new_inner (options=..., will_snapshot=false, maybe_load_callback=...) at runtime/jsruntime.rs:680
#10 0x00006056c1e9021f in deno_core::runtime::jsruntime::JsRuntime::new (options=...) at runtime/jsruntime.rs:458
after some trial and error i've discovered adding unsafe { V8::dispose() };
before creating the worker fixes the issue. but reading the V8::dispose
and seeing the use of static i feel I shouldn't do this.
Now for my main question! I was wondering if such setups like creating multiple isolates and using them in the process or even better in the same tokio runtime/thread is possible in deno?
I've prepared a minimal reproducible example:
new Promise(async r => {
let i = 0;
while (r < 100) {
await Deno.core.opAsync('random_number');
i += 1;
}
r();
});
use deno_core::{extension, op2, PollEventLoopOptions};
use deno_core::{JsRuntime, RuntimeOptions};
use rand::Rng;
use tokio::runtime::{self, Runtime};
#[op2(async)]
async fn random_number() -> i32 {
let r: i32 = rand::random();
tokio::time::sleep(std::time::Duration::from_millis(rand::thread_rng().gen_range(0..100))).await;
r
}
extension! {
utils,
ops = [random_number]
}
const SCRIPT: &'static str = include_str!("./script.js");
fn main() {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<usize>();
for id in 0..100 {
std::thread::spawn(move || {
let rt = runtime::Builder::new_current_thread().enable_all().build().unwrap();
rt.block_on(async {
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![utils::init_ops()],
..RuntimeOptions::default()
});
println!("[{id}] before execute_script");
runtime.execute_script("lol", SCRIPT).unwrap();
println!("[{id}] after execute_script");
runtime.run_event_loop(Default::default()).await.unwrap();
println!("[{id}] after run_event_loop");
});
println!("[{id}] dropping");
});
}
}
I have only been studying Deno for a short time and have similar ideas as you. Do you know how to implement this issue ?
@hanrea I didn't get how your problem relates to mine. can you elaborate?
if you want to implement something like Deno.cwd()
, you can get the cwd using getcwd
and to integrate it with Deno use #[op2]
and pass it as an extension.
@sahandevs Thank you, I found that the runtime and opstate of the worker are globally shared, while 'Deno. cwd()' references the global opstate. The main process of deno itself is also a worker, so you can implement your requirements from the worker level.
@sahandevs I have found an open-source project based on deno_core, which you can refer to: edge-runtime