Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

process: port on-exit-leak-free to core #53239

Merged
merged 20 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -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:
H4ad marked this conversation as resolved.
Show resolved Hide resolved
"""
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.
"""
213 changes: 213 additions & 0 deletions doc/api/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment on lines +1940 to +1943
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it guaranteed that V8 will always run the garbage collector before the process exits? It'd seem simpler not to.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, there's no guarantee when the GC will run

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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description here doesn’t seem very accurate. All closures can reference the context. The special thing about arrow function is that it captures the this binding so it can leak the this object in the closest non-arrow closure context if the arrow function closure is kept alive by a global registry.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some changes requested by Matteo, I thought it would be a good idea to document exactly how the user should use the feature and what they could do to accidentally hold the reference to the object.

What about:

Suggested change
Arrow functions will hold the previous context. Consider, for example:
Arrow functions will capture the `this` binding and possibly hold the reference to the `ref` object. 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() {}
}
```
mcollina marked this conversation as resolved.
Show resolved Hide resolved

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) {
Copy link
Member

@joyeecheung joyeecheung Jun 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's possible for the callback to get called twice if the object is still alive after the first callback invocation so it needs to consider re-entrancy.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like object.isDisposed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

callRefsToFree is called synchronously, so I don't think this function can be called twice since we have one instance per process.

Copy link
Member

@joyeecheung joyeecheung Jul 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't get to control when the finalization registry tasks are posted, and it can happen after we emit the process exit event to JS land

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you think it is worth, we can address it in a follow-up PR

// 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
Expand Down
20 changes: 20 additions & 0 deletions lib/internal/bootstrap/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading
Loading