Permalink
Please
sign in to comment.
Browse files
worker,src: display remaining handles if `uv_loop_close` fails
Right now, we crash the process if there are handles remaining on the event loop when we exit (except for the main thread). This does not provide a lot of information about causes, though; in particular, we don’t show which handles are pending and who own them. This patch adds debug output to these cases to help with the situation. PR-URL: #21238 Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gus Caplan <me@gus.host> Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
- Loading branch information...
Showing
with
262 additions
and 15 deletions.
- +52 −7 src/debug_utils.cc
- +5 −0 src/debug_utils.h
- +5 −4 src/inspector_io.cc
- +2 −2 src/node_watchdog.cc
- +2 −1 src/node_worker.cc
- +2 −1 src/spawn_sync.cc
- +97 −0 test/abort/test-addon-uv-handle-leak.js
- +48 −0 test/addons/uv-handle-leak/binding.cc
- +9 −0 test/addons/uv-handle-leak/binding.gyp
- +23 −0 test/addons/uv-handle-leak/test.js
- +17 −0 test/common/index.js
| @@ -0,0 +1,97 @@ | |||
| // Flags: --experimental-worker | |||
| 'use strict'; | |||
| const common = require('../common'); | |||
| const assert = require('assert'); | |||
| const fs = require('fs'); | |||
| const path = require('path'); | |||
| const cp = require('child_process'); | |||
| const { Worker } = require('worker_threads'); | |||
|
|
|||
| // This is a sibling test to test/addons/uv-handle-leak. | |||
|
|
|||
| const bindingPath = path.resolve( | |||
| __dirname, '..', 'addons', 'uv-handle-leak', 'build', | |||
| `${common.buildType}/binding.node`); | |||
|
|
|||
| if (!fs.existsSync(bindingPath)) | |||
| common.skip('binding not built yet'); | |||
|
|
|||
| if (process.argv[2] === 'child') { | |||
| new Worker(` | |||
| const binding = require(${JSON.stringify(bindingPath)}); | |||
| binding.leakHandle(); | |||
| binding.leakHandle(0); | |||
| binding.leakHandle(0x42); | |||
| `, { eval: true }); | |||
| } else { | |||
| const child = cp.spawnSync(process.execPath, | |||
| ['--experimental-worker', __filename, 'child']); | |||
| const stderr = child.stderr.toString(); | |||
|
|
|||
| assert.strictEqual(child.stdout.toString(), ''); | |||
|
|
|||
| const lines = stderr.split('\n'); | |||
|
|
|||
| let state = 'initial'; | |||
|
|
|||
| // parse output that is formatted like this: | |||
|
|
|||
| // uv loop at [0x559b65ed5770] has active handles | |||
| // [0x7f2de0018430] timer | |||
| // Close callback: 0x7f2df31de220 CloseCallback(uv_handle_s*) [...] | |||
| // Data: 0x7f2df33df140 example_instance [...] | |||
| // (First field): 0x7f2df33dedc0 vtable for ExampleOwnerClass [...] | |||
| // [0x7f2de000b870] timer | |||
| // Close callback: 0x7f2df31de220 CloseCallback(uv_handle_s*) [...] | |||
| // Data: (nil) | |||
| // [0x7f2de000b910] timer | |||
| // Close callback: 0x7f2df31de220 CloseCallback(uv_handle_s*) [...] | |||
| // Data: 0x42 | |||
|
|
|||
| if (!(common.isFreeBSD || | |||
| common.isAIX || | |||
| (common.isLinux && !common.isGlibc()) || | |||
| common.isWindows)) { | |||
| assert(stderr.includes('ExampleOwnerClass'), stderr); | |||
| assert(stderr.includes('CloseCallback'), stderr); | |||
| assert(stderr.includes('example_instance'), stderr); | |||
| } | |||
|
|
|||
| while (lines.length > 0) { | |||
| const line = lines.shift().trim(); | |||
|
|
|||
| switch (state) { | |||
| case 'initial': | |||
| assert(/^uv loop at \[.+\] has active handles$/.test(line), line); | |||
| state = 'handle-start'; | |||
| break; | |||
| case 'handle-start': | |||
| if (/Assertion .+ failed/.test(line)) { | |||
| state = 'done'; | |||
| break; | |||
| } | |||
| assert(/^\[.+\] timer$/.test(line), line); | |||
| state = 'close-callback'; | |||
| break; | |||
| case 'close-callback': | |||
| assert(/^Close callback:/.test(line), line); | |||
| state = 'data'; | |||
| break; | |||
| case 'data': | |||
| assert(/^Data: .+$/.test(line), line); | |||
| state = 'maybe-first-field'; | |||
| break; | |||
| case 'maybe-first-field': | |||
| if (/^\(First field\)$/.test(line)) { | |||
| lines.unshift(line); | |||
| state = 'handle-start'; | |||
| break; | |||
| } | |||
| state = 'maybe-first-field'; | |||
| break; | |||
| case 'done': | |||
| break; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,48 @@ | |||
| #include <node.h> | |||
| #include <v8.h> | |||
| #include <uv.h> | |||
|
|
|||
| using v8::Context; | |||
| using v8::FunctionCallbackInfo; | |||
| using v8::Isolate; | |||
| using v8::Local; | |||
| using v8::Object; | |||
| using v8::Value; | |||
|
|
|||
| // Give these things names in the public namespace so that we can see | |||
| // them show up in symbol dumps. | |||
| void CloseCallback(uv_handle_t* handle) {} | |||
|
|
|||
| class ExampleOwnerClass { | |||
| public: | |||
| virtual ~ExampleOwnerClass() {} | |||
| }; | |||
|
|
|||
| ExampleOwnerClass example_instance; | |||
|
|
|||
| void LeakHandle(const FunctionCallbackInfo<Value>& args) { | |||
| Isolate* isolate = args.GetIsolate(); | |||
| Local<Context> context = isolate->GetCurrentContext(); | |||
| uv_loop_t* loop = node::GetCurrentEventLoop(isolate); | |||
| assert(loop != nullptr); | |||
|
|
|||
| uv_timer_t* leaked_timer = new uv_timer_t; | |||
| leaked_timer->close_cb = CloseCallback; | |||
|
|
|||
| if (args[0]->IsNumber()) { | |||
| leaked_timer->data = | |||
| reinterpret_cast<void*>(args[0]->IntegerValue(context).FromJust()); | |||
| } else { | |||
| leaked_timer->data = &example_instance; | |||
| } | |||
|
|
|||
| uv_timer_init(loop, leaked_timer); | |||
| uv_timer_start(leaked_timer, [](uv_timer_t*){}, 1000, 1000); | |||
| uv_unref(reinterpret_cast<uv_handle_t*>(leaked_timer)); | |||
| } | |||
|
|
|||
| void Initialize(v8::Local<v8::Object> exports) { | |||
| NODE_SET_METHOD(exports, "leakHandle", LeakHandle); | |||
| } | |||
|
|
|||
| NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize) | |||
| @@ -0,0 +1,9 @@ | |||
| { | |||
| 'targets': [ | |||
| { | |||
| 'target_name': 'binding', | |||
| 'defines': [ 'V8_DEPRECATION_WARNINGS=1' ], | |||
| 'sources': [ 'binding.cc' ] | |||
| } | |||
| ] | |||
| } | |||
| @@ -0,0 +1,23 @@ | |||
| 'use strict'; | |||
| const common = require('../../common'); | |||
| const bindingPath = require.resolve(`./build/${common.buildType}/binding`); | |||
| const binding = require(bindingPath); | |||
|
|
|||
| // This tests checks that addons may leak libuv handles until process exit. | |||
| // It’s really not a good idea to do so, but it tests existing behaviour | |||
| // that likely can never be removed for backwards compatibility. | |||
|
|
|||
| // This has a sibling test in test/abort/ which checks output for failures | |||
| // from workers. | |||
|
|
|||
| try { | |||
| // We don’t want to run this in Workers because they do actually enforce | |||
| // a clean-exit policy. | |||
| const { isMainThread } = require('worker_threads'); | |||
| if (!isMainThread) | |||
| common.skip('Cannot run test in environment with clean-exit policy'); | |||
| } catch {} | |||
|
|
|||
| binding.leakHandle(); | |||
| binding.leakHandle(0); | |||
| binding.leakHandle(1); | |||
Oops, something went wrong.
0 comments on commit
4a54ebc