Merge lp:~lifeless/launchpad/malone into lp:launchpad

Proposed by Robert Collins on 2010-09-15
Status: Merged
Approved by: Robert Collins on 2010-09-16
Approved revision: no longer in the source branch.
Merged at revision: 11557
Proposed branch: lp:~lifeless/launchpad/malone
Merge into: lp:launchpad
Prerequisite: lp:~lifeless/launchpad/decoratedresultset
Diff against target: 259 lines (+143/-26)
4 files modified
lib/canonical/launchpad/database/message.py (+7/-2)
lib/lp/bugs/model/bug.py (+95/-14)
lib/lp/bugs/tests/test_bugs_webservice.py (+41/-4)
lib/lp/bugs/tests/test_searchtasks_webservice.py (+0/-6)
To merge this branch: bzr merge lp:~lifeless/launchpad/malone
Reviewer Review Type Date Requested Status
Edwin Grubbs (community) code 2010-09-15 Approve on 2010-09-15
Review via email: mp+35511@code.launchpad.net

Commit Message

Remove listification from the internals of the bugs/message API.

Description of the Change

Remove listification from the internals of the bugs/message API.

To post a comment you must log in.
Edwin Grubbs (edwin-grubbs) wrote :
Download full text (6.7 KiB)

Hi Robert,

This is a nice improvement. I have a few comments below.

-Edwin

>=== modified file 'lib/canonical/launchpad/database/message.py'
>--- lib/canonical/launchpad/database/message.py 2010-08-29 21:24:47 +0000
>+++ lib/canonical/launchpad/database/message.py 2010-09-15 19:20:47 +0000
>@@ -158,10 +159,14 @@
> """See IMessage."""
> return self.owner
>
>- @property
>+ @cachedproperty
> def text_contents(self):
> """See IMessage."""
>- bits = [unicode(chunk) for chunk in self if chunk.content]
>+ return Message.chunks_text(self.chunks)
>+
>+ @classmethod
>+ def chunks_text(klass, chunks):

PEP8 wants you to use cls instead of klass.

>+ bits = [unicode(chunk) for chunk in chunks if chunk.content]
> return '\n\n'.join(bits)
>
> # XXX flacoste 2006-09-08: Bogus attribute only present so that
>
>=== modified file 'lib/lp/bugs/model/bug.py'
>--- lib/lp/bugs/model/bug.py 2010-09-09 17:02:33 +0000
>+++ lib/lp/bugs/model/bug.py 2010-09-15 19:20:47 +0000
>@@ -58,6 +58,7 @@
> SQLRaw,
> Union,
> )
>+from storm.info import ClassAlias
> from storm.store import (
> EmptyResultSet,
> Store,
>@@ -426,21 +427,79 @@
> @property
> def indexed_messages(self):
> """See `IMessageTarget`."""
>+ return self._indexed_messages(content=True)
>+
>+ def _indexed_messages(self, content=False):
>+ """Get the bugs messages, indexed.
>+
>+ :param content: If True retrieve the content for the msssages too.

s/msssages/messages/
The "content" parameter sounds more like it should contain content
as opposed to being a boolean. How about "include_content" instead?

>+ """
>+ # Make all messages be 'in' the main bugtask.
> inside = self.default_bugtask
>- messages = list(self.messages)
>- message_set = set(messages)
>-
>- indexed_messages = []
>- for index, message in enumerate(messages):
>- if message.parent not in message_set:
>- parent = None
>- else:
>- parent = message.parent
>-
>- indexed_message = IndexedMessage(message, inside, index, parent)
>- indexed_messages.append(indexed_message)
>-
>- return indexed_messages
>+ store = Store.of(self)
>+ message_by_id = {}
>+ def eager_load_owners(rows):
>+ # Because we may have multiple owners, we spend less time in storm
>+ # with very large bugs by not joining and instead querying a second
>+ # time. If this starts to show high db time, we can left outer join

Long lines.

>+ # instead.
>+ owner_ids = set(row[0].ownerID for row in rows)
>+ owner_ids.discard(None)
>+ if not owner_ids:
>+ return
>+ list(store.find(Person, Person.id.is_in(owner_ids)))
>+ def eager_load_content(rows):
>+ # To avoid the complexity of having multiple rows per
>+ # message, or joining in the database (though perhaps in
>+ # future we should do that), we do a single separate query
>+ # for the message content.
...

Read more...

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/database/message.py'
2--- lib/canonical/launchpad/database/message.py 2010-08-29 21:24:47 +0000
3+++ lib/canonical/launchpad/database/message.py 2010-09-15 23:43:54 +0000
4@@ -82,6 +82,7 @@
5 from lp.app.errors import NotFoundError
6 from lp.registry.interfaces.person import validate_public_person
7 from lp.services.job.model.job import Job
8+from lp.services.propertycache import cachedproperty
9
10 # this is a hard limit on the size of email we will be willing to store in
11 # the database.
12@@ -158,10 +159,14 @@
13 """See IMessage."""
14 return self.owner
15
16- @property
17+ @cachedproperty
18 def text_contents(self):
19 """See IMessage."""
20- bits = [unicode(chunk) for chunk in self if chunk.content]
21+ return Message.chunks_text(self.chunks)
22+
23+ @classmethod
24+ def chunks_text(cls, chunks):
25+ bits = [unicode(chunk) for chunk in chunks if chunk.content]
26 return '\n\n'.join(bits)
27
28 # XXX flacoste 2006-09-08: Bogus attribute only present so that
29
30=== modified file 'lib/lp/bugs/model/bug.py'
31--- lib/lp/bugs/model/bug.py 2010-09-09 17:02:33 +0000
32+++ lib/lp/bugs/model/bug.py 2010-09-15 23:43:54 +0000
33@@ -58,6 +58,7 @@
34 SQLRaw,
35 Union,
36 )
37+from storm.info import ClassAlias
38 from storm.store import (
39 EmptyResultSet,
40 Store,
41@@ -426,21 +427,101 @@
42 @property
43 def indexed_messages(self):
44 """See `IMessageTarget`."""
45+ return self._indexed_messages(include_content=True)
46+
47+ def _indexed_messages(self, include_content=False, include_parents=True):
48+ """Get the bugs messages, indexed.
49+
50+ :param include_content: If True retrieve the content for the messages
51+ too.
52+ :param include_parents: If True retrieve the object for parent messages
53+ too. If False the parent attribute will be *forced* to None to
54+ reduce database lookups.
55+ """
56+ # Make all messages be 'in' the main bugtask.
57 inside = self.default_bugtask
58- messages = list(self.messages)
59- message_set = set(messages)
60-
61- indexed_messages = []
62- for index, message in enumerate(messages):
63- if message.parent not in message_set:
64- parent = None
65+ store = Store.of(self)
66+ message_by_id = {}
67+ if include_parents:
68+ def to_messages(rows):
69+ return [row[0] for row in rows]
70+ else:
71+ def to_messages(rows):
72+ return rows
73+ def eager_load_owners(messages):
74+ # Because we may have multiple owners, we spend less time in storm
75+ # with very large bugs by not joining and instead querying a second
76+ # time. If this starts to show high db time, we can left outer join
77+ # instead.
78+ owner_ids = set(message.ownerID for message in messages)
79+ owner_ids.discard(None)
80+ if not owner_ids:
81+ return
82+ list(store.find(Person, Person.id.is_in(owner_ids)))
83+ def eager_load_content(messages):
84+ # To avoid the complexity of having multiple rows per
85+ # message, or joining in the database (though perhaps in
86+ # future we should do that), we do a single separate query
87+ # for the message content.
88+ message_ids = set(message.id for message in messages)
89+ chunks = store.find(
90+ MessageChunk, MessageChunk.messageID.is_in(message_ids))
91+ chunks.order_by(MessageChunk.id)
92+ chunk_map = {}
93+ for chunk in chunks:
94+ message_chunks = chunk_map.setdefault(chunk.messageID, [])
95+ message_chunks.append(chunk)
96+ for message in messages:
97+ if message.id not in chunk_map:
98+ continue
99+ cache = IPropertyCache(message)
100+ cache.text_contents = Message.chunks_text(
101+ chunk_map[message.id])
102+ def eager_load(rows, slice_info):
103+ messages = to_messages(rows)
104+ eager_load_owners(messages)
105+ if include_content:
106+ eager_load_content(messages)
107+ def index_message(row, index):
108+ # convert row to an IndexedMessage
109+ if include_parents:
110+ message, parent = row
111+ if parent is not None:
112+ # If there is an IndexedMessage available as parent, use
113+ # that to reduce on-demand parent lookups.
114+ parent = message_by_id.get(parent.id, parent)
115 else:
116- parent = message.parent
117-
118- indexed_message = IndexedMessage(message, inside, index, parent)
119- indexed_messages.append(indexed_message)
120-
121- return indexed_messages
122+ message = row
123+ parent = None # parent attribute is not going to be accessed.
124+ result = IndexedMessage(message, inside, index, parent)
125+ # This message may be the parent for another: stash it to permit
126+ # use.
127+ message_by_id[message.id] = result
128+ return result
129+ # There is possibly some nicer way to do this in storm, but
130+ # this is a lot easier to figure out.
131+ if include_parents:
132+ ParentMessage = ClassAlias(Message, name="parent_message")
133+ tables = SQL("""
134+Message left outer join
135+message as parent_message on (
136+ message.parent=parent_message.id and
137+ parent_message.id in (
138+ select bugmessage.message from bugmessage where bugmessage.bug=%s)),
139+BugMessage""" % sqlvalues(self.id))
140+ results = store.using(tables).find(
141+ (Message, ParentMessage),
142+ BugMessage.bugID == self.id,
143+ BugMessage.messageID == Message.id,
144+ )
145+ else:
146+ results = store.find(Message,
147+ BugMessage.bugID == self.id,
148+ BugMessage.messageID == Message.id,
149+ )
150+ results.order_by(Message.datecreated, Message.id)
151+ return DecoratedResultSet(results, index_message,
152+ pre_iter_hook=eager_load, slice_info=True)
153
154 @property
155 def displayname(self):
156@@ -1785,7 +1866,7 @@
157 when attachments is evaluated, not when the resultset is processed).
158 """
159 message_to_indexed = {}
160- for message in self.indexed_messages:
161+ for message in self._indexed_messages(include_parents=False):
162 message_to_indexed[message.id] = message
163 def set_indexed_message(row):
164 attachment = row[0]
165
166=== modified file 'lib/lp/bugs/tests/test_bugs_webservice.py'
167--- lib/lp/bugs/tests/test_bugs_webservice.py 2010-09-02 19:30:03 +0000
168+++ lib/lp/bugs/tests/test_bugs_webservice.py 2010-09-15 23:43:54 +0000
169@@ -143,7 +143,7 @@
170 rendered_comment, self.expected_comment_html)
171
172
173-class TestBugAttachments(TestCaseWithFactory):
174+class TestBugScaling(TestCaseWithFactory):
175
176 layer = LaunchpadFunctionalLayer
177
178@@ -165,12 +165,12 @@
179 collector = QueryCollector()
180 collector.register()
181 self.addCleanup(collector.unregister)
182- url = '/bugs/%d/attachments' % self.bug.id
183+ url = '/bugs/%d/attachments?ws.size=75' % self.bug.id
184 #First request
185 store.flush()
186 store.reset()
187 response = webservice.get(url)
188- self.assertThat(collector, HasQueryCount(LessThan(24)))
189+ self.assertThat(collector, HasQueryCount(LessThan(21)))
190 with_2_count = collector.count
191 self.failUnlessEqual(response.status, 200)
192 login(USER_EMAIL)
193@@ -181,7 +181,44 @@
194 store.flush()
195 store.reset()
196 response = webservice.get(url)
197- self.assertThat(collector, HasQueryCount(Equals(with_2_count+1)))
198+ self.assertThat(collector, HasQueryCount(Equals(with_2_count)))
199+
200+ def test_messages_query_counts_constant(self):
201+ # XXX Robert Collins 2010-09-15 bug=619017
202+ # This test may be thrown off by the reference bug. To get around the
203+ # problem, flush and reset are called on the bug storm cache before
204+ # each call to the webservice. When lp's storm is updated to release
205+ # the committed fix for this bug, please see about updating this test.
206+ login(USER_EMAIL)
207+ bug = self.factory.makeBug()
208+ store = Store.of(bug)
209+ self.factory.makeBugComment(bug)
210+ self.factory.makeBugComment(bug)
211+ self.factory.makeBugComment(bug)
212+ person = self.factory.makePerson()
213+ webservice = LaunchpadWebServiceCaller(
214+ 'launchpad-library', 'salgado-change-anything')
215+ collector = QueryCollector()
216+ collector.register()
217+ self.addCleanup(collector.unregister)
218+ url = '/bugs/%d/messages?ws.size=75' % bug.id
219+ #First request
220+ store.flush()
221+ store.reset()
222+ response = webservice.get(url)
223+ self.assertThat(collector, HasQueryCount(LessThan(21)))
224+ with_2_count = collector.count
225+ self.failUnlessEqual(response.status, 200)
226+ login(USER_EMAIL)
227+ for i in range(50):
228+ self.factory.makeBugComment(bug)
229+ self.factory.makeBugAttachment(bug)
230+ logout()
231+ #Second request
232+ store.flush()
233+ store.reset()
234+ response = webservice.get(url)
235+ self.assertThat(collector, HasQueryCount(Equals(with_2_count)))
236
237
238 class TestBugMessages(TestCaseWithFactory):
239
240=== modified file 'lib/lp/bugs/tests/test_searchtasks_webservice.py'
241--- lib/lp/bugs/tests/test_searchtasks_webservice.py 2010-08-20 21:48:35 +0000
242+++ lib/lp/bugs/tests/test_searchtasks_webservice.py 2010-09-15 23:43:54 +0000
243@@ -5,8 +5,6 @@
244
245 __metaclass__ = type
246
247-from unittest import TestLoader
248-
249 from canonical.launchpad.ftests import login
250 from lp.testing import TestCaseWithFactory
251 from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller
252@@ -37,7 +35,3 @@
253 response = self.webservice.named_get('/mebuntu/inkanyamba',
254 'searchTasks', api_version='devel').jsonBody()
255 self.assertEqual(response['total_size'], 1)
256-
257-
258-def test_suite():
259- return TestLoader().loadTestsFromName(__name__)