GeezerVison is showing github.com — n-api: detect deadlocks in thread-safe function · nodejs/node@3907de7
File treeExpand file treeCollapse file tree Open diff view settings test/node-api/test_threadsafe_function
Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/n-api.md +19-2Lines changed: 19 additions & 2 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -457,6 +457,7 @@ typedef enum { | 457 | 457 | napi_date_expected, | 458 | 458 | napi_arraybuffer_expected, | 459 | 459 | napi_detachable_arraybuffer_expected, | | 460 | + napi_would_deadlock, | 460 | 461 | } napi_status; | 461 | 462 | ``` | 462 | 463 |
| @@ -5095,6 +5096,12 @@ preventing data from being successfully added to the queue. If set to | 5095 | 5096 | `napi_call_threadsafe_function()` never blocks if the thread-safe function was | 5096 | 5097 | created with a maximum queue size of 0. | 5097 | 5098 |
| | 5099 | +As a special case, when `napi_call_threadsafe_function()` is called from the | | 5100 | +main thread, it will return `napi_would_deadlock` if the queue is full and it | | 5101 | +was called with `napi_tsfn_blocking`. The reason for this is that the main | | 5102 | +thread is responsible for reducing the number of items in the queue so, if it | | 5103 | +waits for room to become available on the queue, then it will deadlock. | | 5104 | + | 5098 | 5105 | The actual call into JavaScript is controlled by the callback given via the | 5099 | 5106 | `call_js_cb` parameter. `call_js_cb` is invoked on the main thread once for each | 5100 | 5107 | 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`. | 5231 | 5238 | <!-- YAML | 5232 | 5239 | added: v10.6.0 | 5233 | 5240 | napiVersion: 4 | | 5241 | +changes: | | 5242 | + - version: REPLACEME | | 5243 | + pr-url: https://github.com/nodejs/node/pull/32689 | | 5244 | + description: > | | 5245 | + Return `napi_would_deadlock` when called with `napi_tsfn_blocking` from | | 5246 | + the main thread and the queue is full. | 5234 | 5247 | --> | 5235 | 5248 |
| 5236 | 5249 | ```C | @@ -5248,9 +5261,13 @@ napi_call_threadsafe_function(napi_threadsafe_function func, | 5248 | 5261 | `napi_tsfn_nonblocking` to indicate that the call should return immediately | 5249 | 5262 | with a status of `napi_queue_full` whenever the queue is full. | 5250 | 5263 |
| | 5264 | +This API will return `napi_would_deadlock` if called with `napi_tsfn_blocking` | | 5265 | +from the main thread and the queue is full. | | 5266 | + | 5251 | 5267 | This API will return `napi_closing` if `napi_release_threadsafe_function()` was | 5252 | | -called with `abort` set to `napi_tsfn_abort` from any thread. The value is only | 5253 | | -added to the queue if the API returns `napi_ok`. | | 5268 | +called with `abort` set to `napi_tsfn_abort` from any thread. | | 5269 | + | | 5270 | +The value is only added to the queue if the API returns `napi_ok`. | 5254 | 5271 |
| 5255 | 5272 | This API may be called from any thread which makes use of `func`. | 5256 | 5273 |
| |
Collapse file Copy file name to clipboardExpand all lines: src/js_native_api_types.h +7-3Lines changed: 7 additions & 3 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -82,11 +82,15 @@ typedef enum { | 82 | 82 | napi_date_expected, | 83 | 83 | napi_arraybuffer_expected, | 84 | 84 | napi_detachable_arraybuffer_expected, | | 85 | + napi_would_deadlock | 85 | 86 | } napi_status; | 86 | 87 | // Note: when adding a new enum value to `napi_status`, please also update | 87 | | -// `const int last_status` in `napi_get_last_error_info()' definition, | 88 | | -// in file js_native_api_v8.cc. Please also update the definition of | 89 | | -// `napi_status` in doc/api/n-api.md to reflect the newly added value(s). | | 88 | +// * `const int last_status` in the definition of `napi_get_last_error_info()' | | 89 | +// in file js_native_api_v8.cc. | | 90 | +// * `const char* error_messages[]` in file js_native_api_v8.cc with a brief | | 91 | +// message explaining the error. | | 92 | +// * the definition of `napi_status` in doc/api/n-api.md to reflect the newly | | 93 | +// added value(s). | 90 | 94 |
| 91 | 95 | typedef napi_value (*napi_callback)(napi_env env, | 92 | 96 | napi_callback_info info); | |
Collapse file Copy file name to clipboardExpand all lines: src/js_native_api_v8.cc +2-1Lines changed: 2 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -740,6 +740,7 @@ const char* error_messages[] = {nullptr, | 740 | 740 | "A date was expected", | 741 | 741 | "An arraybuffer was expected", | 742 | 742 | "A detachable arraybuffer was expected", | | 743 | + "Main thread would deadlock", | 743 | 744 | }; | 744 | 745 |
| 745 | 746 | napi_status napi_get_last_error_info(napi_env env, | @@ -751,7 +752,7 @@ napi_status napi_get_last_error_info(napi_env env, | 751 | 752 | // message in the `napi_status` enum each time a new error message is added. | 752 | 753 | // We don't have a napi_status_last as this would result in an ABI | 753 | 754 | // change each time a message was added. | 754 | | - const int last_status = napi_detachable_arraybuffer_expected; | | 755 | + const int last_status = napi_would_deadlock; | 755 | 756 |
| 756 | 757 | static_assert( | 757 | 758 | NAPI_ARRAYSIZE(error_messages) == last_status + 1, | |
Collapse file Copy file name to clipboardExpand all lines: src/node_api.cc +5Lines changed: 5 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -129,6 +129,7 @@ class ThreadSafeFunction : public node::AsyncResource { | 129 | 129 | is_closing(false), | 130 | 130 | context(context_), | 131 | 131 | max_queue_size(max_queue_size_), | | 132 | + main_thread(uv_thread_self()), | 132 | 133 | env(env_), | 133 | 134 | finalize_data(finalize_data_), | 134 | 135 | finalize_cb(finalize_cb_), | @@ -148,12 +149,15 @@ class ThreadSafeFunction : public node::AsyncResource { | 148 | 149 |
| 149 | 150 | napi_status Push(void* data, napi_threadsafe_function_call_mode mode) { | 150 | 151 | node::Mutex::ScopedLock lock(this->mutex); | | 152 | + uv_thread_t current_thread = uv_thread_self(); | 151 | 153 |
| 152 | 154 | while (queue.size() >= max_queue_size && | 153 | 155 | max_queue_size > 0 && | 154 | 156 | !is_closing) { | 155 | 157 | if (mode == napi_tsfn_nonblocking) { | 156 | 158 | return napi_queue_full; | | 159 | + } else if (uv_thread_equal(¤t_thread, &main_thread)) { | | 160 | + return napi_would_deadlock; | 157 | 161 | } | 158 | 162 | cond->Wait(lock); | 159 | 163 | } | @@ -434,6 +438,7 @@ class ThreadSafeFunction : public node::AsyncResource { | 434 | 438 | // means we don't need the mutex to read them. | 435 | 439 | void* context; | 436 | 440 | size_t max_queue_size; | | 441 | + uv_thread_t main_thread; | 437 | 442 |
| 438 | 443 | // These are variables accessed only from the loop thread. | 439 | 444 | v8impl::Persistent<v8::Function> ref; | |
Collapse file Copy file name to clipboardExpand all lines: test/node-api/test_threadsafe_function/binding.c +55Lines changed: 55 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -267,6 +267,60 @@ static napi_value StartThreadNoJsFunc(napi_env env, napi_callback_info info) { | 267 | 267 | /** block_on_full */true, /** alt_ref_js_cb */true); | 268 | 268 | } | 269 | 269 |
| | 270 | +static void DeadlockTestDummyMarshaller(napi_env env, | | 271 | + napi_value empty0, | | 272 | + void* empty1, | | 273 | + void* empty2) {} | | 274 | + | | 275 | +static napi_value TestDeadlock(napi_env env, napi_callback_info info) { | | 276 | + napi_threadsafe_function tsfn; | | 277 | + napi_status status; | | 278 | + napi_value async_name; | | 279 | + napi_value return_value; | | 280 | + | | 281 | + // Create an object to store the returned information. | | 282 | + NAPI_CALL(env, napi_create_object(env, &return_value)); | | 283 | + | | 284 | + // Create a string to be used with the thread-safe function. | | 285 | + NAPI_CALL(env, napi_create_string_utf8(env, | | 286 | + "N-API Thread-safe Function Deadlock Test", | | 287 | + NAPI_AUTO_LENGTH, | | 288 | + &async_name)); | | 289 | + | | 290 | + // Create the thread-safe function with a single queue slot and a single thread. | | 291 | + NAPI_CALL(env, napi_create_threadsafe_function(env, | | 292 | + NULL, | | 293 | + NULL, | | 294 | + async_name, | | 295 | + 1, | | 296 | + 1, | | 297 | + NULL, | | 298 | + NULL, | | 299 | + NULL, | | 300 | + DeadlockTestDummyMarshaller, | | 301 | + &tsfn)); | | 302 | + | | 303 | + // Call the threadsafe function. This should succeed and fill the queue. | | 304 | + NAPI_CALL(env, napi_call_threadsafe_function(tsfn, NULL, napi_tsfn_blocking)); | | 305 | + | | 306 | + // Call the threadsafe function. This should not block, but return | | 307 | + // `napi_would_deadlock`. We save the resulting status in an object to be | | 308 | + // returned. | | 309 | + status = napi_call_threadsafe_function(tsfn, NULL, napi_tsfn_blocking); | | 310 | + add_returned_status(env, | | 311 | + "deadlockTest", | | 312 | + return_value, | | 313 | + "Main thread would deadlock", | | 314 | + napi_would_deadlock, | | 315 | + status); | | 316 | + | | 317 | + // Clean up the thread-safe function before returning. | | 318 | + NAPI_CALL(env, napi_release_threadsafe_function(tsfn, napi_tsfn_release)); | | 319 | + | | 320 | + // Return the result. | | 321 | + return return_value; | | 322 | +} | | 323 | + | 270 | 324 | // Module init | 271 | 325 | static napi_value Init(napi_env env, napi_value exports) { | 272 | 326 | size_t index; | @@ -305,6 +359,7 @@ static napi_value Init(napi_env env, napi_value exports) { | 305 | 359 | DECLARE_NAPI_PROPERTY("StopThread", StopThread), | 306 | 360 | DECLARE_NAPI_PROPERTY("Unref", Unref), | 307 | 361 | DECLARE_NAPI_PROPERTY("Release", Release), | | 362 | + DECLARE_NAPI_PROPERTY("TestDeadlock", TestDeadlock), | 308 | 363 | }; | 309 | 364 |
| 310 | 365 | NAPI_CALL(env, napi_define_properties(env, exports, | |
Collapse file Copy file name to clipboardExpand all lines: test/node-api/test_threadsafe_function/binding.gyp +4-1Lines changed: 4 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
| 2 | 2 | 'targets': [ | 3 | 3 | { | 4 | 4 | 'target_name': 'binding', | 5 | | - 'sources': ['binding.c'] | | 5 | + 'sources': [ | | 6 | + 'binding.c', | | 7 | + '../../js-native-api/common.c' | | 8 | + ] | 6 | 9 | } | 7 | 10 | ] | 8 | 11 | } |
Collapse file Copy file name to clipboardExpand all lines: test/node-api/test_threadsafe_function/test.js +8-3Lines changed: 8 additions & 3 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -210,8 +210,13 @@ new Promise(function testWithoutJSMarshaller(resolve) { | 210 | 210 | })) | 211 | 211 | .then((result) => assert.strictEqual(result.indexOf(0), -1)) | 212 | 212 |
| 213 | | -// Start a child process to test rapid teardown | | 213 | +// Start a child process to test rapid teardown. | 214 | 214 | .then(() => testUnref(binding.MAX_QUEUE_SIZE)) | 215 | 215 |
| 216 | | -// Start a child process with an infinite queue to test rapid teardown | 217 | | -.then(() => testUnref(0)); | | 216 | +// Start a child process with an infinite queue to test rapid teardown. | | 217 | +.then(() => testUnref(0)) | | 218 | + | | 219 | +// Test deadlock prevention. | | 220 | +.then(() => assert.deepStrictEqual(binding.TestDeadlock(), { | | 221 | + deadlockTest: 'Main thread would deadlock' | | 222 | +})); |
|