Skip to content
This repository has been archived by the owner on Jan 25, 2022. It is now read-only.

Explainer: Clarify when the finalization callback is called #148

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ All that said, sometimes finalizers are the right answer to a problem. The foll

Finalizers can locate external resource leaks. For example, if an open file is garbage collected, the underlying operating system resource could be leaked. Although the OS will likely free the resources when the process exits, this sort of leak could make long-running processes eventually exhaust the number of file handles available. To catch these bugs, a `FinalizationGroup` can be used to log the existence of file objects which are garbage collected before being closed.

The `FinalizationGroup` class represents a group of objects registered with a common finalizer callback. This construct can be used to inform the developer about the never-closed files.
The `FinalizationGroup` class represents a group of objects sharing common finalization logic.
A finalizer callback is invoked after any of the objects registered with the group have been garbage collected.
This construct can be used to inform the developer about the never-closed files.

```js
class FileStream {
Expand Down Expand Up @@ -126,7 +128,8 @@ This example shows usage of the whole `FinalizationGroup` API:
- The object whose lifetime we're concerned with. Here, that's `this`, the `FileStream` object.
- A “holdings” value, which is used to represent that object when cleaning it up in the finalizer. Here, the holdings are the underlying `File` object.
- An unregistration token, which is passed to the `unregister` method when the finalizer is no longer needed. Here we use `this`, the `FileStream` object itself, since `FinalizationGroup` doesn't hold a strong reference to the unregister token.
- The `FinalizationGroup` constructor is called with a callback as an argument. This callback is called with an iterator of the holdings values.
- The `FinalizationGroup` constructor is called with a callback as an argument. This callback is called after a garbage collection occurred and at least one registered object was collected. The finalizer callback is called with an iterator of the holdings values for the registered objects that were collected.\
Copy link
Member

Choose a reason for hiding this comment

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

- The `FinalizationGroup` constructor is called with a callback as an argument.
+ The `FinalizationGroup` constructor must be called with a finalizer callback as an argument.
- This callback is called after a garbage collection occurred and at least one registered object was collected.
+ This callback may be called after a garbage collection occurred and at least one registered object was collected.
- The finalizer callback is called with an iterator of the holdings values for the registered objects that were collected.
+ The finalizer callback is called with a dynamic iterator of the holdings values for each of the collected objects registered in the corresponding instance.

Is the \ at the end intentional?

Copy link
Member

Choose a reason for hiding this comment

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

the part were it says the callback may be called after a gc reflects the current normative text as we can't assert if the callback will be called. This reflects an important aspect of the feature as a consecutive GC can't trigger the callback anymore if the object was collected before, in any previous GC.

Copy link
Member Author

Choose a reason for hiding this comment

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

For a collecting engine, if an object registered with the FinalizationGroup is collected, the callback will be called, except if cleanupSome is called between collection and scheduled callback invocation, and the iterator is consumed (or if process is terminated, of course).

I don't think consecutive GC changes this. The callback will be invoked once.
Now we can probably clarify that the callback is invoked only if an object is newly collected, not for previously collected objects which holdings were not consumed. I don't think "may be called" clarifies that enough. Maybe replacing was collected by has just been collected would help?

We could probably clarify the interaction of iterators and collection executions, adding something like this to the following paragraph:

An object stays registered with the FinalizationGroup until it's either explicitly unregistered, or when its holdings value is yielded by the iterator after being collected. If the finalizer callback doesn't consume the iterator, the collected object stays registered, and the holdings value will be available in a future iterator. However, the callback is only called when registered objects are newly collected, it won't be triggered for previously collected objects alone.
The application can perform cleanup for collected objects whose holdings values weren't processed yet by calling cleanupSome().

Copy link
Member

Choose a reason for hiding this comment

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

if an object registered with the FinalizationGroup is collected, the callback will be called,

for what the spec says, callback may be called or not.

I don't think consecutive GC changes this.

consecutive GC can't make the callback be called for the previously collected references. So it's the 1 chance opportunity.

The callback will be invoked once.

once or zero times. Per spec text.

Now we can probably clarify that the callback is invoked only if an object is newly collected, not for previously collected objects which holdings were not consumed.

Yes, with the nit of "callback may be invoked".

Maybe replacing was collected by has just been collected would help?

sure.

The example looks good, the only nit picking part is:

the callback is only called when...

that is actually:

the callback might only be called when ...


The only goal I have is to reflect the spec text, even if a specific implementation will secure it being called. It remains optional.

In this case a single FinalizationGroup is shared by all `File` objects as a static field of the `File` class. The class' `#cleanUp` callback will be invoked when any `File` instance was garbage collected.
Copy link
Member

Choose a reason for hiding this comment

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

- In this case a single FinalizationGroup is shared by all `File` objects as a static field of the `File` class. The class' `#cleanUp` callback will be invoked when any `File` instance was garbage collected.
+ In this case a single FinalizationGroup is shared by all `File` objects as a static field of the `File` class. The class' `#cleanUp` callback may be invoked when any `File` instance was garbage collected.


The finalizer callback is called *after* the object is garbage collected, a pattern which is sometimes called "post-mortem". For this reason, a separate "holdings" value is put in the iterator, rather than the original object--the object's already gone, so it can't be used.

Expand Down