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
1=== modified file 'storm/references.py'
2--- storm/references.py 2009-02-19 16:44:33 +0000
3+++ storm/references.py 2009-11-20 20:56:10 +0000
4@@ -18,6 +18,8 @@
5 # You should have received a copy of the GNU Lesser General Public License
6 # along with this program. If not, see <http://www.gnu.org/licenses/>.
7 #
8+import weakref
9+
10 from storm.exceptions import WrongStoreError, NoStoreError, ClassInfoError
11 from storm.store import Store, get_where_for_args
12 from storm.variables import LazyValue
13@@ -620,7 +622,7 @@
14 #local_info.event.hook("removed", self._break_on_local_removed,
15 # remote_info)
16 remote_info.event.hook("removed", self._break_on_remote_removed,
17- local_info)
18+ weakref.ref(local_info))
19 else:
20 remote_has_changed = False
21 for local_column, remote_column in pairs:
22@@ -649,10 +651,10 @@
23 local_info.event.hook("changed", self._break_on_local_diverged,
24 remote_info)
25 remote_info.event.hook("changed", self._break_on_remote_diverged,
26- local_info)
27+ weakref.ref(local_info))
28 if self.on_remote:
29 remote_info.event.hook("removed", self._break_on_remote_removed,
30- local_info)
31+ weakref.ref(local_info))
32
33 def unlink(self, local_info, remote_info, setting=False):
34 """Break the relation between the local and remote objects.
35@@ -684,11 +686,11 @@
36 remote_info.event.unhook("changed", self._track_remote_changes,
37 local_info)
38 remote_info.event.unhook("changed", self._break_on_remote_diverged,
39- local_info)
40+ weakref.ref(local_info))
41 remote_info.event.unhook("flushed", self._break_on_remote_flushed,
42 local_info)
43 remote_info.event.unhook("removed", self._break_on_remote_removed,
44- local_info)
45+ weakref.ref(local_info))
46
47 if local_store is None:
48 if not self.many or not remote_infos:
49@@ -781,13 +783,16 @@
50 self.unlink(local_info, remote_info)
51
52 def _break_on_remote_diverged(self, remote_info, remote_variable,
53- old_value, new_value, fromdb, local_info):
54+ old_value, new_value, fromdb, local_info_ref):
55 """Break the remote/local relationship on diverging changes.
56
57 This hook ensures that if the remote object has an attribute
58 changed by hand in a way that diverges from the local object,
59 the relationship is undone.
60 """
61+ local_info = local_info_ref()
62+ if local_info is None:
63+ return
64 local_column = self._get_local_column(local_info.cls_info.cls,
65 remote_variable.column)
66 if local_column is not None:
67@@ -803,9 +808,11 @@
68 """Break the remote/local relationship on flush."""
69 self.unlink(local_info, remote_info)
70
71- def _break_on_remote_removed(self, remote_info, local_info):
72+ def _break_on_remote_removed(self, remote_info, local_info_ref):
73 """Break the remote relationship when the remote object is removed."""
74- self.unlink(local_info, remote_info)
75+ local_info = local_info_ref()
76+ if local_info is not None:
77+ self.unlink(local_info, remote_info)
78
79 def _add_all(self, obj_info, local_info):
80 store = Store.of(obj_info)
81
82=== modified file 'tests/store/base.py'
83--- tests/store/base.py 2009-10-22 02:30:20 +0000
84+++ tests/store/base.py 2009-11-20 20:56:10 +0000
85@@ -2605,6 +2605,39 @@
86 bar.foo_id = SQL("20")
87 self.assertEquals(bar.foo.id, 20)
88
89+ def test_reference_remote_leak_on_flush_with_changed(self):
90+ """
91+ "changed" events only hold weak references to remote infos object, thus
92+ not creating a leak when unhooked.
93+ """
94+ self.get_cache(self.store).set_size(0)
95+ bar = self.store.get(Bar, 100)
96+ bar.foo.title = u"Changed title"
97+ bar_ref = weakref.ref(get_obj_info(bar))
98+ foo = bar.foo
99+ del bar
100+ self.store.flush()
101+ gc.collect()
102+ self.assertEquals(bar_ref(), None)
103+
104+ def test_reference_remote_leak_on_flush_with_removed(self):
105+ """
106+ "removed" events only hold weak references to remote infos objects,
107+ thus not creating a leak when unhooked.
108+ """
109+ self.get_cache(self.store).set_size(0)
110+ class MyFoo(Foo):
111+ bar = Reference(Foo.id, Bar.foo_id, on_remote=True)
112+
113+ foo = self.store.get(MyFoo, 10)
114+ foo.bar.title = u"Changed title"
115+ foo_ref = weakref.ref(get_obj_info(foo))
116+ bar = foo.bar
117+ del foo
118+ self.store.flush()
119+ gc.collect()
120+ self.assertEquals(foo_ref(), None)
121+
122 def test_reference_break_on_remote_diverged_by_lazy(self):
123 class MyBar(Bar):
124 pass
125@@ -4484,7 +4517,7 @@
126 self.store.invalidate()
127
128 obj_info = get_obj_info(pickle_blob)
129- variable = obj_info.variables[PickleBlob.bin]
130+ variable = obj_info.variables[PickleBlob.bin]
131 var_ref = weakref.ref(variable)
132 del variable, blob, pickle_blob, obj_info
133 gc.collect()
134@@ -4523,7 +4556,7 @@
135 self.store.invalidate()
136
137 obj_info = get_obj_info(pickle_blob)
138- variable = obj_info.variables[PickleBlob.bin]
139+ variable = obj_info.variables[PickleBlob.bin]
140 var_ref = weakref.ref(variable)
141 del variable, blob, pickle_blob, obj_info, foo
142 gc.collect()

Subscribers

People subscribed via source and target branches

to status/vote changes: