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:
Vinicius Lourenço 2024-07-11 14:57:20 -03:00 committed by GitHub
parent c590117f1a
commit 05bb4a716b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 569 additions and 1 deletions

25
LICENSE
View file

@ -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.
"""

View file

@ -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

View file

@ -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;

View 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,
};

View file

@ -261,7 +261,6 @@ function wrapProcessMethods(binding) {
}
}
return {
_rawDebug,
cpuUsage,

31
test/fixtures/process/before-exit.mjs vendored Normal file
View 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
View 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)
})

View 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
View 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
View 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)
})

View 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}`);
}
});

View file

@ -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"