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

Use weak references in AggregateSensor #51

Merged
merged 7 commits into from Nov 2, 2022

Conversation

bmerry
Copy link
Contributor

@bmerry bmerry commented Nov 2, 2022

This ensures that an aggregate sensor can be garbage collected once it's no longer explicitly referenced, even if its target sensor set is still live. It also makes __del__ more robust to exceptions occurring during __init__.

This will allow aggregates to die once they're no longer part of a
SensorSet or otherwise referenced, even if the sensors they aggregate
are still live.
The closure they returned referenced `self`. Copying the original
function into a local variable should prevent that.
@coveralls
Copy link

coveralls commented Nov 2, 2022

Coverage Status

Coverage increased (+0.1%) to 93.534% when pulling 6ea1a36 on NGC-442-aggregate-sensors-weakref into 82561af on ngc-442-aggregate-sensors.

This tests the otherwise-unused code paths in _weak_callback.
Copy link
Contributor

@james-smith-za james-smith-za left a comment

Choose a reason for hiding this comment

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

So I'll confess to still not quite understanding why this works. I decided to check whether it's necessary by running the unit tests without your weakref modifications, and I get the following:

    def test_aggregate_garbage_collection(self, ss, sensors):
        """Check that the aggregate can be garbage collected."""
        # Don't use the agg_sensor fixture, because pytest will hold its own
        # references to it.
        my_agg = MyAgg(target=ss, sensor_type=int, name="garbage")
        ss.add(sensors[1])
        weak = weakref.ref(my_agg)
        del my_agg
        # Some Python implementations need multiple rounds to garbage-collect
        # everything.
        for _ in range(5):
            gc.collect()
>       assert weak() is None  # i.e. my_agg was garbage-collected
E       AssertionError: assert <test_sensor_updated.MyAgg object at 0x7fba41553c70> is None
E        +  where <test_sensor_updated.MyAgg object at 0x7fba41553c70> = <weakref at 0x7fba414eef90; to 'MyAgg' at 0x7fba41553c70>()

So clearly the changes are necessary. It's going to take me a while to understand why though, and I don't want to hold up the process.


@functools.wraps(self._func)
def wrapper(*args, **kwargs):
strong_instance = weak_instance()
Copy link
Contributor

Choose a reason for hiding this comment

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

This line reads rather weirdly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The __call__ on a weakref gives you the object it's referencing (or None if it is dead).

# Don't use the fixtures, because they have mocks that might
# record things and keep them alive.
# The noqa is to suppress
# "local variable '_my_agg' is assigned to but never used"
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
# "local variable '_my_agg' is assigned to but never used"
# "local variable 'my_agg' is assigned to but never used"

Either that or add an underscore to the name of the variable that you create? I don't have a preference, they should probably just be consistent.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch - I'd tried using a leading underscore in case it shut up flake8 (it didn't) and copy-pasted the error from that version. Fixed now.

@bmerry
Copy link
Contributor Author

bmerry commented Nov 2, 2022

So clearly the changes are necessary. It's going to take me a while to understand why though, and I don't want to hold up the process.

In case it helps: without the code changes, sensors[1] will have my_agg._update_aggregate as one of its observers. A bound method holds a reference to the instance its bound to, so sensors[1] indirectly references my_agg, keeping it alive. With the change, the observer becomes the wrapper closure defined by _weak_callback, which holds only a weak reference to my_agg.

@bmerry bmerry merged commit 9f7a3ba into ngc-442-aggregate-sensors Nov 2, 2022
@bmerry bmerry deleted the NGC-442-aggregate-sensors-weakref branch November 2, 2022 08:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants