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

bind_extension_point prevents object garbage collection #97

Closed
pankajp opened this issue May 30, 2017 · 12 comments · Fixed by #546
Closed

bind_extension_point prevents object garbage collection #97

pankajp opened this issue May 30, 2017 · 12 comments · Fixed by #546
Milestone

Comments

@pankajp
Copy link
Contributor

pankajp commented May 30, 2017

bind_extension_point stores a reference to the binding and prevents collection of the object, and it does not offer any way to unbind either.

ExtensionPointBinding objects are stored in a WeakKeyDictionary mapping object weakref to the ExtensionPointBinding instance, but the binding itself stores a strong reference to the object and hence prevents its garbage collection.

@mdickinson
Copy link
Member

Related: #79

@pankajp
Copy link
Contributor Author

pankajp commented May 30, 2017

Great, thanks. Should this be closed as duplicate of the other issue?
I have a simple fix using Weakref trait for obj and it seems to fix a garbage collection issue. I could submit a PR if you think this approach would be acceptable.

@mdickinson
Copy link
Member

I don't think it's quite a duplicate; it seems reasonable to me to leave both issues open. (But also reasonable to create a PR that solves both issues simultaneously.)

@mdickinson
Copy link
Member

So if I understand correctly, this is a bug, in the sense that it's in clear opposition to the design intent:

    # We keep a reference to each binding alive until its associated object
    # is garbage collected.
    _bindings = weakref.WeakKeyDictionary()

(from

# We keep a reference to each binding alive until its associated object
# is garbage collected.
_bindings = weakref.WeakKeyDictionary()
)

That is, the WeakKeyDictionary is pointless here, since the ref-counting machinery will never remove items from it. We could replace it with a regular Python dict and no behaviour would change. Does that match your understanding of the current code?

If so, I'm actually quite happy about this, since WeakKeyDictionary objects can be a source of weird and non-deterministic behaviour. :-)

Rather than fixing the weak-ref mechanics, I'd be much happier to have an extension unbinding operation available.

@pankajp
Copy link
Contributor Author

pankajp commented Jun 1, 2017

That is, the WeakKeyDictionary is pointless here, since the ref-counting machinery will never remove items from it. We could replace it with a regular Python dict and no behaviour would change. Does that match your understanding of the current code?

Yes indeed. In the current code (w/o this PR), the WeakKeyDictionary used for _bindings is useless because the value (ExtensionPointBinding) itself holds a reference to the key preventing its collection. That is what I tried to fix by changing the obj reference to be a weakref, so that it works as designed.

Even with adding an unbind operation, I would like to fix this issue as well.

@mdickinson
Copy link
Member

I would like to fix this issue as well.

Right, but I think there's a case for "fixing" it by simply replacing the WeakKeyDictionary with a regular dictionary, and requiring that clients explicitly unbind when necessary.

@pankajp
Copy link
Contributor Author

pankajp commented Jun 1, 2017

Right, but I think there's a case for "fixing" it by simply replacing the WeakKeyDictionary with a regular dictionary, and requiring that clients explicitly unbind when necessary.

Yes, and that would be perfectly fine since it would match the current behavior as well. I just thought not having to do any changes in exiting projects would be good, and would also behave much like existing on_trait_change and friends about not holding references.

As of now, the need for this has been worked around. I'll get to adding unbind functionality later. Would adding a remove argument to the bind_extension_point be the canonical way to implement it, as is done for on_trait_change and sync_trait?

@mdickinson
Copy link
Member

I just thought not having to do any changes in exiting projects would be good

Yes, there is that. But on the flip side, I'm a bit worried about breaking existing code that was inadvertently relying on the non-collection of the bindings. Maybe that's not a serious possibility.

would also behave much like existing on_trait_change and friends about not holding references.

Agreed that it would be good to have that consistency.

@mdickinson
Copy link
Member

This has just bitten me yet again. Let's see if we can fix for the upcoming Envisage release.

@mdickinson
Copy link
Member

Some thoughts about possible steps towards a fix:

  • In the bind_extension_point function, warn if the extension_registry argument is not supplied. The goal is to eventually make this a required argument, so that we can get rid of the sneaky global introduced in the _extension_registry_default.
  • Move the addition of the extension point to the global dict out of ExtensionPointBinding.__init__ and into the bind_extension_point function. This will make it possible to create an ExtensionPointBinding without modifying global state in the process. We can update the bind_extension_point docstring to clarify the global state change.
  • Add an unbind_extension_point function that takes an ExtensionPointBinding instance and both undoes the connection between the extension point and the trait, and removes the extension point from the global dict.
  • Consider moving the _bindings dictionary out of the ExtensionPointBinding class. (This may have to be done later, since we currently have downstream code that relies on ExtensionPointBinding._bindings existing, precisely so that it can work around this issue.)
  • Consider making the _bindings dictionary a real dictionary (ditto)
  • Consider moving the _bindings dictionary to the extension registry (ditto)

An org-wide code search turned up no direct uses of ExtensionPointBinding beyond those being used specifically to work around this bug, and no uses of bind_extension_point where the registry was not given.

But: there's an ExtensionPoint.bind method that doesn't allow passing a registry. Looks like we'll need to deprecate that, too. It's probably not used much (it's generally inconvenient to use a method on a TraitType).

@mdickinson mdickinson added this to the Release 7.0 milestone Mar 21, 2023
@mdickinson
Copy link
Member

There are some horrible things going on here. At first sight, it looks as though the reference to ExtensionPoint.extension_registry in ExtensionPointBinding._extension_registry_default can't possibly work (and indeed, when I tried to write tests for deprecating the ExtensionPoint.bind method, I got an AttributeError: type object 'ExtensionPoint' has no attribute 'extension_registry'). It turns out that the way that this is expected to work is that the client code is supposed to inject that extension_registry value onto the ExtensionPoint trait type (the class, not any instance), and the tests for extension_point_binding do exactly that (and then never undo that global state change).

I believe (and sincerely hope) that we have no client code that's making that injection, in which case the three-argument form of bind_extension_point simply doesn't work in practice, and I don't think we need to worry about going through a deprecation period. I think we can remove it.

@mdickinson
Copy link
Member

It's probably not used much

A code search turned up no evidence that ExtensionPoint.bind is used anywhere.

mdickinson added a commit that referenced this issue Mar 22, 2023
This PR cleans up some odd use of global state (an extension registry
that lived on the `ExtensionPoint` trait type class) that's never used
in practice.

In detail:

- `ExtensionPointBinding` no longer uses
`ExtensionPoint.extension_registry` as the default extension registry.
(Note that `ExtensionPoint.extension_registry` doesn't exist unless it's
explicitly injected from some external source.)
- The tests for `bind_extension_point` no longer _inject_ a registry
onto the `ExtensionPoint` trait type class, and have been updated to use
an explicit registry everywhere
- The `extension_registry` argument to `bind_extension_point` is now
required rather than optional
- The `bind` static method on the `ExtensionPoint` trait type, which
doesn't support specifying an extension registry, has been removed. If
any users of this exist, they should use `bind_extension_point` instead.

My original plan was to deprecate calls to `bind_extension_point` that
didn't specify a registry. That changed when I discovered that those
calls only work after a deliberate injection of a registry onto the
`ExtensionPoint` trait type class. To the best of my knowledge, no code
is doing that (outside of `test_extension_point_binding` in this
package), and all uses of `bind_extension_point` that I found in the
wild are already supplying a registry explicitly. I also found no uses
of the `ExtensionPoint.bind` static method. So I think in this case
deprecation is unnecessary and we can simply remove the functionality.

xref: #97
mdickinson added a commit that referenced this issue Mar 23, 2023
This PR adds an `unbind_extension_point` function that reverses the
effects of `bind_extension_point`. In particular,
`unbind_extension_point` removes references to the target `HasTraits`
object from global state, allowing that object to be garbage collected
in the usual way. (Previously, those references would live until the end
of the process.)

## Detailed changes

- New `unbind_extension_point` function (exported via `envisage.api`),
with parameters exactly matching those of `bind_extension_point`. This
undoes listener changes, and removes the binding from the global
`ExtensionPointBinding._bindings` dictionary.
- New private `ExtensionPointBinding._unbind` method that undoes
listener changes. The `_initialize` method has been renamed to `_bind`
to match.
- Creation of an `ExtensionPointBinding` object no longer modifies the
global `ExtensionPointBinding._bindings` dictionary; instead, that
mutation of global state is done in `bind_extension_point`. (At some
point in the future, we may want to move this global state somewhere
else - perhaps onto the extension registry.)
- `ExtensionPointBinding._bindings` is now a regular Python `dict`
instead of a `WeakKeyDictionary`: the weak reference functionality never
worked in the first place because our dictionary values had a strong
reference to the corresponding keys, so there's no need for the
additional complication introduced by the use of a `WeakKeyDictionary`.

Fixes #97
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants