diagnostics_channel: replace using with try-finally#64251
diagnostics_channel: replace using with try-finally#64251ayush23chaudhary wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR removes the experimental using syntax from lib/diagnostics_channel.js, replacing it with equivalent try...finally disposal to avoid crashes when Node is run with --no-js-explicit-resource-management.
Changes:
- Replaced
usingstatements with explicittry...finallyblocks that invoke[SymbolDispose](). - Updated scope handling in
ActiveChannel,BoundedChannel, andTracingChannelpaths to ensure disposal occurs reliably without relying on flagged syntax.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| triggerUncaughtException(err, false); | ||
| }); | ||
| continue; | ||
| const stack = new DisposableStack(); |
There was a problem hiding this comment.
DisposableStack is also a flagged global.
There was a problem hiding this comment.
@Renegade334, I did not realize DisposableStack was part of the same flagged feature. I've replaced it with a standard array that manually iterates in reverse during disposal. Just pushed the update.
| } finally { | ||
| if (this.#stack === undefined) { | ||
| for (let i = stack.length - 1; i >= 0; i--) { | ||
| stack[i][SymbolDispose](); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
#stack is not disposed by the constructor. This code should be removed, and the try ... finally block in the constructor is not needed.
| for (let i = this.#stack.length - 1; i >= 0; i--) { | ||
| this.#stack[i][SymbolDispose](); | ||
| } | ||
| this.#stack = undefined; |
There was a problem hiding this comment.
| for (let i = this.#stack.length - 1; i >= 0; i--) { | |
| this.#stack[i][SymbolDispose](); | |
| } | |
| this.#stack = undefined; | |
| const stack = this.#stack; | |
| this.#stack = undefined; | |
| for (let i = stack.length - 1; i >= 0; i--) { | |
| stack[i][SymbolDispose](); | |
| } |
Matches the order of operations of DisposableStack.prototype.dispose(), in case anything touches the stack during disposal.
There was a problem hiding this comment.
Can any of the store disposers ever throw? If so, we need error handling here as well.
548c1c1 to
1d66ae6
Compare
|
Thanks for the thorough review, @Renegade334 and @anonrig!
|
Renegade334
left a comment
There was a problem hiding this comment.
Thanks for your changes! Some more comments 👍
| try { | ||
| const result = ReflectApply(fn, thisArg, args); | ||
| context.result = result; | ||
| return result; | ||
| } catch (err) { | ||
| context.error = err; | ||
| error.publish(context); | ||
| throw err; | ||
| } | ||
| } finally { | ||
| scope[SymbolDispose](); |
There was a problem hiding this comment.
There's no need to nest this, it can just be try ... catch ... finally.
| try { | |
| const result = ReflectApply(fn, thisArg, args); | |
| context.result = result; | |
| return result; | |
| } catch (err) { | |
| context.error = err; | |
| error.publish(context); | |
| throw err; | |
| } | |
| } finally { | |
| scope[SymbolDispose](); | |
| const result = ReflectApply(fn, thisArg, args); | |
| context.result = result; | |
| return result; | |
| } catch (err) { | |
| context.error = err; | |
| error.publish(context); | |
| throw err; | |
| } finally { | |
| scope[SymbolDispose](); |
| try { | ||
| // TODO: Is there a way to have asyncEnd _after_ the continuation? | ||
| } finally { | ||
| scope[SymbolDispose](); | ||
| } |
There was a problem hiding this comment.
No need for an empty block here.
| try { | |
| // TODO: Is there a way to have asyncEnd _after_ the continuation? | |
| } finally { | |
| scope[SymbolDispose](); | |
| } | |
| // TODO: Is there a way to have asyncEnd _after_ the continuation? | |
| scope[SymbolDispose](); |
| try { | ||
| const result = ReflectApply(fn, thisArg, args); | ||
| // If the return value is not a thenable, return it directly with a warning. | ||
| // Do not publish to asyncStart/asyncEnd. | ||
| if (typeof result?.then !== 'function') { | ||
| emitNonThenableWarning(fn); | ||
| context.result = result; | ||
| return result; | ||
| } | ||
| // isPromise() matches sub-classes, but we need to match only direct | ||
| // instances of the native Promise type to safely use PromisePrototypeThen. | ||
| if (isPromise(result) && ObjectGetPrototypeOf(result) === PromisePrototype) { | ||
| return PromisePrototypeThen(result, onResolve, onRejectWithRethrow); | ||
| } | ||
| // For non-native thenables, subscribe to the result but return the | ||
| // original thenable so the consumer can continue handling it directly. | ||
| // Non-native thenables don't have unhandledRejection tracking, so | ||
| // swallowing the rejection here doesn't change existing behaviour. | ||
| result.then(onResolve, onReject); | ||
| return result; | ||
| } catch (err) { | ||
| context.error = err; | ||
| error.publish(context); | ||
| throw err; | ||
| } |
There was a problem hiding this comment.
Same here.
| try { | |
| const result = ReflectApply(fn, thisArg, args); | |
| // If the return value is not a thenable, return it directly with a warning. | |
| // Do not publish to asyncStart/asyncEnd. | |
| if (typeof result?.then !== 'function') { | |
| emitNonThenableWarning(fn); | |
| context.result = result; | |
| return result; | |
| } | |
| // isPromise() matches sub-classes, but we need to match only direct | |
| // instances of the native Promise type to safely use PromisePrototypeThen. | |
| if (isPromise(result) && ObjectGetPrototypeOf(result) === PromisePrototype) { | |
| return PromisePrototypeThen(result, onResolve, onRejectWithRethrow); | |
| } | |
| // For non-native thenables, subscribe to the result but return the | |
| // original thenable so the consumer can continue handling it directly. | |
| // Non-native thenables don't have unhandledRejection tracking, so | |
| // swallowing the rejection here doesn't change existing behaviour. | |
| result.then(onResolve, onReject); | |
| return result; | |
| } catch (err) { | |
| context.error = err; | |
| error.publish(context); | |
| throw err; | |
| } | |
| const result = ReflectApply(fn, thisArg, args); | |
| // If the return value is not a thenable, return it directly with a warning. | |
| // Do not publish to asyncStart/asyncEnd. | |
| if (typeof result?.then !== 'function') { | |
| emitNonThenableWarning(fn); | |
| context.result = result; | |
| return result; | |
| } | |
| // isPromise() matches sub-classes, but we need to match only direct | |
| // instances of the native Promise type to safely use PromisePrototypeThen. | |
| if (isPromise(result) && ObjectGetPrototypeOf(result) === PromisePrototype) { | |
| return PromisePrototypeThen(result, onResolve, onRejectWithRethrow); | |
| } | |
| // For non-native thenables, subscribe to the result but return the | |
| // original thenable so the consumer can continue handling it directly. | |
| // Non-native thenables don't have unhandledRejection tracking, so | |
| // swallowing the rejection here doesn't change existing behaviour. | |
| result.then(onResolve, onReject); | |
| return result; | |
| } catch (err) { | |
| context.error = err; | |
| error.publish(context); | |
| throw err; |
| try { | ||
| return ReflectApply(fn, thisArg, args); | ||
| } catch (err) { | ||
| context.error = err; | ||
| error.publish(context); | ||
| throw err; | ||
| } | ||
| } finally { | ||
| scope[SymbolDispose](); |
There was a problem hiding this comment.
Same here.
| try { | |
| return ReflectApply(fn, thisArg, args); | |
| } catch (err) { | |
| context.error = err; | |
| error.publish(context); | |
| throw err; | |
| } | |
| } finally { | |
| scope[SymbolDispose](); | |
| return ReflectApply(fn, thisArg, args); | |
| } catch (err) { | |
| context.error = err; | |
| error.publish(context); | |
| throw err; | |
| } finally { | |
| scope[SymbolDispose](); |
| const child = spawnSync(process.execPath, [ | ||
| '--no-js-explicit-resource-management', | ||
| '--eval', | ||
| 'require("diagnostics_channel").tracingChannel("foo").traceSync(() => {})' | ||
| ]); |
There was a problem hiding this comment.
The test runner can handle this abstraction for you. Just place a line like the following at the top of the script:
// Flags: --no-js-explicit-resource-managementand the test script will be run under those runtime flags, without needing to spawn a new process. See https://github.com/nodejs/node/blob/main/test/parallel/test-permission-has.js for an example.
| const errors = []; | ||
| for (let i = stack.length - 1; i >= 0; i--) { | ||
| try { | ||
| stack[i][SymbolDispose](); | ||
| } catch (error) { | ||
| ArrayPrototypePush(errors, error); | ||
| } | ||
| } | ||
|
|
||
| if (errors.length === 0) { | ||
| return; | ||
| } else if (errors.length === 1) { | ||
| throw errors[0]; | ||
| } else { | ||
| throw new NodeAggregateError(errors); | ||
| } | ||
| } |
There was a problem hiding this comment.
I've had a look, and all of the scope disposers should never throw synchronously (they wrap their code in their own try ... catch blocks and emit errors asynchronously). As such, I don't think we need to implement this, and we can use your original version without the error handling.
Fixes: #64230
Description
This PR replaces instances of the experimental
usingsyntax withtry...finallyblocks insidelib/diagnostics_channel.js.Since Explicit Resource Management is still a flagged feature in V8, encountering
usingin core causes a segfault when running Node with the--no-js-explicit-resource-managementflag. This refactor maintains the exact same resource cleanup semantics by manually calling[SymbolDispose]()in afinallyblock, avoiding the flagged syntax entirely.Example of Refactor
Before:
After