Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple JSRuntime in a single tokio runtime #708

Open
sahandevs opened this issue Apr 22, 2024 · 5 comments
Open

Multiple JSRuntime in a single tokio runtime #708

sahandevs opened this issue Apr 22, 2024 · 5 comments

Comments

@sahandevs
Copy link

sahandevs commented Apr 22, 2024

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
Copy link
Author

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

}

@hanrea
Copy link

hanrea commented Jun 25, 2024

I have only been studying Deno for a short time and have similar ideas as you. Do you know how to implement this issue ?

@sahandevs
Copy link
Author

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

@hanrea
Copy link

hanrea commented Jun 28, 2024

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

hanrea commented Jun 28, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants