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

Conversation

mhofman
Copy link
Member

@mhofman mhofman commented Jun 29, 2019

The FinalizationGroup naming can be understood as a the single finalization of a group of objects.

It wasn't clear in the explainer that the finalizer is simply shared by a group of objects and that the callback is called after any object registered with the group is collected.
See #66 (comment)

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

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

@mhofman
Copy link
Member Author

mhofman commented Jul 12, 2019

@leobalter, I'm just now looking at your comments.

I'm wondering if in the explainer we have to be walking on egg shells everytime about the fact the finalizer callback may not be invoked. Realistically, if the engine collects garbage, the callback will usually be invoked when an object is collected.
I think the note of caution at the beginning of the section that finalizers are tricky business and cannot be relied upon is enough to give context to subsequent mentions of finalizers.

@leobalter
Copy link
Member

I don't think it needs to walk on egg shells, but for me I'd prefer if the readme reflects the spec text, not any specific implementation. This helps highlighting the optional parts of the specs, which is important.


I honestly believe that GC should remains optional, but I honestly don't see a blocker for this optional point where the callback may be called. My suggestion:

  1. Weak references are stored/registered in a FG instance
  2. if the impl. has a GC and it runs the GC:
    a. GC collects that reference (so far this is exactly like the spec says)
    b. (new) the FG callback must be called. The only thing that should remain optional is: it does not necessarily block synchronous code. In this case, it needs to be enqueued in the async code execution.

It fits existing implementation. I could make a PR for this proposed change if you're interested in following up with this suggestion.

Otherwise, I don't want to block this PR, it's the README and it does not affect normative text anyway. I appreciate the convo, thou.

@mhofman
Copy link
Member Author

mhofman commented Jul 12, 2019

  1. if the impl. has a GC and it runs the GC:
    a. GC collects that reference (so far this is exactly like the spec says)
    b. (new) the FG callback must be called. The only thing that should remain optional is: it does not necessarily block synchronous code. In this case, it needs to be enqueued in the async code execution.

It fits existing implementation. I could make a PR for this proposed change if you're interested in following up with this suggestion.

Yeah I agree it's seems weird an implementation would be able to clear a WeakRef which is observable by the program, yet choose to not invoke the callback to notify the program.

#147 already clarifies an implementation must invoke the callback for FinalizationGroups regardless of their liveness. Maybe we need a dedicated section in the spec that puts conditions on the optionality of the callback invocation.

Otherwise, I don't want to block this PR, it's the README and it does not affect normative text anyway. I appreciate the convo, thou.

I'll adjust this PR to capture that the callback may not always be invoked. We can works on the spec improvements in another.

@mhofman mhofman closed this Aug 15, 2019
@mhofman mhofman deleted the explainer-clarify-finalizer-calls branch August 15, 2019 02:30
@mhofman mhofman restored the explainer-clarify-finalizer-calls branch August 15, 2019 02:37
@mhofman mhofman reopened this Aug 15, 2019
@littledan
Copy link
Member

@mhofman Are you planning on addressing @leobalter 's comments?

@mhofman
Copy link
Member Author

mhofman commented Nov 21, 2019

Sorry it totally skipped my mind. Hoping to have some time later this week.

@littledan
Copy link
Member

@mhofman Ping, are these comments being addressed?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants