mirror of
https://github.com/nodejs/node.git
synced 2025-08-15 13:48:44 +02:00
process: port on-exit-leak-free to core
PR-URL: https://github.com/nodejs/node/pull/53239 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Yagiz Nizipli <yagiz.nizipli@sentry.io>
This commit is contained in:
parent
c590117f1a
commit
05bb4a716b
12 changed files with 569 additions and 1 deletions
25
LICENSE
25
LICENSE
|
@ -2378,3 +2378,28 @@ The externally maintained libraries used by Node.js are:
|
|||
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
- on-exit-leak-free, located at lib/internal/process/finalization, is licensed as follows:
|
||||
"""
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Matteo Collina
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
|
|
@ -1897,6 +1897,219 @@ a code.
|
|||
Specifying a code to [`process.exit(code)`][`process.exit()`] will override any
|
||||
previous setting of `process.exitCode`.
|
||||
|
||||
## `process.finalization.register(ref, callback)`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
> Stability: 1.1 - Active Development
|
||||
|
||||
* `ref` {Object | Function} The reference to the resource that is being tracked.
|
||||
* `callback` {Function} The callback function to be called when the resource
|
||||
is finalized.
|
||||
* `ref` {Object | Function} The reference to the resource that is being tracked.
|
||||
* `event` {string} The event that triggered the finalization. Defaults to 'exit'.
|
||||
|
||||
This function registers a callback to be called when the process emits the `exit`
|
||||
event if the `ref` object was not garbage collected. If the object `ref` was garbage collected
|
||||
before the `exit` event is emitted, the callback will be removed from the finalization registry,
|
||||
and it will not be called on process exit.
|
||||
|
||||
Inside the callback you can release the resources allocated by the `ref` object.
|
||||
Be aware that all limitations applied to the `beforeExit` event are also applied to the `callback` function,
|
||||
this means that there is a possibility that the callback will not be called under special circumstances.
|
||||
|
||||
The idea of this function is to help you free up resources when the starts process exiting,
|
||||
but also let the object be garbage collected if it is no longer being used.
|
||||
|
||||
Eg: you can register an object that contains a buffer, you want to make sure that buffer is released
|
||||
when the process exit, but if the object is garbage collected before the process exit, we no longer
|
||||
need to release the buffer, so in this case we just remove the callback from the finalization registry.
|
||||
|
||||
```cjs
|
||||
const { finalization } = require('node:process');
|
||||
|
||||
// Please make sure that the function passed to finalization.register()
|
||||
// does not create a closure around unnecessary objects.
|
||||
function onFinalize(obj, event) {
|
||||
// You can do whatever you want with the object
|
||||
obj.dispose();
|
||||
}
|
||||
|
||||
function setup() {
|
||||
// This object can be safely garbage collected,
|
||||
// and the resulting shutdown function will not be called.
|
||||
// There are no leaks.
|
||||
const myDisposableObject = {
|
||||
dispose() {
|
||||
// Free your resources synchronously
|
||||
},
|
||||
};
|
||||
|
||||
finalization.register(myDisposableObject, onFinalize);
|
||||
}
|
||||
|
||||
setup();
|
||||
```
|
||||
|
||||
```mjs
|
||||
import { finalization } from 'node:process';
|
||||
|
||||
// Please make sure that the function passed to finalization.register()
|
||||
// does not create a closure around unnecessary objects.
|
||||
function onFinalize(obj, event) {
|
||||
// You can do whatever you want with the object
|
||||
obj.dispose();
|
||||
}
|
||||
|
||||
function setup() {
|
||||
// This object can be safely garbage collected,
|
||||
// and the resulting shutdown function will not be called.
|
||||
// There are no leaks.
|
||||
const myDisposableObject = {
|
||||
dispose() {
|
||||
// Free your resources synchronously
|
||||
},
|
||||
};
|
||||
|
||||
finalization.register(myDisposableObject, onFinalize);
|
||||
}
|
||||
|
||||
setup();
|
||||
```
|
||||
|
||||
The code above relies on the following assumptions:
|
||||
|
||||
* arrow functions are avoided
|
||||
* regular functions are recommended to be within the global context (root)
|
||||
|
||||
Regular functions _could_ reference the context where the `obj` lives, making the `obj` not garbage collectible.
|
||||
|
||||
Arrow functions will hold the previous context. Consider, for example:
|
||||
|
||||
```js
|
||||
class Test {
|
||||
constructor() {
|
||||
finalization.register(this, (ref) => ref.dispose());
|
||||
|
||||
// even something like this is highly discouraged
|
||||
// finalization.register(this, () => this.dispose());
|
||||
}
|
||||
dispose() {}
|
||||
}
|
||||
```
|
||||
|
||||
It is very unlikely (not impossible) that this object will be garbage collected,
|
||||
but if it is not, `dispose` will be called when `process.exit` is called.
|
||||
|
||||
Be careful and avoid relying on this feature for the disposal of critical resources,
|
||||
as it is not guaranteed that the callback will be called under all circumstances.
|
||||
|
||||
## `process.finalization.registerBeforeExit(ref, callback)`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
> Stability: 1.1 - Active Development
|
||||
|
||||
* `ref` {Object | Function} The reference
|
||||
to the resource that is being tracked.
|
||||
* `callback` {Function} The callback function to be called when the resource
|
||||
is finalized.
|
||||
* `ref` {Object | Function} The reference to the resource that is being tracked.
|
||||
* `event` {string} The event that triggered the finalization. Defaults to 'beforeExit'.
|
||||
|
||||
This function behaves exactly like the `register`, except that the callback will be called
|
||||
when the process emits the `beforeExit` event if `ref` object was not garbage collected.
|
||||
|
||||
Be aware that all limitations applied to the `beforeExit` event are also applied to the `callback` function,
|
||||
this means that there is a possibility that the callback will not be called under special circumstances.
|
||||
|
||||
## `process.finalization.unregister(ref)`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
> Stability: 1.1 - Active Development
|
||||
|
||||
* `ref` {Object | Function} The reference
|
||||
to the resource that was registered previously.
|
||||
|
||||
This function remove the register of the object from the finalization
|
||||
registry, so the callback will not be called anymore.
|
||||
|
||||
```cjs
|
||||
const { finalization } = require('node:process');
|
||||
|
||||
// Please make sure that the function passed to finalization.register()
|
||||
// does not create a closure around unnecessary objects.
|
||||
function onFinalize(obj, event) {
|
||||
// You can do whatever you want with the object
|
||||
obj.dispose();
|
||||
}
|
||||
|
||||
function setup() {
|
||||
// This object can be safely garbage collected,
|
||||
// and the resulting shutdown function will not be called.
|
||||
// There are no leaks.
|
||||
const myDisposableObject = {
|
||||
dispose() {
|
||||
// Free your resources synchronously
|
||||
},
|
||||
};
|
||||
|
||||
finalization.register(myDisposableObject, onFinalize);
|
||||
|
||||
// Do something
|
||||
|
||||
myDisposableObject.dispose();
|
||||
finalization.unregister(myDisposableObject);
|
||||
}
|
||||
|
||||
setup();
|
||||
```
|
||||
|
||||
```mjs
|
||||
import { finalization } from 'node:process';
|
||||
|
||||
// Please make sure that the function passed to finalization.register()
|
||||
// does not create a closure around unnecessary objects.
|
||||
function onFinalize(obj, event) {
|
||||
// You can do whatever you want with the object
|
||||
obj.dispose();
|
||||
}
|
||||
|
||||
function setup() {
|
||||
// This object can be safely garbage collected,
|
||||
// and the resulting shutdown function will not be called.
|
||||
// There are no leaks.
|
||||
const myDisposableObject = {
|
||||
dispose() {
|
||||
// Free your resources synchronously
|
||||
},
|
||||
};
|
||||
|
||||
// Please make sure that the function passed to finalization.register()
|
||||
// does not create a closure around unnecessary objects.
|
||||
function onFinalize(obj, event) {
|
||||
// You can do whatever you want with the object
|
||||
obj.dispose();
|
||||
}
|
||||
|
||||
finalization.register(myDisposableObject, onFinalize);
|
||||
|
||||
// Do something
|
||||
|
||||
myDisposableObject.dispose();
|
||||
finalization.unregister(myDisposableObject);
|
||||
}
|
||||
|
||||
setup();
|
||||
```
|
||||
|
||||
## `process.getActiveResourcesInfo()`
|
||||
|
||||
<!-- YAML
|
||||
|
|
|
@ -182,6 +182,26 @@ const rawMethods = internalBinding('process_methods');
|
|||
process.kill = wrapped.kill;
|
||||
process.exit = wrapped.exit;
|
||||
|
||||
let finalizationMod;
|
||||
ObjectDefineProperty(process, 'finalization', {
|
||||
__proto__: null,
|
||||
get() {
|
||||
if (finalizationMod !== undefined) {
|
||||
return finalizationMod;
|
||||
}
|
||||
|
||||
const { createFinalization } = require('internal/process/finalization');
|
||||
finalizationMod = createFinalization();
|
||||
|
||||
return finalizationMod;
|
||||
},
|
||||
set(value) {
|
||||
finalizationMod = value;
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
process.hrtime = perThreadSetup.hrtime;
|
||||
process.hrtime.bigint = perThreadSetup.hrtimeBigInt;
|
||||
|
||||
|
|
150
lib/internal/process/finalization.js
Normal file
150
lib/internal/process/finalization.js
Normal file
|
@ -0,0 +1,150 @@
|
|||
'use strict';
|
||||
// This file is a modified version of the on-exit-leak-free module on npm.
|
||||
|
||||
const {
|
||||
ArrayPrototypeFilter,
|
||||
ArrayPrototypeIndexOf,
|
||||
ArrayPrototypePush,
|
||||
ArrayPrototypeSplice,
|
||||
SafeFinalizationRegistry,
|
||||
SafeWeakRef,
|
||||
} = primordials;
|
||||
const { validateObject, kValidateObjectAllowFunction } = require('internal/validators');
|
||||
const { emitExperimentalWarning } = require('internal/util');
|
||||
|
||||
function createFinalization() {
|
||||
/**
|
||||
* @type {SafeFinalizationRegistry}
|
||||
*/
|
||||
let registry = null;
|
||||
|
||||
const refs = {
|
||||
__proto__: null,
|
||||
exit: [],
|
||||
beforeExit: [],
|
||||
};
|
||||
|
||||
const functions = {
|
||||
__proto__: null,
|
||||
exit: onExit,
|
||||
beforeExit: onBeforeExit,
|
||||
};
|
||||
|
||||
function install(event) {
|
||||
if (refs[event].length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
process.on(event, functions[event]);
|
||||
}
|
||||
|
||||
function uninstall(event) {
|
||||
if (refs[event].length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
process.removeListener(event, functions[event]);
|
||||
|
||||
if (refs.exit.length === 0 && refs.beforeExit.length === 0) {
|
||||
registry = null;
|
||||
}
|
||||
}
|
||||
|
||||
function onExit() {
|
||||
callRefsToFree('exit');
|
||||
}
|
||||
|
||||
function onBeforeExit() {
|
||||
callRefsToFree('beforeExit');
|
||||
}
|
||||
|
||||
function callRefsToFree(event) {
|
||||
for (const ref of refs[event]) {
|
||||
const obj = ref.deref();
|
||||
const fn = ref.fn;
|
||||
|
||||
// This should always happen, however GC is
|
||||
// undeterministic so it might not happen.
|
||||
/* istanbul ignore else */
|
||||
if (obj !== undefined) {
|
||||
fn(obj, event);
|
||||
}
|
||||
}
|
||||
refs[event] = [];
|
||||
}
|
||||
|
||||
function clear(ref) {
|
||||
for (const event of ['exit', 'beforeExit']) {
|
||||
const index = ArrayPrototypeIndexOf(refs[event], ref);
|
||||
ArrayPrototypeSplice(refs[event], index, index + 1);
|
||||
uninstall(event);
|
||||
}
|
||||
}
|
||||
|
||||
function _register(event, obj, fn) {
|
||||
install(event);
|
||||
|
||||
const ref = new SafeWeakRef(obj);
|
||||
ref.fn = fn;
|
||||
|
||||
registry ||= new SafeFinalizationRegistry(clear);
|
||||
registry.register(obj, ref);
|
||||
|
||||
ArrayPrototypePush(refs[event], ref);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the given function when the process exits,
|
||||
* and clean things up when the object is gc.
|
||||
* @param {any} obj
|
||||
* @param {Function} fn
|
||||
*/
|
||||
function register(obj, fn) {
|
||||
emitExperimentalWarning('process.finalization.register');
|
||||
validateObject(obj, 'obj', kValidateObjectAllowFunction);
|
||||
|
||||
_register('exit', obj, fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the given function before the process exits,
|
||||
* and clean things up when the object is gc.
|
||||
* @param {any} obj
|
||||
* @param {Function} fn
|
||||
*/
|
||||
function registerBeforeExit(obj, fn) {
|
||||
emitExperimentalWarning('process.finalization.registerBeforeExit');
|
||||
validateObject(obj, 'obj', kValidateObjectAllowFunction);
|
||||
|
||||
_register('beforeExit', obj, fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister the given object from the onExit or onBeforeExit event.
|
||||
* @param {object} obj
|
||||
*/
|
||||
function unregister(obj) {
|
||||
emitExperimentalWarning('process.finalization.unregister');
|
||||
if (!registry) {
|
||||
return;
|
||||
}
|
||||
registry.unregister(obj);
|
||||
for (const event of ['exit', 'beforeExit']) {
|
||||
refs[event] = ArrayPrototypeFilter(refs[event], (ref) => {
|
||||
const _obj = ref.deref();
|
||||
return _obj && _obj !== obj;
|
||||
});
|
||||
uninstall(event);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
register,
|
||||
registerBeforeExit,
|
||||
unregister,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createFinalization,
|
||||
};
|
|
@ -261,7 +261,6 @@ function wrapProcessMethods(binding) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
_rawDebug,
|
||||
cpuUsage,
|
||||
|
|
31
test/fixtures/process/before-exit.mjs
vendored
Normal file
31
test/fixtures/process/before-exit.mjs
vendored
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { strictEqual } from 'assert'
|
||||
|
||||
function setup() {
|
||||
const obj = { foo: 'bar' }
|
||||
process.finalization.registerBeforeExit(obj, shutdown)
|
||||
}
|
||||
|
||||
let shutdownCalled = false
|
||||
let timeoutFinished = false
|
||||
|
||||
function shutdown(obj, event) {
|
||||
shutdownCalled = true
|
||||
if (event === 'beforeExit') {
|
||||
setTimeout(function () {
|
||||
timeoutFinished = true
|
||||
strictEqual(obj.foo, 'bar')
|
||||
process.finalization.unregister(obj)
|
||||
}, 100)
|
||||
process.on('beforeExit', function () {
|
||||
strictEqual(timeoutFinished, true)
|
||||
})
|
||||
} else {
|
||||
throw new Error(`different event, expected beforeExit but got ${event}`)
|
||||
}
|
||||
}
|
||||
|
||||
setup()
|
||||
|
||||
process.on('exit', function () {
|
||||
strictEqual(shutdownCalled, true)
|
||||
})
|
18
test/fixtures/process/close.mjs
vendored
Normal file
18
test/fixtures/process/close.mjs
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { strictEqual } from 'assert'
|
||||
|
||||
function setup() {
|
||||
const obj = { foo: 'bar' }
|
||||
process.finalization.register(obj, shutdown)
|
||||
}
|
||||
|
||||
let shutdownCalled = false
|
||||
function shutdown(obj) {
|
||||
shutdownCalled = true
|
||||
strictEqual(obj.foo, 'bar')
|
||||
}
|
||||
|
||||
setup()
|
||||
|
||||
process.on('exit', function () {
|
||||
strictEqual(shutdownCalled, true)
|
||||
})
|
15
test/fixtures/process/different-registry-per-thread.mjs
vendored
Normal file
15
test/fixtures/process/different-registry-per-thread.mjs
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { isMainThread, Worker } from 'node:worker_threads';
|
||||
|
||||
if (isMainThread) {
|
||||
process.finalization.register({ foo: 'foo' }, () => {
|
||||
process.stdout.write('shutdown on main thread\n');
|
||||
});
|
||||
|
||||
const worker = new Worker(import.meta.filename);
|
||||
|
||||
worker.postMessage('ping');
|
||||
} else {
|
||||
process.finalization.register({ foo: 'bar' }, () => {
|
||||
process.stdout.write('shutdown on worker\n');
|
||||
});
|
||||
}
|
21
test/fixtures/process/gc-not-close.mjs
vendored
Normal file
21
test/fixtures/process/gc-not-close.mjs
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { strictEqual } from 'assert'
|
||||
|
||||
function setup() {
|
||||
let obj = { foo: 'bar' }
|
||||
process.finalization.register(obj, shutdown)
|
||||
setImmediate(function () {
|
||||
obj = undefined
|
||||
gc()
|
||||
})
|
||||
}
|
||||
|
||||
let shutdownCalled = false
|
||||
function shutdown(obj) {
|
||||
shutdownCalled = true
|
||||
}
|
||||
|
||||
setup()
|
||||
|
||||
process.on('exit', function () {
|
||||
strictEqual(shutdownCalled, false)
|
||||
})
|
21
test/fixtures/process/unregister.mjs
vendored
Normal file
21
test/fixtures/process/unregister.mjs
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { strictEqual } from 'assert'
|
||||
|
||||
function setup() {
|
||||
const obj = { foo: 'bar' }
|
||||
process.finalization.register(obj, shutdown)
|
||||
setImmediate(function () {
|
||||
process.finalization.unregister(obj)
|
||||
process.finalization.unregister(obj) // twice, this should not throw
|
||||
})
|
||||
}
|
||||
|
||||
let shutdownCalled = false
|
||||
function shutdown(obj) {
|
||||
shutdownCalled = true
|
||||
}
|
||||
|
||||
setup()
|
||||
|
||||
process.on('exit', function () {
|
||||
strictEqual(shutdownCalled, false)
|
||||
})
|
52
test/parallel/test-process-finalization.mjs
Normal file
52
test/parallel/test-process-finalization.mjs
Normal file
|
@ -0,0 +1,52 @@
|
|||
import '../common/index.mjs';
|
||||
import { spawnSyncAndAssert } from '../common/child_process.js';
|
||||
import fixtures from '../common/fixtures.js';
|
||||
|
||||
import { it } from 'node:test';
|
||||
|
||||
import assert from 'assert';
|
||||
|
||||
const files = [
|
||||
'close.mjs',
|
||||
'before-exit.mjs',
|
||||
'gc-not-close.mjs',
|
||||
'unregister.mjs',
|
||||
'different-registry-per-thread.mjs',
|
||||
];
|
||||
|
||||
for (const file of files) {
|
||||
it(`should exit file ${file} with code=0`, () => {
|
||||
spawnSyncAndAssert(process.execPath, ['--expose-gc', `${file}`], {
|
||||
cwd: fixtures.path('process'),
|
||||
}, {
|
||||
code: 0,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it('register is different per thread', () => {
|
||||
spawnSyncAndAssert(process.execPath, ['--expose-gc', 'different-registry-per-thread.mjs'], {
|
||||
cwd: fixtures.path('process'),
|
||||
}, {
|
||||
code: 0,
|
||||
stdout: 'shutdown on worker\nshutdown on main thread\n',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw when register undefined value', () => {
|
||||
try {
|
||||
process.finalization.register(undefined);
|
||||
|
||||
assert.fail('Expected an error to be thrown for registerFreeOnExit');
|
||||
} catch (e) {
|
||||
assert.ok(e.message.includes('must be of type object'), `Expected error message to include 'Invalid' but got: ${e.message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
process.finalization.registerBeforeExit(undefined);
|
||||
|
||||
assert.fail('Expected an error to be thrown for registerFreeOnBeforeExit');
|
||||
} catch (e) {
|
||||
assert.ok(e.message.includes('must be of type object'), `Expected error message to include 'Invalid' but got: ${e.message}`);
|
||||
}
|
||||
});
|
|
@ -150,4 +150,7 @@ addlicense "nghttp3" "deps/ngtcp2/nghttp3/" "$licenseText"
|
|||
licenseText="$(curl -sL https://raw.githubusercontent.com/jprichardson/node-fs-extra/b34da2762a4865b025cac06d02d6a2f1f1027b65/LICENSE)"
|
||||
addlicense "node-fs-extra" "lib/internal/fs/cp" "$licenseText"
|
||||
|
||||
licenseText="$(curl -sL https://raw.githubusercontent.com/mcollina/on-exit-leak-free/2a01c7e66c690aca17187b10b0cecbe43e083eb2/LICENSE)"
|
||||
addlicense "on-exit-leak-free" "lib/internal/process/finalization" "$licenseText"
|
||||
|
||||
mv "$tmplicense" "$licensefile"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue