Merge lp:~therve/storm/reference-changed-leak into lp:storm

Proposed by Thomas Herve
Status: Merged
Approved by: James Henstridge
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~therve/storm/reference-changed-leak
Merge into: lp:storm
Diff against target: 142 lines (+50/-10)
2 files modified
storm/references.py (+15/-8)
tests/store/base.py (+35/-2)
To merge this branch: bzr merge lp:~therve/storm/reference-changed-leak
Reviewer Review Type Date Requested Status
James Henstridge Approve
Jamu Kakar (community) Approve
Review via email: mp+14480@code.launchpad.net
To post a comment you must log in.
340. By Thomas Herve

Fix the problem using a weak reference, and test the actual problem observerd

Revision history for this message
James Henstridge (jamesh) wrote :

This looks pretty good. I just have a few comments:

[1]
This seems to be relying on the fact that weakref.ref() always returns the same reference object given the same input. Looking at the weakref.__new__ implementation this is indeed true, but I don't see it spelled out in the Python documentation.

The source code makes it sound like an optimisation rather than a language feature, so I wonder if it is a good idea to rely on it?

[2]
For on_remote references, an additional "removed" event handler is hooked in the remote -> local direction. Won't this cause the same circular reference problem that the remote -> local "changed" handler does?

[3]
If we have references on both the local and remote objects (one forward, one backward) and access them both, will that generate the same kind of cycle? While each ref in isolation doesn't form a cycle, as a pair they do.

341. By Thomas Herve

Use the same trick for "removed" event

Revision history for this message
Thomas Herve (therve) wrote :

Thanks a lot for the review.

> [1]
> This seems to be relying on the fact that weakref.ref() always returns the
> same reference object given the same input. Looking at the weakref.__new__
> implementation this is indeed true, but I don't see it spelled out in the
> Python documentation.
>
> The source code makes it sound like an optimisation rather than a language
> feature, so I wonder if it is a good idea to rely on it?

Actually, I think it relies on the fact they have the same hash and are equals, which is made explicit in the documentation.

> [2]
> For on_remote references, an additional "removed" event handler is hooked in
> the remote -> local direction. Won't this cause the same circular reference
> problem that the remote -> local "changed" handler does?

Nice catch, I fixed it.

> [3]
> If we have references on both the local and remote objects (one forward, one
> backward) and access them both, will that generate the same kind of cycle?
> While each ref in isolation doesn't form a cycle, as a pair they do.

The thing is that we want (for now) to keep the objects alive if they have a direct reference. So, no, I don't think it's a problem, because the objects will stay alive only if at least one of both objects are referenced directly.

342. By Thomas Herve

Explicit None check

Revision history for this message
Jamu Kakar (jkakar) wrote :

Looks good, tests pass here.

review: Approve
Revision history for this message
James Henstridge (jamesh) wrote :

> Thanks a lot for the review.
>
> > [1]
> Actually, I think it relies on the fact they have the same hash and are
> equals, which is made explicit in the documentation.

Gar. I missed that in the docs. I noticed that weakref.ref() returned the same reference object for an instance and was wondering if that was what was causing equality and hashing to work. Never mind that issue then.

> > [2]
> > For on_remote references, an additional "removed" event handler is hooked in
> > the remote -> local direction. Won't this cause the same circular reference
> > problem that the remote -> local "changed" handler does?
>
> Nice catch, I fixed it.

Fix looks good.

> > [3]
> > If we have references on both the local and remote objects (one forward, one
> > backward) and access them both, will that generate the same kind of cycle?
> > While each ref in isolation doesn't form a cycle, as a pair they do.
>
> The thing is that we want (for now) to keep the objects alive if they have a
> direct reference. So, no, I don't think it's a problem, because the objects
> will stay alive only if at least one of both objects are referenced directly.

