Skip to content

Commit

Permalink
fix: Make sure no more than 1 Runtime exist per thread
Browse files Browse the repository at this point in the history
  • Loading branch information
ubolonton committed Feb 2, 2024
1 parent d6cf390 commit 0d49192
Show file tree
Hide file tree
Showing 2 changed files with 42 additions and 6 deletions.
23 changes: 17 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::cell::RefCell;
use std::rc::Rc;

use deno_core::{FsModuleLoader, JsRuntime, ModuleCode, ModuleId, ModuleSpecifier, RuntimeOptions};
Expand All @@ -11,21 +12,29 @@ mod types;

/// A wrapper around deno_core's JsRuntime.
///
/// Objects of this class can only be used from the thread they were created on.
/// Instances of this class can only be used from the thread they were created on.
/// If they are sent to another thread, they will panic when used.
///
/// Each thread should create only one Runtime object.
/// It is possible to create more, but that's not very useful.
/// Each thread is associated with at most one instance. After the constructor is called once,
/// subsequent calls on the same thread return the same instance.
#[pyclass(unsendable, module = "denopy")]
struct Runtime {
js_runtime: JsRuntime,
tokio_runtime: tokio::runtime::Runtime,
}

thread_local! {
static RUNTIME: RefCell<Option<Py<Runtime>>> = RefCell::new(None);
}

#[pymethods]
impl Runtime {
#[new]
fn new() -> PyResult<Self> {
fn new(py: Python<'_>) -> PyResult<Py<Self>> {
if let Some(runtime) = RUNTIME.with(|r| r.borrow().clone()) {
return Ok(runtime);
}

// TODO: Figure out what happens if this is called from a thread that is not a child of the thread where the
// module was loaded.
let js_runtime = JsRuntime::new(RuntimeOptions {
Expand All @@ -34,7 +43,9 @@ impl Runtime {
});
let tokio_runtime = tokio::runtime::Builder::new_current_thread()
.max_blocking_threads(1).enable_all().build()?;
Ok(Self { js_runtime, tokio_runtime })
let runtime = Py::new(py, Self { js_runtime, tokio_runtime })?;
RUNTIME.with(|r| r.borrow_mut().replace(runtime.clone()));
Ok(runtime)
}

fn eval(&mut self, py: Python<'_>, source_code: &str) -> PyResult<PyObject> {
Expand Down Expand Up @@ -66,7 +77,7 @@ impl Runtime {
})
}

#[pyo3(signature = (function, *args))]
#[pyo3(signature = (function, * args))]
fn call(&mut self, py: Python<'_>, function: &JsFunction, args: &PyTuple) -> PyResult<PyObject> {
let args = {
let scope = &mut self.js_runtime.handle_scope();
Expand Down
25 changes: 25 additions & 0 deletions tests/test_threading.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import threading

import denopy


Expand Down Expand Up @@ -25,3 +27,26 @@ def test_not_segfault_on_many_runtime_objects():
# - Is assigned to a variable, but the variable is then deleted: `del r`.
# - Is added directly to a list without a variable: `runtimes.append(denopy.Runtime())`.
r = denopy.Runtime()


def test_one_thread_per_runtime():
runtime = denopy.Runtime()
result = {}

def _eval():
try:
runtime.eval("1")
except BaseException as e:
result['error'] = e

thread = threading.Thread(target=_eval)
thread.start()
thread.join()
# TODO: Figure out a way to get the exception type.
assert "Runtime is unsendable, but sent to another thread" in str(result['error'])


def test_one_runtime_per_thread():
runtime1 = denopy.Runtime()
runtime2 = denopy.Runtime()
assert runtime2 is runtime1

0 comments on commit 0d49192

Please sign in to comment.