deno_core icon indicating copy to clipboard operation
deno_core copied to clipboard

Multiple `JSRuntime` in a single tokio runtime

Open sahandevs opened this issue 10 months ago • 5 comments

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?

sahandevs avatar Apr 22 '24 08:04 sahandevs

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");
        });
    }

}

sahandevs avatar Apr 23 '24 07:04 sahandevs

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 avatar Jun 25 '24 08:06 hanrea

@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 avatar Jun 27 '24 08:06 sahandevs

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

hanrea avatar Jun 28 '24 04:06 hanrea

@sahandevs I have found an open-source project based on deno_core, which you can refer to: edge-runtime

hanrea avatar Jun 28 '24 04:06 hanrea