Merge lp:~benji/launchpad/bug-877195-code into lp:launchpad

Proposed by Benji York
Status: Merged
Approved by: j.c.sackett
Approved revision: no longer in the source branch.
Merged at revision: 14364
Proposed branch: lp:~benji/launchpad/bug-877195-code
Merge into: lp:launchpad
Prerequisite: lp:~benji/launchpad/bug-877195-db
Diff against target: 253 lines (+197/-4)
6 files modified
lib/canonical/config/schema-lazr.conf (+7/-0)
lib/lp/translations/browser/translationmessage.py (+3/-4)
lib/lp/translations/configure.zcml (+9/-0)
lib/lp/translations/interfaces/pofilestatsjob.py (+16/-0)
lib/lp/translations/model/pofilestatsjob.py (+88/-0)
lib/lp/translations/tests/test_pofilestatsjob.py (+74/-0)
To merge this branch: bzr merge lp:~benji/launchpad/bug-877195-code
Reviewer Review Type Date Requested Status
j.c.sackett (community) Approve
Review via email: mp+80619@code.launchpad.net

This proposal supersedes a proposal from 2011-10-27.

Commit message

[r=jcsackett] move POFile statistics update into a job

Description of the change

When a change is made to a translation several statistics about that
translation are updated. That update is slow. So slow that it often
times out. Bug 877195 is about moving those statistics updates into a
job so the web requests can return faster but the statistics will still
be relatively up to date.

I had pre-implementation calls with Danilo.

There is a prerequisite branch that includes the database changes.

Tests for the new job are in
lib/lp/translations/tests/test_pofilestatsjob.py and can be run like so:

    bin/test -c -m lp.translations.tests.test_pofilestatsjob

The "make lint" report is clean.

QA: note a translations statistics, make a change to a translation that
would impact its statistics, get a LOSA to run the cron job, verify that
the statistics changed.

To post a comment you must log in.
Revision history for this message
j.c.sackett (jcsackett) wrote :

