Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions doc/api/n-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ typedef enum {
napi_date_expected,
napi_arraybuffer_expected,
napi_detachable_arraybuffer_expected,
napi_would_deadlock,
} napi_status;
```

Expand Down Expand Up @@ -5095,6 +5096,19 @@ preventing data from being successfully added to the queue. If set to
`napi_call_threadsafe_function()` never blocks if the thread-safe function was
created with a maximum queue size of 0.

As a special case, when `napi_call_threadsafe_function()` is called from a
JavaScript 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
JavaScript thread is responsible for removing items from the queue, thereby
reducing their number. Thus, if it waits for room to become available on the
queue, then it will deadlock.

`napi_call_threadsafe_function()` will also return `napi_would_deadlock` if the
thread-safe function created on one JavaScript thread is called from another
JavaScript thread. The reason for this is to prevent a deadlock arising from the
possibility that the two JavaScript threads end up waiting on one another,
thereby both deadlocking.

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
value that was placed into the queue by a successful call to
Expand Down Expand Up @@ -5231,6 +5245,12 @@ This API may be called from any thread which makes use of `func`.
<!-- YAML
added: v10.6.0
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 or a worker thread and the queue is full.
-->

```C
Expand All @@ -5248,9 +5268,13 @@ napi_call_threadsafe_function(napi_threadsafe_function func,
`napi_tsfn_nonblocking` to indicate that the call should return immediately
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
called with `abort` set to `napi_tsfn_abort` from any thread. The value is only
added to the queue if the API returns `napi_ok`.
called with `abort` set to `napi_tsfn_abort` from any thread.

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

Expand Down
10 changes: 7 additions & 3 deletions src/js_native_api_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,15 @@ typedef enum {
napi_date_expected,
napi_arraybuffer_expected,
napi_detachable_arraybuffer_expected,
napi_would_deadlock
} napi_status;
// Note: when adding a new enum value to `napi_status`, please also update
// `const int last_status` in `napi_get_last_error_info()' definition,
// in file js_native_api_v8.cc. Please also update the definition of
// `napi_status` in doc/api/n-api.md to reflect the newly added value(s).
// * `const int last_status` in the definition of `napi_get_last_error_info()'
// in file js_native_api_v8.cc.
// * `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,
napi_callback_info info);
Expand Down
3 changes: 2 additions & 1 deletion src/js_native_api_v8.cc
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,7 @@ const char* error_messages[] = {nullptr,
"A date was expected",
"An arraybuffer was expected",
"A detachable arraybuffer was expected",
"Main thread would deadlock",
};

napi_status napi_get_last_error_info(napi_env env,
Expand All @@ -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.
// We don't have a napi_status_last as this would result in an ABI
// change each time a message was added.
const int last_status = napi_detachable_arraybuffer_expected;
const int last_status = napi_would_deadlock;

static_assert(
NAPI_ARRAYSIZE(error_messages) == last_status + 1,
Expand Down
19 changes: 19 additions & 0 deletions src/node_api.cc
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,25 @@ class ThreadSafeFunction : public node::AsyncResource {
if (mode == napi_tsfn_nonblocking) {
return napi_queue_full;
}

// Return `napi_would_deadlock` because waiting on the queue from a JS
// thread will block the event loop of that same thread thus preventing it
// from removing items from the queue thereby causing deadlock. Deadlock
// can also be caused by one JS thread calling the thread-safe function of
// another JS thread because if two JS threads call each other's thread-
// safe functions blockingly when both their queues are full they will
// both deadlock as they wait on each other.
//
// Thus, we return `napi_would_deadlock` if we find a Node.js event loop.
v8::Isolate* isolate = v8::Isolate::GetCurrent();
if (isolate != nullptr) {
node::Environment* node_env = node::Environment::GetCurrent(isolate);
if (node_env != nullptr) {
if (node_env->event_loop() != nullptr)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have GetCurrentEventLoop(isolate) which would give you the loop directly. That being said, if you have a Environment instance, you will also always have a event loop here, so the line that this comment is pointing to is currently redundant.

That being said, isn’t the question whether the event loop is the same event loop as env->node_env()->event_loop(), not whether an event loop is currently running at all?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@addaleax the question is whether a Node.js event loop is running at all, because we also wish to prevent making a blocking call from one event loop thread to another event loop thread, because such a call could result in mutual deadlock:

  1. JSThread1 calls JSThread2 and blocks while its queue is full and because JSThread2's queue is also full.
  2. JSThread2 calls JSThread1 before it's had a chance to remove an item from its own queue and blocks because JSThread1's queue is also full.

Now both threads are deadlocked.

It's already undesirable that a JS thread calls napi_call_threadsafe_function() blockingly, therefore IMO it should be OK if we're liberal about returning napi_would_deadlock.

return napi_would_deadlock;
}
}

cond->Wait(lock);
}

Expand Down
55 changes: 55 additions & 0 deletions test/node-api/test_threadsafe_function/binding.c
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,60 @@ static napi_value StartThreadNoJsFunc(napi_env env, napi_callback_info info) {
/** 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
static napi_value Init(napi_env env, napi_value exports) {
size_t index;
Expand Down Expand Up @@ -305,6 +359,7 @@ static napi_value Init(napi_env env, napi_value exports) {
DECLARE_NAPI_PROPERTY("StopThread", StopThread),
DECLARE_NAPI_PROPERTY("Unref", Unref),
DECLARE_NAPI_PROPERTY("Release", Release),
DECLARE_NAPI_PROPERTY("TestDeadlock", TestDeadlock),
};

NAPI_CALL(env, napi_define_properties(env, exports,
Expand Down
5 changes: 4 additions & 1 deletion test/node-api/test_threadsafe_function/binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
'targets': [
{
'target_name': 'binding',
'sources': ['binding.c']
'sources': [
'binding.c',
'../../js-native-api/common.c'
]
}
]
}
11 changes: 8 additions & 3 deletions test/node-api/test_threadsafe_function/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,13 @@ new Promise(function testWithoutJSMarshaller(resolve) {
}))
.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))

// Start a child process with an infinite queue to test rapid teardown
.then(() => testUnref(0));
// Start a child process with an infinite queue to test rapid teardown.
.then(() => testUnref(0))

// Test deadlock prevention.
.then(() => assert.deepStrictEqual(binding.TestDeadlock(), {
deadlockTest: 'Main thread would deadlock'
}));