Okay. If it is an issue, we can look at it later. This patch certainly won't introduce a problem of this type.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'storm/references.py'
--- storm/references.py 2009-02-19 16:44:33 +0000
+++ storm/references.py 2009-11-20 20:56:10 +0000
@@ -18,6 +18,8 @@
18# You should have received a copy of the GNU Lesser General Public License18# You should have received a copy of the GNU Lesser General Public License
19# along with this program. If not, see <http://www.gnu.org/licenses/>.19# along with this program. If not, see <http://www.gnu.org/licenses/>.
20#20#
21import weakref
22
21from storm.exceptions import WrongStoreError, NoStoreError, ClassInfoError23from storm.exceptions import WrongStoreError, NoStoreError, ClassInfoError
22from storm.store import Store, get_where_for_args24from storm.store import Store, get_where_for_args
23from storm.variables import LazyValue25from storm.variables import LazyValue
@@ -620,7 +622,7 @@
620 #local_info.event.hook("removed", self._break_on_local_removed,622 #local_info.event.hook("removed", self._break_on_local_removed,
621 # remote_info)623 # remote_info)
622 remote_info.event.hook("removed", self._break_on_remote_removed,624 remote_info.event.hook("removed", self._break_on_remote_removed,
623 local_info)625 weakref.ref(local_info))
624 else:626 else:
625 remote_has_changed = False627 remote_has_changed = False
626 for local_column, remote_column in pairs:628 for local_column, remote_column in pairs:
@@ -649,10 +651,10 @@
649 local_info.event.hook("changed", self._break_on_local_diverged,651 local_info.event.hook("changed", self._break_on_local_diverged,
650 remote_info)652 remote_info)
651 remote_info.event.hook("changed", self._break_on_remote_diverged,653 remote_info.event.hook("changed", self._break_on_remote_diverged,
652 local_info)654 weakref.ref(local_info))
653 if self.on_remote:655 if self.on_remote:
654 remote_info.event.hook("removed", self._break_on_remote_removed,656 remote_info.event.hook("removed", self._break_on_remote_removed,
655 local_info)657 weakref.ref(local_info))
656658
657 def unlink(self, local_info, remote_info, setting=False):659 def unlink(self, local_info, remote_info, setting=False):
658 """Break the relation between the local and remote objects.660 """Break the relation between the local and remote objects.
@@ -684,11 +686,11 @@
684 remote_info.event.unhook("changed", self._track_remote_changes,686 remote_info.event.unhook("changed", self._track_remote_changes,
685 local_info)687 local_info)
686 remote_info.event.unhook("changed", self._break_on_remote_diverged,688 remote_info.event.unhook("changed", self._break_on_remote_diverged,
687 local_info)689 weakref.ref(local_info))
688 remote_info.event.unhook("flushed", self._break_on_remote_flushed,690 remote_info.event.unhook("flushed", self._break_on_remote_flushed,
689 local_info)691 local_info)
690 remote_info.event.unhook("removed", self._break_on_remote_removed,692 remote_info.event.unhook("removed", self._break_on_remote_removed,
691 local_info)693 weakref.ref(local_info))
692694
693 if local_store is None:695 if local_store is None:
694 if not self.many or not remote_infos:696 if not self.many or not remote_infos:
@@ -781,13 +783,16 @@
781 self.unlink(local_info, remote_info)783 self.unlink(local_info, remote_info)
782784
783 def _break_on_remote_diverged(self, remote_info, remote_variable,785 def _break_on_remote_diverged(self, remote_info, remote_variable,
784 old_value, new_value, fromdb, local_info):786 old_value, new_value, fromdb, local_info_ref):
785 """Break the remote/local relationship on diverging changes.787 """Break the remote/local relationship on diverging changes.
786788
787 This hook ensures that if the remote object has an attribute789 This hook ensures that if the remote object has an attribute
788 changed by hand in a way that diverges from the local object,790 changed by hand in a way that diverges from the local object,
789 the relationship is undone.791 the relationship is undone.
790 """792 """
793 local_info = local_info_ref()
794 if local_info is None:
795 return
791 local_column = self._get_local_column(local_info.cls_info.cls,796 local_column = self._get_local_column(local_info.cls_info.cls,
792 remote_variable.column)797 remote_variable.column)
793 if local_column is not None:798 if local_column is not None:
@@ -803,9 +808,11 @@
803 """Break the remote/local relationship on flush."""808 """Break the remote/local relationship on flush."""
804 self.unlink(local_info, remote_info)809 self.unlink(local_info, remote_info)
805810
806 def _break_on_remote_removed(self, remote_info, local_info):811 def _break_on_remote_removed(self, remote_info, local_info_ref):
807 """Break the remote relationship when the remote object is removed."""812 """Break the remote relationship when the remote object is removed."""
808 self.unlink(local_info, remote_info)813 local_info = local_info_ref()
814 if local_info is not None:
815 self.unlink(local_info, remote_info)
809816
810 def _add_all(self, obj_info, local_info):817 def _add_all(self, obj_info, local_info):
811 store = Store.of(obj_info)818 store = Store.of(obj_info)
812819
=== modified file 'tests/store/base.py'
--- tests/store/base.py 2009-10-22 02:30:20 +0000
+++ tests/store/base.py 2009-11-20 20:56:10 +0000
@@ -2605,6 +2605,39 @@
2605 bar.foo_id = SQL("20")2605 bar.foo_id = SQL("20")
2606 self.assertEquals(bar.foo.id, 20)2606 self.assertEquals(bar.foo.id, 20)
26072607
2608 def test_reference_remote_leak_on_flush_with_changed(self):
2609 """
2610 "changed" events only hold weak references to remote infos object, thus
2611 not creating a leak when unhooked.
2612 """
2613 self.get_cache(self.store).set_size(0)
2614 bar = self.store.get(Bar, 100)
2615 bar.foo.title = u"Changed title"
2616 bar_ref = weakref.ref(get_obj_info(bar))
2617 foo = bar.foo
2618 del bar
2619 self.store.flush()
2620 gc.collect()
2621 self.assertEquals(bar_ref(), None)
2622
2623 def test_reference_remote_leak_on_flush_with_removed(self):
2624 """
2625 "removed" events only hold weak references to remote infos objects,
2626 thus not creating a leak when unhooked.
2627 """
2628 self.get_cache(self.store).set_size(0)
2629 class MyFoo(Foo):
2630 bar = Reference(Foo.id, Bar.foo_id, on_remote=True)
2631
2632 foo = self.store.get(MyFoo, 10)
2633 foo.bar.title = u"Changed title"
2634 foo_ref = weakref.ref(get_obj_info(foo))
2635 bar = foo.bar
2636 del foo
2637 self.store.flush()
2638 gc.collect()
2639 self.assertEquals(foo_ref(), None)
2640
2608 def test_reference_break_on_remote_diverged_by_lazy(self):2641 def test_reference_break_on_remote_diverged_by_lazy(self):
2609 class MyBar(Bar):2642 class MyBar(Bar):
2610 pass2643 pass
@@ -4484,7 +4517,7 @@
4484 self.store.invalidate()4517 self.store.invalidate()
44854518
4486 obj_info = get_obj_info(pickle_blob)4519 obj_info = get_obj_info(pickle_blob)
4487 variable = obj_info.variables[PickleBlob.bin]4520 variable = obj_info.variables[PickleBlob.bin]
4488 var_ref = weakref.ref(variable)4521 var_ref = weakref.ref(variable)
4489 del variable, blob, pickle_blob, obj_info4522 del variable, blob, pickle_blob, obj_info
4490 gc.collect()4523 gc.collect()
@@ -4523,7 +4556,7 @@
4523 self.store.invalidate()4556 self.store.invalidate()
45244557
4525 obj_info = get_obj_info(pickle_blob)4558 obj_info = get_obj_info(pickle_blob)
4526 variable = obj_info.variables[PickleBlob.bin]4559 variable = obj_info.variables[PickleBlob.bin]
4527 var_ref = weakref.ref(variable)4560 var_ref = weakref.ref(variable)
4528 del variable, blob, pickle_blob, obj_info, foo4561 del variable, blob, pickle_blob, obj_info, foo
4529 gc.collect()4562 gc.collect()

Subscribers

People subscribed via source and target branches

to status/vote changes: