Merge lp:~wgrant/launchpad/bug-728174 into lp:launchpad

Proposed by William Grant
Status: Merged
Approved by: Robert Collins
Approved revision: no longer in the source branch.
Merged at revision: 12540
Proposed branch: lp:~wgrant/launchpad/bug-728174
Merge into: lp:launchpad
Diff against target: 287 lines (+161/-40)
3 files modified
lib/lp/services/worlddata/interfaces/language.py (+5/-3)
lib/lp/services/worlddata/model/language.py (+96/-34)
lib/lp/services/worlddata/tests/test_language.py (+60/-3)
To merge this branch: bzr merge lp:~wgrant/launchpad/bug-728174
Reviewer Review Type Date Requested Status
Robert Collins (community) Approve
Review via email: mp+52374@code.launchpad.net

Commit message

[r=lifeless][bug=728174] Optionally preload Language.translators_count in LanguageSet.get(All|Default)Languages.

Description of the change

Webservice Language collections returned by LanguageSet were timing out on repeated heavy queries to calculate Language.translators_count. This branch calculates them all in a single query, running only slightly slower than a single individual query.

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

ids = [language.id for language in languages]

I would setify this. Possibly not needed, but belt and braces.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/services/worlddata/interfaces/language.py'
2--- lib/lp/services/worlddata/interfaces/language.py 2010-12-02 16:13:51 +0000
3+++ lib/lp/services/worlddata/interfaces/language.py 2011-03-07 07:51:04 +0000
4@@ -19,6 +19,7 @@
5 )
6 from lazr.lifecycle.snapshot import doNotSnapshot
7 from lazr.restful.declarations import (
8+ call_with,
9 collection_default_content,
10 export_as_webservice_collection,
11 export_as_webservice_entry,
12@@ -176,11 +177,12 @@
13
14 @export_read_operation()
15 @operation_returns_collection_of(ILanguage)
16- def getAllLanguages():
17+ @call_with(want_translators_count=True)
18+ def getAllLanguages(want_translators_count=False):
19 """Return a result set of all ILanguages from Launchpad."""
20
21- @collection_default_content()
22- def getDefaultLanguages():
23+ @collection_default_content(want_translators_count=True)
24+ def getDefaultLanguages(want_translators_count=False):
25 """An API wrapper for `common_languages`"""
26
27 common_languages = Attribute(
28
29=== modified file 'lib/lp/services/worlddata/model/language.py'
30--- lib/lp/services/worlddata/model/language.py 2010-12-02 16:13:51 +0000
31+++ lib/lp/services/worlddata/model/language.py 2011-03-07 07:51:04 +0000
32@@ -1,5 +1,6 @@
33-# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
34-# GNU Affero General Public License version 3 (see the file LICENSE).
35+# Copyright 2009-2010 Canonical Ltd. This software is licensed under
36+# the GNU Affero General Public License version 3 (see the file
37+# LICENSE).
38
39 # pylint: disable-msg=E0611,W0212
40
41@@ -17,17 +18,35 @@
42 SQLRelatedJoin,
43 StringCol,
44 )
45-from storm.locals import Or
46+from storm.expr import (
47+ And,
48+ Count,
49+ Desc,
50+ Join,
51+ LeftJoin,
52+ Or,
53+ )
54 from zope.interface import implements
55
56 from canonical.database.enumcol import EnumCol
57-from canonical.database.sqlbase import (
58- SQLBase,
59- sqlvalues,
60+from canonical.database.sqlbase import SQLBase
61+from canonical.launchpad.components.decoratedresultset import (
62+ DecoratedResultSet,
63 )
64 from canonical.launchpad.helpers import ensure_unicode
65-from canonical.launchpad.interfaces.lpstorm import ISlaveStore
66+from canonical.launchpad.interfaces.lpstorm import (
67+ ISlaveStore,
68+ IStore,
69+ )
70 from lp.app.errors import NotFoundError
71+from lp.registry.model.karma import (
72+ KarmaCache,
73+ KarmaCategory,
74+ )
75+from lp.services.propertycache import (
76+ cachedproperty,
77+ get_property_cache,
78+ )
79 from lp.services.worlddata.interfaces.language import (
80 ILanguage,
81 ILanguageSet,
82@@ -39,6 +58,7 @@
83 from lp.services.worlddata.model.spokenin import SpokenIn
84 SpokenIn
85
86+
87 class Language(SQLBase):
88 implements(ILanguage)
89
90@@ -140,34 +160,51 @@
91 @property
92 def translators(self):
93 """See `ILanguage`."""
94+ from lp.registry.model.person import (
95+ Person,
96+ PersonLanguage,
97+ )
98+ return IStore(Language).using(
99+ Join(
100+ Person,
101+ LanguageSet._getTranslatorJoins(),
102+ Person.id == PersonLanguage.personID),
103+ ).find(
104+ Person,
105+ PersonLanguage.language == self,
106+ ).order_by(Desc(KarmaCache.karmavalue))
107+
108+ @cachedproperty
109+ def translators_count(self):
110+ """See `ILanguage`."""
111+ return self.translators.count()
112+
113+
114+class LanguageSet:
115+ implements(ILanguageSet)
116+
117+ @staticmethod
118+ def _getTranslatorJoins():
119 # XXX CarlosPerelloMarin 2007-03-31 bug=102257:
120 # The KarmaCache table doesn't have a field to store karma per
121 # language, so we are actually returning the people with the most
122 # translation karma that have this language selected in their
123 # preferences.
124- from lp.registry.model.person import Person
125- return Person.select('''
126- PersonLanguage.person = Person.id AND
127- PersonLanguage.language = %s AND
128- KarmaCache.person = Person.id AND
129- KarmaCache.product IS NULL AND
130- KarmaCache.project IS NULL AND
131- KarmaCache.sourcepackagename IS NULL AND
132- KarmaCache.distribution IS NULL AND
133- KarmaCache.category = KarmaCategory.id AND
134- KarmaCategory.name = 'translations'
135- ''' % sqlvalues(self), orderBy=['-KarmaCache.karmavalue'],
136- clauseTables=[
137- 'PersonLanguage', 'KarmaCache', 'KarmaCategory'])
138-
139- @property
140- def translators_count(self):
141- """See `ILanguage`."""
142- return self.translators.count()
143-
144-
145-class LanguageSet:
146- implements(ILanguageSet)
147+ from lp.registry.model.person import PersonLanguage
148+ return Join(
149+ PersonLanguage,
150+ Join(
151+ KarmaCache,
152+ KarmaCategory,
153+ And(
154+ KarmaCategory.name == 'translations',
155+ KarmaCache.categoryID == KarmaCategory.id,
156+ KarmaCache.productID == None,
157+ KarmaCache.projectID == None,
158+ KarmaCache.sourcepackagenameID == None,
159+ KarmaCache.distributionID == None)),
160+ PersonLanguage.personID ==
161+ KarmaCache.personID)
162
163 @property
164 def _visible_languages(self):
165@@ -180,14 +217,39 @@
166 """See `ILanguageSet`."""
167 return iter(self._visible_languages)
168
169- def getDefaultLanguages(self):
170+ def getDefaultLanguages(self, want_translators_count=False):
171 """See `ILanguageSet`."""
172- return self._visible_languages
173+ return self.getAllLanguages(
174+ want_translators_count=want_translators_count,
175+ only_visible=True)
176
177- def getAllLanguages(self):
178+ def getAllLanguages(self, want_translators_count=False,
179+ only_visible=False):
180 """See `ILanguageSet`."""
181- return ISlaveStore(Language).find(Language).order_by(
182+ result = IStore(Language).find(
183+ Language,
184+ Language.visible == True if only_visible else True,
185+ ).order_by(
186 Language.englishname)
187+ if want_translators_count:
188+ def preload_translators_count(languages):
189+ from lp.registry.model.person import PersonLanguage
190+ ids = set(language.id for language in languages).difference(
191+ set([None]))
192+ counts = IStore(Language).using(
193+ LeftJoin(
194+ Language,
195+ self._getTranslatorJoins(),
196+ PersonLanguage.languageID == Language.id),
197+ ).find(
198+ (Language, Count(PersonLanguage)),
199+ Language.id.is_in(ids),
200+ ).group_by(Language)
201+ for language, count in counts:
202+ get_property_cache(language).translators_count = count
203+ return DecoratedResultSet(
204+ result, pre_iter_hook=preload_translators_count)
205+ return result
206
207 def __iter__(self):
208 """See `ILanguageSet`."""
209
210=== modified file 'lib/lp/services/worlddata/tests/test_language.py'
211--- lib/lp/services/worlddata/tests/test_language.py 2010-12-02 16:13:51 +0000
212+++ lib/lp/services/worlddata/tests/test_language.py 2011-03-07 07:51:04 +0000
213@@ -4,10 +4,26 @@
214 __metaclass__ = type
215
216 from lazr.lifecycle.interfaces import IDoNotSnapshot
217+from testtools.matchers import Equals
218+from zope.component import getUtility
219
220-from canonical.testing import DatabaseFunctionalLayer
221-from lp.services.worlddata.interfaces.language import ILanguage
222-from lp.testing import TestCaseWithFactory
223+from canonical.launchpad.interfaces.lpstorm import IStore
224+from canonical.testing import (
225+ DatabaseFunctionalLayer,
226+ LaunchpadZopelessLayer,
227+ )
228+from lp.registry.interfaces.karma import IKarmaCacheManager
229+from lp.registry.model.karma import KarmaCategory
230+from lp.services.worlddata.interfaces.language import (
231+ ILanguage,
232+ ILanguageSet,
233+ )
234+from lp.testing import (
235+ TestCaseWithFactory,
236+ StormStatementRecorder,
237+ )
238+from lp.testing.dbuser import dbuser
239+from lp.testing.matchers import HasQueryCount
240
241
242 class TestLanguageWebservice(TestCaseWithFactory):
243@@ -29,3 +45,44 @@
244 def test_guessed_pluralforms_knows(self):
245 language = self.factory.makeLanguage(pluralforms=3)
246 self.assertEqual(language.pluralforms, language.guessed_pluralforms)
247+
248+
249+class TestTranslatorsCounts(TestCaseWithFactory):
250+ """Test preloading of Language.translators_counts."""
251+
252+ layer = LaunchpadZopelessLayer
253+
254+ def setUp(self):
255+ super(TestTranslatorsCounts, self).setUp()
256+ self.translated_lang = self.factory.makeLanguage(pluralforms=None)
257+ self.untranslated_lang = self.factory.makeLanguage(pluralforms=None)
258+ for i in range(3):
259+ translator = self.factory.makePerson()
260+ translator.addLanguage(self.translated_lang)
261+ with dbuser('karma'):
262+ translations_category = IStore(KarmaCategory).find(
263+ KarmaCategory, name='translations').one()
264+ getUtility(IKarmaCacheManager).new(
265+ person_id=translator.id,
266+ category_id=translations_category.id,
267+ value=100)
268+
269+ def test_translators_count(self):
270+ # translators_count works.
271+ self.assertEquals(3, self.translated_lang.translators_count)
272+ self.assertEquals(0, self.untranslated_lang.translators_count)
273+
274+ def test_translators_count_queries(self):
275+ # translators_count issues a single query.
276+ with StormStatementRecorder() as recorder:
277+ self.translated_lang.translators_count
278+ self.assertThat(recorder, HasQueryCount(Equals(1)))
279+
280+ def test_getAllLanguages_can_preload_translators_count(self):
281+ # LanguageSet.getAllLanguages() can preload translators_count.
282+ list(getUtility(ILanguageSet).getAllLanguages(
283+ want_translators_count=True))
284+ with StormStatementRecorder() as recorder:
285+ self.assertEquals(3, self.translated_lang.translators_count)
286+ self.assertEquals(0, self.untranslated_lang.translators_count)
287+ self.assertThat(recorder, HasQueryCount(Equals(0)))