Skip to content

Commit 91745fa

Browse files
authored
Disallow multiple threads using the same Context, even at different times (PetterS#4)
* Disallow multiple threads using the same Context, even at different times. * Bump version.
1 parent d4f1fda commit 91745fa

File tree

4 files changed

+95
-10
lines changed

4 files changed

+95
-10
lines changed

module.c

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -211,12 +211,20 @@ static PyObject *quickjs_to_python(ContextData *context_obj, JSValue value) {
211211
JSValue exception = JS_GetException(context);
212212
JSValue error_string = JS_ToString(context, exception);
213213
const char *cstring = JS_ToCString(context, error_string);
214-
if (strstr(cstring, "stack overflow") != NULL) {
215-
PyErr_Format(StackOverflow, "%s", cstring);
214+
if (cstring != NULL) {
215+
if (strstr(cstring, "stack overflow") != NULL) {
216+
PyErr_Format(StackOverflow, "%s", cstring);
217+
} else {
218+
PyErr_Format(JSException, "%s", cstring);
219+
}
220+
JS_FreeCString(context, cstring);
216221
} else {
217-
PyErr_Format(JSException, "%s", cstring);
222+
// This has been observed to happen when different threads have used the same QuickJS
223+
// runtime, but not at the same time.
224+
// Could potentially be another problem though, since JS_ToCString may return NULL.
225+
PyErr_Format(JSException,
226+
"(Failed obtaining QuickJS error string. Concurrency issue?)");
218227
}
219-
JS_FreeCString(context, cstring);
220228
JS_FreeValue(context, error_string);
221229
JS_FreeValue(context, exception);
222230
} else if (tag == JS_TAG_FLOAT64) {

quickjs/__init__.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import concurrent.futures
12
import json
23
import threading
4+
from typing import Tuple
35

46
import _quickjs
57

@@ -15,15 +17,33 @@ def test():
1517

1618

1719
class Function:
18-
def __init__(self, name: str, code: str) -> None:
19-
self._context = Context()
20-
self._context.eval(code)
21-
self._f = self._context.get(name)
20+
# There are unit tests demonstrating that we are crashing if different threads are accessing the
21+
# same runtime, even if it is not at the same time. So we run everything on the same thread in
22+
# order to prevent this.
23+
_threadpool = concurrent.futures.ThreadPoolExecutor(max_workers=1)
24+
25+
def __init__(self, name: str, code: str, *, own_executor=False) -> None:
26+
"""
27+
Arguments:
28+
name: The name of the function in the provided code that will be executed.
29+
code: The source code of the function and possibly helper functions, classes, global
30+
variables etc.
31+
own_executor: Create an executor specifically for this function. The default is False in
32+
order to save system resources if a large number of functions are created.
33+
"""
34+
if own_executor:
35+
self._threadpool = concurrent.futures.ThreadPoolExecutor(max_workers=1)
2236
self._lock = threading.Lock()
2337

38+
future = self._threadpool.submit(self._compile, name, code)
39+
concurrent.futures.wait([future])
40+
self._context, self._f = future.result()
41+
2442
def __call__(self, *args, run_gc=True):
2543
with self._lock:
26-
return self._call(*args, run_gc=run_gc)
44+
future = self._threadpool.submit(self._call, *args, run_gc=run_gc)
45+
concurrent.futures.wait([future])
46+
return future.result()
2747

2848
def set_memory_limit(self, limit):
2949
with self._lock:
@@ -49,6 +69,12 @@ def gc(self):
4969
with self._lock:
5070
self._context.gc()
5171

72+
def _compile(self, name: str, code: str) -> Tuple[Context, Object]:
73+
context = Context()
74+
context.eval(code)
75+
f = context.get(name)
76+
return context, f
77+
5278
def _call(self, *args, run_gc=True):
5379
def convert_arg(arg):
5480
if isinstance(arg, (type(None), str, bool, float, int)):

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def get_c_sources(include_headers=False):
4848
author_email="petter.strandmark@gmail.com",
4949
name='quickjs',
5050
url='https://github.com/PetterS/quickjs',
51-
version='1.5.0',
51+
version='1.5.1',
5252
description='Wrapping the quickjs C library.',
5353
long_description=long_description,
5454
packages=["quickjs"],

test_quickjs.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import concurrent.futures
12
import json
23
import unittest
34

@@ -340,3 +341,53 @@ def test_unicode(self):
340341
for x in ["äpple", "≤≥", "☺"]:
341342
self.assertEqual(identity(x), x)
342343
self.assertEqual(context.eval('(function(){ return "' + x + '";})()'), x)
344+
345+
346+
class Threads(unittest.TestCase):
347+
def setUp(self):
348+
self.context = quickjs.Context()
349+
self.executor = concurrent.futures.ThreadPoolExecutor()
350+
351+
def tearDown(self):
352+
self.executor.shutdown()
353+
354+
def test_concurrent(self):
355+
"""Demonstrates that the execution will crash unless the function executes on the same
356+
thread every time.
357+
358+
If the executor in Function is not present, this test will fail.
359+
"""
360+
data = list(range(1000))
361+
jssum = quickjs.Function(
362+
"sum", """
363+
function sum(data) {
364+
return data.reduce((a, b) => a + b, 0)
365+
}
366+
""")
367+
368+
futures = [self.executor.submit(jssum, data) for _ in range(10)]
369+
expected = sum(data)
370+
for future in concurrent.futures.as_completed(futures):
371+
self.assertEqual(future.result(), expected)
372+
373+
def test_concurrent_own_executor(self):
374+
data = list(range(1000))
375+
jssum1 = quickjs.Function("sum",
376+
"""
377+
function sum(data) {
378+
return data.reduce((a, b) => a + b, 0)
379+
}
380+
""",
381+
own_executor=True)
382+
jssum2 = quickjs.Function("sum",
383+
"""
384+
function sum(data) {
385+
return data.reduce((a, b) => a + b, 0)
386+
}
387+
""",
388+
own_executor=True)
389+
390+
futures = [self.executor.submit(f, data) for _ in range(10) for f in (jssum1, jssum2)]
391+
expected = sum(data)
392+
for future in concurrent.futures.as_completed(futures):
393+
self.assertEqual(future.result(), expected)

0 commit comments

Comments
 (0)