This looks good to me, Benji. Thanks for offloading this work into a job, sounds like a good fix.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/config/schema-lazr.conf'
2--- lib/canonical/config/schema-lazr.conf 2011-11-14 06:04:41 +0000
3+++ lib/canonical/config/schema-lazr.conf 2011-11-22 05:26:23 +0000
4@@ -1855,6 +1855,13 @@
5 dbuser: send-branch-mail
6 storm_cache: generational
7 storm_cache_size: 500
8+oops_prefix: none
9+error_dir: none
10+
11+[pofile_stats]
12+dbuser: pofilestats
13+storm_cache: generational
14+source_interface: lp.translations.interfaces.pofilestatsjob.IPOFileStatsJobSource
15
16 # See [error_reports].
17 error_dir: none
18
19=== modified file 'lib/lp/translations/browser/translationmessage.py'
20--- lib/lp/translations/browser/translationmessage.py 2011-07-13 06:08:16 +0000
21+++ lib/lp/translations/browser/translationmessage.py 2011-11-22 05:26:23 +0000
22@@ -70,6 +70,7 @@
23 )
24 from lp.translations.interfaces.translations import TranslationConstants
25 from lp.translations.interfaces.translationsperson import ITranslationsPerson
26+from lp.translations.model import pofilestatsjob
27 from lp.translations.utilities.sanitize import (
28 sanitize_translations_from_webui,
29 )
30@@ -892,10 +893,8 @@
31
32 def _redirectToNextPage(self):
33 """After a successful submission, redirect to the next batch page."""
34- # XXX: kiko 2006-09-27:
35- # Isn't this a hell of a performance issue, hitting this
36- # same table for every submit?
37- self.pofile.updateStatistics()
38+ # Schedule this POFile to have its statistics updated.
39+ pofilestatsjob.schedule(self.pofile)
40 next_url = self.batchnav.nextBatchURL()
41 if next_url is None or next_url == '':
42 # We are already at the end of the batch, forward to the
43
44=== modified file 'lib/lp/translations/configure.zcml'
45--- lib/lp/translations/configure.zcml 2011-10-18 10:09:58 +0000
46+++ lib/lp/translations/configure.zcml 2011-11-22 05:26:23 +0000
47@@ -694,4 +694,13 @@
48
49 <webservice:register module="lp.translations.interfaces.webservice" />
50
51+ <class
52+ class="lp.translations.model.pofilestatsjob.POFileStatsJob">
53+ <allow interface='lp.services.job.interfaces.job.IRunnableJob'/>
54+ </class>
55+ <utility
56+ component="lp.translations.model.pofilestatsjob.POFileStatsJob"
57+ provides="lp.translations.interfaces.pofilestatsjob.IPOFileStatsJobSource"
58+ />
59+
60 </configure>
61
62=== added file 'lib/lp/translations/interfaces/pofilestatsjob.py'
63--- lib/lp/translations/interfaces/pofilestatsjob.py 1970-01-01 00:00:00 +0000
64+++ lib/lp/translations/interfaces/pofilestatsjob.py 2011-11-22 05:26:23 +0000
65@@ -0,0 +1,16 @@
66+# Copyright 2010 Canonical Ltd. This software is licensed under the
67+# GNU Affero General Public License version 3 (see the file LICENSE).
68+
69+# pylint: disable-msg=E0213
70+
71+__metaclass__ = type
72+
73+__all__ = [
74+ 'IPOFileStatsJobSource',
75+ ]
76+
77+from lp.services.job.interfaces.job import IJobSource
78+
79+
80+class IPOFileStatsJobSource(IJobSource):
81+ """A source for jobs to update POFile statistics."""
82
83=== added file 'lib/lp/translations/model/pofilestatsjob.py'
84--- lib/lp/translations/model/pofilestatsjob.py 1970-01-01 00:00:00 +0000
85+++ lib/lp/translations/model/pofilestatsjob.py 2011-11-22 05:26:23 +0000
86@@ -0,0 +1,88 @@
87+# Copyright 2011 Canonical Ltd. This software is licensed under the
88+# GNU Affero General Public License version 3 (see the file LICENSE).
89+
90+"""Job for merging translations."""
91+
92+
93+__metaclass__ = type
94+
95+
96+__all__ = [
97+ 'POFileStatsJob',
98+ ]
99+
100+import logging
101+from zope.component import getUtility
102+
103+from storm.locals import (
104+ And,
105+ Int,
106+ Reference,
107+ )
108+from zope.interface import (
109+ classProvides,
110+ implements,
111+ )
112+
113+from canonical.launchpad.webapp.interfaces import (
114+ DEFAULT_FLAVOR,
115+ IStoreSelector,
116+ MAIN_STORE,
117+ )
118+from lp.services.database.stormbase import StormBase
119+from lp.services.job.interfaces.job import IRunnableJob
120+from lp.services.job.model.job import Job
121+from lp.services.job.runner import BaseRunnableJob
122+from lp.translations.interfaces.pofilestatsjob import IPOFileStatsJobSource
123+from lp.translations.model.pofile import POFile
124+
125+
126+class POFileStatsJob(StormBase, BaseRunnableJob):
127+ """The details for a POFile status update job."""
128+
129+ __storm_table__ = 'POFileStatsJob'
130+
131+ # Instances of this class are runnable jobs.
132+ implements(IRunnableJob)
133+
134+ # Oddly, BaseRunnableJob inherits from BaseRunnableJobSource so this class
135+ # is both the factory for jobs (the "implements", above) and the source
136+ # for runnable jobs (not the constructor of the job source, the class
137+ # provides the IJobSource interface itself).
138+ classProvides(IPOFileStatsJobSource)
139+
140+ # The Job table contains core job details.
141+ job_id = Int('job', primary=True)
142+ job = Reference(job_id, Job.id)
143+
144+ # This is the POFile which needs its statistics updated.
145+ pofile_id = Int('pofile')
146+ pofile = Reference(pofile_id, POFile.id)
147+
148+ def __init__(self, pofile):
149+ self.job = Job()
150+ self.pofile = pofile
151+ super(POFileStatsJob, self).__init__()
152+
153+ def getOperationDescription(self):
154+ """See `IRunnableJob`."""
155+ return 'updating POFile statistics'
156+
157+ def run(self):
158+ """See `IRunnableJob`."""
159+ logger = logging.getLogger()
160+ logger.info('Updating statistics for %s' % self.pofile.title)
161+ self.pofile.updateStatistics()
162+
163+ @staticmethod
164+ def iterReady():
165+ """See `IJobSource`."""
166+ store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
167+ return store.find((POFileStatsJob),
168+ And(POFileStatsJob.job == Job.id,
169+ Job.id.is_in(Job.ready_jobs)))
170+
171+
172+def schedule(pofile):
173+ """Schedule a job to update a POFile's stats."""
174+ return POFileStatsJob(pofile)
175
176=== added file 'lib/lp/translations/tests/test_pofilestatsjob.py'
177--- lib/lp/translations/tests/test_pofilestatsjob.py 1970-01-01 00:00:00 +0000
178+++ lib/lp/translations/tests/test_pofilestatsjob.py 2011-11-22 05:26:23 +0000
179@@ -0,0 +1,74 @@
180+# Copyright 2011 Canonical Ltd. This software is licensed under the
181+# GNU Affero General Public License version 3 (see the file LICENSE).
182+
183+"""Tests for merging translations."""
184+
185+__metaclass__ = type
186+
187+
188+from canonical.launchpad.webapp.testing import verifyObject
189+from canonical.testing.layers import (
190+ LaunchpadZopelessLayer,
191+ )
192+from lp.services.job.interfaces.job import (
193+ IJobSource,
194+ IRunnableJob,
195+ )
196+from lp.testing import TestCaseWithFactory
197+from lp.translations.interfaces.pofilestatsjob import IPOFileStatsJobSource
198+from lp.translations.interfaces.side import TranslationSide
199+from lp.translations.model import pofilestatsjob
200+from lp.translations.model.pofilestatsjob import POFileStatsJob
201+
202+
203+class TestPOFileStatsJob(TestCaseWithFactory):
204+
205+ layer = LaunchpadZopelessLayer
206+
207+ def test_job_interface(self):
208+ # Instances of POFileStatsJob are runnable jobs.
209+ verifyObject(IRunnableJob, POFileStatsJob(0))
210+
211+ def test_source_interface(self):
212+ # The POFileStatsJob class is a source of POFileStatsJobs.
213+ verifyObject(IPOFileStatsJobSource, POFileStatsJob)
214+ verifyObject(IJobSource, POFileStatsJob)
215+
216+ def test_run(self):
217+ # Running a job causes the POFile statistics to be updated.
218+ singular = self.factory.getUniqueString()
219+ pofile = self.factory.makePOFile(side=TranslationSide.UPSTREAM)
220+ # Create a message so we have something to have statistics about.
221+ self.factory.makePOTMsgSet(pofile.potemplate, singular)
222+ # The statistics start at 0.
223+ self.assertEqual(pofile.potemplate.messageCount(), 0)
224+ job = pofilestatsjob.schedule(pofile.id)
225+ # Just scheduling the job doesn't update the statistics.
226+ self.assertEqual(pofile.potemplate.messageCount(), 0)
227+ job.run()
228+ # Now that the job ran, the statistics have been updated.
229+ self.assertEqual(pofile.potemplate.messageCount(), 1)
230+
231+ def test_iterReady(self):
232+ # The POFileStatsJob class provides a way to iterate over the jobs
233+ # that are ready to run. Initially, there aren't any.
234+ self.assertEqual(len(list(POFileStatsJob.iterReady())), 0)
235+ # We need a POFile to update.
236+ pofile = self.factory.makePOFile(side=TranslationSide.UPSTREAM)
237+ # If we schedule a job, then we'll get it back.
238+ job = pofilestatsjob.schedule(pofile.id)
239+ self.assertIs(list(POFileStatsJob.iterReady())[0], job)
240+
241+ def test_second_job_is_scheduled(self):
242+ # If there is already one POFileStatsJob scheduled for a particular
243+ # POFile, then a second one is scheduled.
244+ self.assertEqual(len(list(POFileStatsJob.iterReady())), 0)
245+ # We need a POFile to update.
246+ pofile = self.factory.makePOFile(side=TranslationSide.UPSTREAM)
247+ # If we schedule a job, then there will be one scheduled.
248+ pofilestatsjob.schedule(pofile.id)
249+ self.assertIs(len(list(POFileStatsJob.iterReady())), 1)
250+ # If we attempt to schedule another job for the same POFile, a new job
251+ # is added.
252+ pofilestatsjob.schedule(pofile.id)
253+ self.assertIs(len(list(POFileStatsJob.iterReady())), 2)