Merge lp:~jtv/launchpad/db-bug-782096 into lp:launchpad/db-devel

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 10573
Proposed branch: lp:~jtv/launchpad/db-bug-782096
Merge into: lp:launchpad/db-devel
Diff against target: 364 lines (+219/-10)
6 files modified
lib/lp/registry/browser/distroseries.py (+20/-0)
lib/lp/registry/browser/tests/test_distroseries.py (+27/-0)
lib/lp/registry/templates/distroseries-localdifferences.pt (+13/-7)
lib/lp/soyuz/interfaces/packagecopyjob.py (+15/-0)
lib/lp/soyuz/model/packagecopyjob.py (+43/-2)
lib/lp/soyuz/tests/test_packagecopyjob.py (+101/-1)
To merge this branch: bzr merge lp:~jtv/launchpad/db-bug-782096
Reviewer Review Type Date Requested Status
j.c.sackett (community) Approve
Review via email: mp+61612@code.launchpad.net

Commit message

[r=jcsackett][bug=782096] Active PlainPackageCopyJobs must be in WAITING state, and sort oldest first.

Description of the change

= Summary =

IPlainPackageCopyJobSource.getActiveJobs() had two flaws that made it a poor way to fetch the next job to execute:

1. It failed to check for job status, so unless finished jobs were immediately deleted, it would pick up completed or failed jobs.

2. It failed to impose a sane ordering, so it wouldn't necessarily pick up the oldest available job.

This fixes that.

The test uses the makeJob helper, which isn't actually in db-devel at the time of writing. But it's on the way through EC2 and should land soon. Tests pass when I merge in that branch, but I wanted to keep the diff minimal so I left it out. (There were no text conflicts to complicate this, luckily).

== Tests ==

{{{
./bin/test -vvc lp.soyuz.tests.test_packagecopyjob
}}}

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/soyuz/model/packagecopyjob.py
  lib/lp/soyuz/tests/test_packagecopyjob.py

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

