n-api: detect deadlocks in thread-safe function
We introduce status `napi_would_deadlock` to be used as a return status by `napi_call_threadsafe_function` if the call is made with `napi_tsfn_blocking` on the main thread and the queue is full. PR-URL: https://github.com/nodejs/node/pull/32689 Fixes: https://github.com/nodejs/node/issues/32615 Signed-off-by: Gabriel Schulhof <gabriel.schulhof@intel.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: David Carlier <devnexen@gmail.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com> Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
This commit is contained in:
parent
b82d72c199
commit
aeb7084fe6
@ -457,6 +457,7 @@ typedef enum {
|
|||||||
napi_date_expected,
|
napi_date_expected,
|
||||||
napi_arraybuffer_expected,
|
napi_arraybuffer_expected,
|
||||||
napi_detachable_arraybuffer_expected,
|
napi_detachable_arraybuffer_expected,
|
||||||
|
napi_would_deadlock,
|
||||||
} napi_status;
|
} napi_status;
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -5095,6 +5096,12 @@ preventing data from being successfully added to the queue. If set to
|
|||||||
`napi_call_threadsafe_function()` never blocks if the thread-safe function was
|
`napi_call_threadsafe_function()` never blocks if the thread-safe function was
|
||||||
created with a maximum queue size of 0.
|
created with a maximum queue size of 0.
|
||||||
|
|
||||||
|
As a special case, when `napi_call_threadsafe_function()` is called from the
|
||||||
|
main thread, it will return `napi_would_deadlock` if the queue is full and it
|
||||||
|
was called with `napi_tsfn_blocking`. The reason for this is that the main
|
||||||
|
thread is responsible for reducing the number of items in the queue so, if it
|
||||||
|
waits for room to become available on the queue, then it will deadlock.
|
||||||
|
|
||||||
The actual call into JavaScript is controlled by the callback given via the
|
The actual call into JavaScript is controlled by the callback given via the
|
||||||
`call_js_cb` parameter. `call_js_cb` is invoked on the main thread once for each
|
`call_js_cb` parameter. `call_js_cb` is invoked on the main thread once for each
|
||||||
value that was placed into the queue by a successful call to
|
value that was placed into the queue by a successful call to
|
||||||
@ -5231,6 +5238,12 @@ This API may be called from any thread which makes use of `func`.
|
|||||||
<!-- YAML
|
<!-- YAML
|
||||||
added: v10.6.0
|
added: v10.6.0
|
||||||
napiVersion: 4
|
napiVersion: 4
|
||||||
|
changes:
|
||||||
|
- version: REPLACEME
|
||||||
|
pr-url: https://github.com/nodejs/node/pull/32689
|
||||||
|
description: >
|
||||||
|
Return `napi_would_deadlock` when called with `napi_tsfn_blocking` from
|
||||||
|
the main thread and the queue is full.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
```C
|
```C
|
||||||
@ -5248,9 +5261,13 @@ napi_call_threadsafe_function(napi_threadsafe_function func,
|
|||||||
`napi_tsfn_nonblocking` to indicate that the call should return immediately
|
`napi_tsfn_nonblocking` to indicate that the call should return immediately
|
||||||
with a status of `napi_queue_full` whenever the queue is full.
|
with a status of `napi_queue_full` whenever the queue is full.
|
||||||
|
|
||||||
|
This API will return `napi_would_deadlock` if called with `napi_tsfn_blocking`
|
||||||
|
from the main thread and the queue is full.
|
||||||
|
|
||||||
This API will return `napi_closing` if `napi_release_threadsafe_function()` was
|
This API will return `napi_closing` if `napi_release_threadsafe_function()` was
|
||||||
called with `abort` set to `napi_tsfn_abort` from any thread. The value is only
|
called with `abort` set to `napi_tsfn_abort` from any thread.
|
||||||
added to the queue if the API returns `napi_ok`.
|
|
||||||
|
The value is only added to the queue if the API returns `napi_ok`.
|
||||||
|
|
||||||
This API may be called from any thread which makes use of `func`.
|
This API may be called from any thread which makes use of `func`.
|
||||||
|
|
||||||
|
@ -82,11 +82,15 @@ typedef enum {
|
|||||||
napi_date_expected,
|
napi_date_expected,
|
||||||
napi_arraybuffer_expected,
|
napi_arraybuffer_expected,
|
||||||
napi_detachable_arraybuffer_expected,
|
napi_detachable_arraybuffer_expected,
|
||||||
|
napi_would_deadlock
|
||||||
} napi_status;
|
} napi_status;
|
||||||
// Note: when adding a new enum value to `napi_status`, please also update
|
// Note: when adding a new enum value to `napi_status`, please also update
|
||||||
// `const int last_status` in `napi_get_last_error_info()' definition,
|
// * `const int last_status` in the definition of `napi_get_last_error_info()'
|
||||||
// in file js_native_api_v8.cc. Please also update the definition of
|
// in file js_native_api_v8.cc.
|
||||||
// `napi_status` in doc/api/n-api.md to reflect the newly added value(s).
|
// * `const char* error_messages[]` in file js_native_api_v8.cc with a brief
|
||||||
|
// message explaining the error.
|
||||||
|
// * the definition of `napi_status` in doc/api/n-api.md to reflect the newly
|
||||||
|
// added value(s).
|
||||||
|
|
||||||
typedef napi_value (*napi_callback)(napi_env env,
|
typedef napi_value (*napi_callback)(napi_env env,
|
||||||
napi_callback_info info);
|
napi_callback_info info);
|
||||||
|
@ -740,6 +740,7 @@ const char* error_messages[] = {nullptr,
|
|||||||
"A date was expected",
|
"A date was expected",
|
||||||
"An arraybuffer was expected",
|
"An arraybuffer was expected",
|
||||||
"A detachable arraybuffer was expected",
|
"A detachable arraybuffer was expected",
|
||||||
|
"Main thread would deadlock",
|
||||||
};
|
};
|
||||||
|
|
||||||
napi_status napi_get_last_error_info(napi_env env,
|
napi_status napi_get_last_error_info(napi_env env,
|
||||||
@ -751,7 +752,7 @@ napi_status napi_get_last_error_info(napi_env env,
|
|||||||
// message in the `napi_status` enum each time a new error message is added.
|
// message in the `napi_status` enum each time a new error message is added.
|
||||||
// We don't have a napi_status_last as this would result in an ABI
|
// We don't have a napi_status_last as this would result in an ABI
|
||||||
// change each time a message was added.
|
// change each time a message was added.
|
||||||
const int last_status = napi_detachable_arraybuffer_expected;
|
const int last_status = napi_would_deadlock;
|
||||||
|
|
||||||
static_assert(
|
static_assert(
|
||||||
NAPI_ARRAYSIZE(error_messages) == last_status + 1,
|
NAPI_ARRAYSIZE(error_messages) == last_status + 1,
|
||||||
|
@ -129,6 +129,7 @@ class ThreadSafeFunction : public node::AsyncResource {
|
|||||||
is_closing(false),
|
is_closing(false),
|
||||||
context(context_),
|
context(context_),
|
||||||
max_queue_size(max_queue_size_),
|
max_queue_size(max_queue_size_),
|
||||||
|
main_thread(uv_thread_self()),
|
||||||
env(env_),
|
env(env_),
|
||||||
finalize_data(finalize_data_),
|
finalize_data(finalize_data_),
|
||||||
finalize_cb(finalize_cb_),
|
finalize_cb(finalize_cb_),
|
||||||
@ -148,12 +149,15 @@ class ThreadSafeFunction : public node::AsyncResource {
|
|||||||
|
|
||||||
napi_status Push(void* data, napi_threadsafe_function_call_mode mode) {
|
napi_status Push(void* data, napi_threadsafe_function_call_mode mode) {
|
||||||
node::Mutex::ScopedLock lock(this->mutex);
|
node::Mutex::ScopedLock lock(this->mutex);
|
||||||
|
uv_thread_t current_thread = uv_thread_self();
|
||||||
|
|
||||||
while (queue.size() >= max_queue_size &&
|
while (queue.size() >= max_queue_size &&
|
||||||
max_queue_size > 0 &&
|
max_queue_size > 0 &&
|
||||||
!is_closing) {
|
!is_closing) {
|
||||||
if (mode == napi_tsfn_nonblocking) {
|
if (mode == napi_tsfn_nonblocking) {
|
||||||
return napi_queue_full;
|
return napi_queue_full;
|
||||||
|
} else if (uv_thread_equal(¤t_thread, &main_thread)) {
|
||||||
|
return napi_would_deadlock;
|
||||||
}
|
}
|
||||||
cond->Wait(lock);
|
cond->Wait(lock);
|
||||||
}
|
}
|
||||||
@ -434,6 +438,7 @@ class ThreadSafeFunction : public node::AsyncResource {
|
|||||||
// means we don't need the mutex to read them.
|
// means we don't need the mutex to read them.
|
||||||
void* context;
|
void* context;
|
||||||
size_t max_queue_size;
|
size_t max_queue_size;
|
||||||
|
uv_thread_t main_thread;
|
||||||
|
|
||||||
// These are variables accessed only from the loop thread.
|
// These are variables accessed only from the loop thread.
|
||||||
v8impl::Persistent<v8::Function> ref;
|
v8impl::Persistent<v8::Function> ref;
|
||||||
|
@ -267,6 +267,60 @@ static napi_value StartThreadNoJsFunc(napi_env env, napi_callback_info info) {
|
|||||||
/** block_on_full */true, /** alt_ref_js_cb */true);
|
/** block_on_full */true, /** alt_ref_js_cb */true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void DeadlockTestDummyMarshaller(napi_env env,
|
||||||
|
napi_value empty0,
|
||||||
|
void* empty1,
|
||||||
|
void* empty2) {}
|
||||||
|
|
||||||
|
static napi_value TestDeadlock(napi_env env, napi_callback_info info) {
|
||||||
|
napi_threadsafe_function tsfn;
|
||||||
|
napi_status status;
|
||||||
|
napi_value async_name;
|
||||||
|
napi_value return_value;
|
||||||
|
|
||||||
|
// Create an object to store the returned information.
|
||||||
|
NAPI_CALL(env, napi_create_object(env, &return_value));
|
||||||
|
|
||||||
|
// Create a string to be used with the thread-safe function.
|
||||||
|
NAPI_CALL(env, napi_create_string_utf8(env,
|
||||||
|
"N-API Thread-safe Function Deadlock Test",
|
||||||
|
NAPI_AUTO_LENGTH,
|
||||||
|
&async_name));
|
||||||
|
|
||||||
|
// Create the thread-safe function with a single queue slot and a single thread.
|
||||||
|
NAPI_CALL(env, napi_create_threadsafe_function(env,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
async_name,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
DeadlockTestDummyMarshaller,
|
||||||
|
&tsfn));
|
||||||
|
|
||||||
|
// Call the threadsafe function. This should succeed and fill the queue.
|
||||||
|
NAPI_CALL(env, napi_call_threadsafe_function(tsfn, NULL, napi_tsfn_blocking));
|
||||||
|
|
||||||
|
// Call the threadsafe function. This should not block, but return
|
||||||
|
// `napi_would_deadlock`. We save the resulting status in an object to be
|
||||||
|
// returned.
|
||||||
|
status = napi_call_threadsafe_function(tsfn, NULL, napi_tsfn_blocking);
|
||||||
|
add_returned_status(env,
|
||||||
|
"deadlockTest",
|
||||||
|
return_value,
|
||||||
|
"Main thread would deadlock",
|
||||||
|
napi_would_deadlock,
|
||||||
|
status);
|
||||||
|
|
||||||
|
// Clean up the thread-safe function before returning.
|
||||||
|
NAPI_CALL(env, napi_release_threadsafe_function(tsfn, napi_tsfn_release));
|
||||||
|
|
||||||
|
// Return the result.
|
||||||
|
return return_value;
|
||||||
|
}
|
||||||
|
|
||||||
// Module init
|
// Module init
|
||||||
static napi_value Init(napi_env env, napi_value exports) {
|
static napi_value Init(napi_env env, napi_value exports) {
|
||||||
size_t index;
|
size_t index;
|
||||||
@ -305,6 +359,7 @@ static napi_value Init(napi_env env, napi_value exports) {
|
|||||||
DECLARE_NAPI_PROPERTY("StopThread", StopThread),
|
DECLARE_NAPI_PROPERTY("StopThread", StopThread),
|
||||||
DECLARE_NAPI_PROPERTY("Unref", Unref),
|
DECLARE_NAPI_PROPERTY("Unref", Unref),
|
||||||
DECLARE_NAPI_PROPERTY("Release", Release),
|
DECLARE_NAPI_PROPERTY("Release", Release),
|
||||||
|
DECLARE_NAPI_PROPERTY("TestDeadlock", TestDeadlock),
|
||||||
};
|
};
|
||||||
|
|
||||||
NAPI_CALL(env, napi_define_properties(env, exports,
|
NAPI_CALL(env, napi_define_properties(env, exports,
|
||||||
|
@ -2,7 +2,10 @@
|
|||||||
'targets': [
|
'targets': [
|
||||||
{
|
{
|
||||||
'target_name': 'binding',
|
'target_name': 'binding',
|
||||||
'sources': ['binding.c']
|
'sources': [
|
||||||
|
'binding.c',
|
||||||
|
'../../js-native-api/common.c'
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -210,8 +210,13 @@ new Promise(function testWithoutJSMarshaller(resolve) {
|
|||||||
}))
|
}))
|
||||||
.then((result) => assert.strictEqual(result.indexOf(0), -1))
|
.then((result) => assert.strictEqual(result.indexOf(0), -1))
|
||||||
|
|
||||||
// Start a child process to test rapid teardown
|
// Start a child process to test rapid teardown.
|
||||||
.then(() => testUnref(binding.MAX_QUEUE_SIZE))
|
.then(() => testUnref(binding.MAX_QUEUE_SIZE))
|
||||||
|
|
||||||
// Start a child process with an infinite queue to test rapid teardown
|
// Start a child process with an infinite queue to test rapid teardown.
|
||||||
.then(() => testUnref(0));
|
.then(() => testUnref(0))
|
||||||
|
|
||||||
|
// Test deadlock prevention.
|
||||||
|
.then(() => assert.deepStrictEqual(binding.TestDeadlock(), {
|
||||||
|
deadlockTest: 'Main thread would deadlock'
|
||||||
|
}));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user