With the return of the order_by, this looks good to land.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/distroseries.py'
2--- lib/lp/registry/browser/distroseries.py 2011-05-18 18:39:03 +0000
3+++ lib/lp/registry/browser/distroseries.py 2011-05-19 17:24:21 +0000
4@@ -105,6 +105,7 @@
5 from lp.soyuz.browser.packagesearch import PackageSearchViewBase
6 from lp.soyuz.interfaces.packagecopyjob import IPlainPackageCopyJobSource
7 from lp.soyuz.interfaces.queue import IPackageUploadSet
8+from lp.soyuz.model.packagecopyjob import specify_dsd_package
9 from lp.soyuz.model.queue import PackageUploadQueue
10 from lp.translations.browser.distroseries import (
11 check_distroseries_translations_viewable,
12@@ -851,6 +852,25 @@
13 return (has_perm and
14 self.cached_differences.batch.total() > 0)
15
16+ @cachedproperty
17+ def pending_syncs(self):
18+ """Pending synchronization jobs for this distroseries.
19+
20+ :return: A dict mapping (name, version) package specifications to
21+ pending sync jobs.
22+ """
23+ job_source = getUtility(IPlainPackageCopyJobSource)
24+ return job_source.getPendingJobsPerPackage(self.context)
25+
26+ def hasPendingSync(self, dsd):
27+ """Is there a package-copying job pending to resolve `dsd`?"""
28+ return self.pending_syncs.get(specify_dsd_package(dsd)) is not None
29+
30+ def canRequestSync(self, dsd):
31+ """Does it make sense to request a sync for this difference?"""
32+ # XXX JeroenVermeulen bug=783435: Also compare versions.
33+ return not self.hasPendingSync(dsd)
34+
35 @property
36 def specified_name_filter(self):
37 """If specified, return the name filter from the GET form data."""
38
39=== modified file 'lib/lp/registry/browser/tests/test_distroseries.py'
40--- lib/lp/registry/browser/tests/test_distroseries.py 2011-05-19 02:36:21 +0000
41+++ lib/lp/registry/browser/tests/test_distroseries.py 2011-05-19 17:24:21 +0000
42@@ -72,6 +72,7 @@
43 ISourcePackageFormatSelectionSet,
44 )
45 from lp.soyuz.model.archivepermission import ArchivePermission
46+from lp.soyuz.model.packagecopyjob import specify_dsd_package
47 from lp.testing import (
48 anonymous_logged_in,
49 celebrity_logged_in,
50@@ -1348,6 +1349,32 @@
51
52 self.assertFalse(view.canPerformSync())
53
54+ def test_hasPendingSync_returns_False_if_no_pending_sync(self):
55+ dsd = self.factory.makeDistroSeriesDifference()
56+ view = create_initialized_view(
57+ dsd.derived_series, '+localpackagediffs')
58+ self.assertFalse(view.hasPendingSync(dsd))
59+
60+ def test_hasPendingSync_returns_True_if_pending_sync(self):
61+ dsd = self.factory.makeDistroSeriesDifference()
62+ view = create_initialized_view(
63+ dsd.derived_series, '+localpackagediffs')
64+ view.pending_syncs = {specify_dsd_package(dsd): object()}
65+ self.assertTrue(view.hasPendingSync(dsd))
66+
67+ def test_canRequestSync_returns_False_if_pending_sync(self):
68+ dsd = self.factory.makeDistroSeriesDifference()
69+ view = create_initialized_view(
70+ dsd.derived_series, '+localpackagediffs')
71+ view.pending_syncs = {specify_dsd_package(dsd): object()}
72+ self.assertFalse(view.canRequestSync(dsd))
73+
74+ def test_canRequestSync_returns_True_if_sync_makes_sense(self):
75+ dsd = self.factory.makeDistroSeriesDifference()
76+ view = create_initialized_view(
77+ dsd.derived_series, '+localpackagediffs')
78+ self.assertTrue(view.canRequestSync(dsd))
79+
80 def _syncAndGetView(self, derived_series, person, sync_differences,
81 difference_type=None, view_name='+localpackagediffs'):
82 # A helper to get the POST'ed sync view.
83
84=== modified file 'lib/lp/registry/templates/distroseries-localdifferences.pt'
85--- lib/lp/registry/templates/distroseries-localdifferences.pt 2011-05-13 15:13:54 +0000
86+++ lib/lp/registry/templates/distroseries-localdifferences.pt 2011-05-19 17:24:21 +0000
87@@ -74,14 +74,20 @@
88 tal:attributes="class src_name">
89
90 <td>
91- <input tal:condition="can_perform_sync"
92- name="field.selected_differences" type="checkbox"
93- tal:attributes="
94- value diff_id;
95- id string:field.selected_differences.${diff_id}"/>
96+ <tal:checkbox tal:condition="python: view.canRequestSync(difference)">
97+ <input tal:condition="can_perform_sync"
98+ name="field.selected_differences" type="checkbox"
99+ tal:attributes="
100+ value diff_id;
101+ id string:field.selected_differences.${diff_id}"/>
102
103- <a tal:attributes="href difference/fmt:url" class="toggle-extra"
104- tal:content="src_name">Foo</a>
105+ <a tal:attributes="href difference/fmt:url" class="toggle-extra"
106+ tal:content="src_name">Foo</a>
107+ </tal:checkbox>
108+ <tal:clockface condition="python: view.hasPendingSync(difference)">
109+ <span class="sprite build_depwait"></span>
110+ <i>(Synchronizing)</i>
111+ </tal:clockface>
112 </td>
113 <td tal:condition="python: not(view.has_unique_parent) and view.show_parent_version">
114 <a tal:attributes="href difference/parent_series/fmt:url"
115
116=== modified file 'lib/lp/soyuz/interfaces/packagecopyjob.py'
117--- lib/lp/soyuz/interfaces/packagecopyjob.py 2011-05-13 08:21:38 +0000
118+++ lib/lp/soyuz/interfaces/packagecopyjob.py 2011-05-19 17:24:21 +0000
119@@ -103,6 +103,21 @@
120 def getActiveJobs(target_archive):
121 """Retrieve all active sync jobs for an archive."""
122
123+ def getPendingJobsPerPackage(target_series):
124+ """Find pending jobs for each package in `target_series`.
125+
126+ This is meant for finding jobs that will resolve specific
127+ `DistroSeriesDifference`s, so see also `specify_dsd_package`.
128+
129+ :param target_series: Target `DistroSeries`; this corresponds to
130+ `DistroSeriesDifference.derived_series`.
131+ :return: A dict containing as keys the (name, version) tuples for
132+ each `DistroSeriesDifference` that has a resolving
133+ `PlainPackageCopyJob` pending. Each of these DSDs maps to its
134+ oldest pending job. The `version` corresponds to
135+ `DistroSeriesDifference.parent_source_version`.
136+ """
137+
138
139 class IPlainPackageCopyJob(IRunnableJob):
140 """A no-frills job to copy packages between `IArchive`s."""
141
142=== modified file 'lib/lp/soyuz/model/packagecopyjob.py'
143--- lib/lp/soyuz/model/packagecopyjob.py 2011-05-10 09:15:18 +0000
144+++ lib/lp/soyuz/model/packagecopyjob.py 2011-05-19 17:24:21 +0000
145@@ -1,4 +1,4 @@
146-# Copyright 2010 Canonical Ltd. This software is licensed under the
147+# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
148 # GNU Affero General Public License version 3 (see the file LICENSE).
149
150 __metaclass__ = type
151@@ -6,6 +6,7 @@
152 __all__ = [
153 "PackageCopyJob",
154 "PlainPackageCopyJob",
155+ "specify_dsd_package",
156 ]
157
158 from lazr.delegates import delegates
159@@ -33,6 +34,7 @@
160 from lp.registry.interfaces.pocket import PackagePublishingPocket
161 from lp.registry.model.distroseries import DistroSeries
162 from lp.services.database.stormbase import StormBase
163+from lp.services.job.interfaces.job import JobStatus
164 from lp.services.job.model.job import Job
165 from lp.services.job.runner import BaseRunnableJob
166 from lp.soyuz.interfaces.archive import CannotCopy
167@@ -46,6 +48,17 @@
168 from lp.soyuz.scripts.packagecopier import do_copy
169
170
171+def specify_dsd_package(dsd):
172+ """Return (name, parent version) for `dsd`'s package.
173+
174+ This describes the package that `dsd` is for in a format suitable for
175+ `PlainPackageCopyJobSource`.
176+
177+ :param dsd: A `DistroSeriesDifference`.
178+ """
179+ return (dsd.source_package_name.name, dsd.parent_source_version)
180+
181+
182 class PackageCopyJob(StormBase):
183 """Base class for package copying jobs."""
184
185@@ -171,9 +184,37 @@
186 jobs = IStore(PackageCopyJob).find(
187 PackageCopyJob,
188 PackageCopyJob.job_type == cls.class_job_type,
189- PackageCopyJob.target_archive == target_archive)
190+ PackageCopyJob.target_archive == target_archive,
191+ Job.id == PackageCopyJob.job_id,
192+ Job._status == JobStatus.WAITING)
193+ jobs = jobs.order_by(PackageCopyJob.id)
194 return DecoratedResultSet(jobs, cls)
195
196+ @classmethod
197+ def getPendingJobsForTargetSeries(cls, target_series):
198+ """Get upcoming jobs for `target_series`, ordered by age."""
199+ raw_jobs = IStore(PackageCopyJob).find(
200+ PackageCopyJob,
201+ Job.id == PackageCopyJob.job_id,
202+ PackageCopyJob.job_type == cls.class_job_type,
203+ PackageCopyJob.target_distroseries == target_series,
204+ Job._status.is_in(Job.PENDING_STATUSES))
205+ raw_jobs = raw_jobs.order_by(PackageCopyJob.id)
206+ return DecoratedResultSet(raw_jobs, cls)
207+
208+ @classmethod
209+ def getPendingJobsPerPackage(cls, target_series):
210+ """See `IPlainPackageCopyJobSource`."""
211+ result = {}
212+ # Go through jobs in-order, picking the first matching job for
213+ # any (package, version) tuple. Because of how
214+ # getPendingJobsForTargetSeries orders its results, the first
215+ # will be the oldest and thus presumably the first to finish.
216+ for job in cls.getPendingJobsForTargetSeries(target_series):
217+ for package in job.metadata["source_packages"]:
218+ result.setdefault(tuple(package), job)
219+ return result
220+
221 @property
222 def source_packages(self):
223 getPublishedSources = self.source_archive.getPublishedSources
224
225=== modified file 'lib/lp/soyuz/tests/test_packagecopyjob.py'
226--- lib/lp/soyuz/tests/test_packagecopyjob.py 2011-05-10 09:15:18 +0000
227+++ lib/lp/soyuz/tests/test_packagecopyjob.py 2011-05-19 17:24:21 +0000
228@@ -1,4 +1,4 @@
229-# Copyright 2010 Canonical Ltd. This software is licensed under the
230+# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
231 # GNU Affero General Public License version 3 (see the file LICENSE).
232
233 """Tests for sync package jobs."""
234@@ -10,12 +10,14 @@
235
236 from canonical.testing import LaunchpadZopelessLayer
237 from lp.registry.interfaces.pocket import PackagePublishingPocket
238+from lp.services.job.interfaces.job import JobStatus
239 from lp.soyuz.interfaces.archive import CannotCopy
240 from lp.soyuz.interfaces.packagecopyjob import (
241 IPackageCopyJob,
242 IPlainPackageCopyJobSource,
243 )
244 from lp.soyuz.interfaces.publishing import PackagePublishingStatus
245+from lp.soyuz.model.packagecopyjob import specify_dsd_package
246 from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
247 from lp.testing import (
248 run_script,
249@@ -28,6 +30,17 @@
250
251 layer = LaunchpadZopelessLayer
252
253+ def makeJob(self, dsd):
254+ """Create a `PlainPackageCopyJob` that would resolve `dsd`."""
255+ source_packages = [specify_dsd_package(dsd)]
256+ source_archive = dsd.parent_series.main_archive
257+ target_archive = dsd.derived_series.main_archive
258+ target_distroseries = dsd.derived_series
259+ target_pocket = self.factory.getAnyPocket()
260+ return getUtility(IPlainPackageCopyJobSource).create(
261+ source_packages, source_archive, target_archive,
262+ target_distroseries, target_pocket)
263+
264 def test_create(self):
265 # A PackageCopyJob can be created and stores its arguments.
266 distroseries = self.factory.makeDistroSeries()
267@@ -65,6 +78,21 @@
268 include_binaries=False)
269 self.assertContentEqual([job], source.getActiveJobs(archive2))
270
271+ def test_getActiveJobs_gets_oldest_first(self):
272+ # getActiveJobs returns the oldest available job first.
273+ dsd = self.factory.makeDistroSeriesDifference()
274+ target_archive = dsd.derived_series.main_archive
275+ jobs = [self.makeJob(dsd) for counter in xrange(2)]
276+ source = getUtility(IPlainPackageCopyJobSource)
277+ self.assertEqual(jobs[0], source.getActiveJobs(target_archive)[0])
278+
279+ def test_getActiveJobs_only_returns_waiting_jobs(self):
280+ # getActiveJobs ignores jobs that aren't in the WAITING state.
281+ job = self.makeJob(self.factory.makeDistroSeriesDifference())
282+ removeSecurityProxy(job).job._status = JobStatus.RUNNING
283+ source = getUtility(IPlainPackageCopyJobSource)
284+ self.assertContentEqual([], source.getActiveJobs(job.target_archive))
285+
286 def test_run_unknown_package(self):
287 # A job properly records failure.
288 distroseries = self.factory.makeDistroSeries()
289@@ -200,3 +228,75 @@
290 distroseries=distroseries, archive1=archive1,
291 archive2=archive2),
292 repr(job))
293+
294+ def test_getPendingJobsPerPackage_finds_jobs(self):
295+ # getPendingJobsPerPackage finds jobs, and the packages they
296+ # belong to.
297+ dsd = self.factory.makeDistroSeriesDifference()
298+ job = self.makeJob(dsd)
299+ job_source = getUtility(IPlainPackageCopyJobSource)
300+ self.assertEqual(
301+ {specify_dsd_package(dsd): job},
302+ job_source.getPendingJobsPerPackage(dsd.derived_series))
303+
304+ def test_getPendingJobsPerPackage_ignores_other_distroseries(self):
305+ # getPendingJobsPerPackage only looks for jobs on the indicated
306+ # distroseries.
307+ dsd = self.factory.makeDistroSeriesDifference()
308+ self.makeJob(dsd)
309+ other_series = self.factory.makeDistroSeries()
310+ job_source = getUtility(IPlainPackageCopyJobSource)
311+ self.assertEqual(
312+ {}, job_source.getPendingJobsPerPackage(other_series))
313+
314+ def test_getPendingJobsPerPackage_only_returns_pending_jobs(self):
315+ # getPendingJobsPerPackage ignores jobs that have already been
316+ # run.
317+ dsd = self.factory.makeDistroSeriesDifference()
318+ package = specify_dsd_package(dsd)
319+ job = self.makeJob(dsd)
320+ job_source = getUtility(IPlainPackageCopyJobSource)
321+ found_by_state = {}
322+ for status in JobStatus.items:
323+ removeSecurityProxy(job).job._status = status
324+ result = job_source.getPendingJobsPerPackage(dsd.derived_series)
325+ if len(result) > 0:
326+ found_by_state[status] = result[package]
327+ expected = {
328+ JobStatus.WAITING: job,
329+ JobStatus.RUNNING: job,
330+ JobStatus.SUSPENDED: job,
331+ }
332+ self.assertEqual(expected, found_by_state)
333+
334+ def test_getPendingJobsPerPackage_distinguishes_jobs(self):
335+ # getPendingJobsPerPackage associates the right job with the
336+ # right package.
337+ derived_series = self.factory.makeDistroSeries()
338+ dsds = [
339+ self.factory.makeDistroSeriesDifference(
340+ derived_series=derived_series)
341+ for counter in xrange(2)]
342+ jobs = map(self.makeJob, dsds)
343+ job_source = getUtility(IPlainPackageCopyJobSource)
344+ self.assertEqual(
345+ dict(zip(map(specify_dsd_package, dsds), jobs)),
346+ job_source.getPendingJobsPerPackage(derived_series))
347+
348+ def test_getPendingJobsPerPackage_picks_oldest_job_for_dsd(self):
349+ # If there are multiple jobs for one package,
350+ # getPendingJobsPerPackage picks the oldest.
351+ dsd = self.factory.makeDistroSeriesDifference()
352+ jobs = [self.makeJob(dsd) for counter in xrange(2)]
353+ job_source = getUtility(IPlainPackageCopyJobSource)
354+ self.assertEqual(
355+ {specify_dsd_package(dsd): jobs[0]},
356+ job_source.getPendingJobsPerPackage(dsd.derived_series))
357+
358+ def test_getPendingJobsPerPackage_ignores_dsds_without_jobs(self):
359+ # getPendingJobsPerPackage produces no dict entry for packages
360+ # that have no pending jobs, even if they do have DSDs.
361+ dsd = self.factory.makeDistroSeriesDifference()
362+ job_source = getUtility(IPlainPackageCopyJobSource)
363+ self.assertEqual(
364+ {}, job_source.getPendingJobsPerPackage(dsd.derived_series))

Subscribers

People subscribed via source and target branches

to status/vote changes: