Merge lp:~jtv/launchpad/pre-832647 into lp:launchpad

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: William Grant
Approved revision: no longer in the source branch.
Merged at revision: 13911
Proposed branch: lp:~jtv/launchpad/pre-832647
Merge into: lp:launchpad
Diff against target: 502 lines (+280/-65)
5 files modified
lib/lp/archivepublisher/domination.py (+122/-50)
lib/lp/archivepublisher/tests/test_dominator.py (+150/-2)
lib/lp/soyuz/interfaces/publishing.py (+0/-3)
lib/lp/soyuz/model/publishing.py (+4/-4)
scripts/gina.py (+4/-6)
To merge this branch: bzr merge lp:~jtv/launchpad/pre-832647
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+74087@code.launchpad.net

Commit message

[r=wgrant][bug=832647] Rearrange bits of domcination and gina.

Description of the change

= Summary =

Moving some code about to prepare for the introduction of domination into gina.

== Pre-implementation notes ==

This is the product of extensive discussions with William and Julian, but I'm sort of empty right now and can't reproduce it all! The upshot of it is that they both think what I'm trying to do in the larger picture would work.

== Implementation details ==

The gina changes aren't very important; they're basically just putting the horse back before the cart.

Then there's the publisher changes: a base class has 2 child classes, BPPH and SPPH. (Or more formally, BinaryPackagePublishingHistory and SourcePackagePublishingHistory). Each of these 3 classes has a method “supersede,” with the two derived-class methods calling the base-class one. The base class is never instantiated, so there's really no reason to give its method the same name. I renamed it “setSuperseded” to match the new structure of requestDeletion calling setDeleted.

But the main course is in the dominator. Code in this class is very aggressively unified between BPPH and SPPH. That makes it a little harder to work with. I extracted the specializations away underneath a separate class, really a sub-module of helper functions that hide the differences between BPPH and SPPH. And finally I extracted a few smaller helpers that exploratory coding reveals a need for, and which I think help make the code more readable.

== Tests ==

{{{
./bin/test -vvc lp.archivepublisher
./bin/test -vvc lp.soyuz
}}}

= Launchpad lint =

Nothing to do about the remaining lint, I think; it was all present before I started. Last I heard, someone or other was working to retract the "two spaces" rule so I did nothing about them.

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/archivepublisher/domination.py
  lib/lp/soyuz/model/publishing.py
  lib/lp/soyuz/interfaces/publishing.py
  scripts/gina.py
  lib/lp/archivepublisher/tests/test_dominator.py

./lib/lp/soyuz/interfaces/publishing.py
     381: E261 at least two spaces before inline comment
     478: E261 at least two spaces before inline comment
     511: E261 at least two spaces before inline comment
     681: E261 at least two spaces before inline comment
     767: E261 at least two spaces before inline comment
./scripts/gina.py
      26: '_pythonpath' imported but unused

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) wrote :

189 + def _composeActiveSourcePubsCondition(self, distroseries, pocket):

It seems slightly odd to have this take distroseries and pocket, but grab archive from self. This is a common problem in Soyuz, since archives didn't exist when most of it was written. Something to think about, maybe.

234 + And(join_spr_spn(), join_spph_spr(), spph_location_clauses),

This would possibly be clearer if the order of arguments was reversed. The (SPR, SPN) -> (SPPH, SPR) -> SPPH chaining takes a while to pick up here.

319 + def listCreaitonDates(self, spphs):

Unused and misspelled :(

review: Approve (code)
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

> 189 + def _composeActiveSourcePubsCondition(self, distroseries, pocket):
>
> It seems slightly odd to have this take distroseries and pocket, but grab
> archive from self. This is a common problem in Soyuz, since archives didn't
> exist when most of it was written. Something to think about, maybe.

It did not escape notice. But the API reflects the current API of the dominator itself, and will have to rearranged in conjunction with it.

> 234 + And(join_spr_spn(), join_spph_spr(), spph_location_clauses),
>
> This would possibly be clearer if the order of arguments was reversed. The
> (SPR, SPN) -> (SPPH, SPR) -> SPPH chaining takes a while to pick up here.

True, it helps. I switched the first two around, but kept join conditions before selection conditions.

> 319 + def listCreaitonDates(self, spphs):
>
> Unused and misspelled :(

Removed.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/archivepublisher/domination.py'
2--- lib/lp/archivepublisher/domination.py 2011-08-30 06:37:55 +0000
3+++ lib/lp/archivepublisher/domination.py 2011-09-05 12:08:08 +0000
4@@ -53,8 +53,6 @@
5 __all__ = ['Dominator']
6
7 from datetime import timedelta
8-import functools
9-import operator
10
11 import apt_pkg
12 from storm.expr import (
13@@ -68,7 +66,7 @@
14 flush_database_updates,
15 sqlvalues,
16 )
17-from canonical.launchpad.interfaces.lpstorm import IMasterStore
18+from canonical.launchpad.interfaces.lpstorm import IStore
19 from lp.registry.model.sourcepackagename import SourcePackageName
20 from lp.soyuz.enums import (
21 BinaryPackageFormat,
22@@ -87,17 +85,93 @@
23 apt_pkg.InitSystem()
24
25
26-def _compare_packages_by_version_and_date(get_release, p1, p2):
27- """Compare publications p1 and p2 by their version; using Debian rules.
28-
29- If the publications are for the same package, compare by datecreated
30- instead. This lets newer records win.
31- """
32- if get_release(p1).id == get_release(p2).id:
33- return cmp(p1.datecreated, p2.datecreated)
34-
35- return apt_pkg.VersionCompare(get_release(p1).version,
36- get_release(p2).version)
37+def join_spr_spn():
38+ """Join condition: SourcePackageRelease/SourcePackageName."""
39+ return (
40+ SourcePackageName.id == SourcePackageRelease.sourcepackagenameID)
41+
42+
43+def join_spph_spr():
44+ """Join condition: SourcePackageRelease/SourcePackagePublishingHistory.
45+ """
46+ # Avoid circular imports.
47+ from lp.soyuz.model.publishing import SourcePackagePublishingHistory
48+
49+ return (
50+ SourcePackageRelease.id ==
51+ SourcePackagePublishingHistory.sourcepackagereleaseID)
52+
53+
54+class SourcePublicationTraits:
55+ """Basic generalized attributes for `SourcePackagePublishingHistory`.
56+
57+ Used by `GeneralizedPublication` to hide the differences from
58+ `BinaryPackagePublishingHistory`.
59+ """
60+ @staticmethod
61+ def getPackageName(spph):
62+ """Return the name of this publication's source package."""
63+ return spph.sourcepackagerelease.sourcepackagename.name
64+
65+ @staticmethod
66+ def getPackageRelease(spph):
67+ """Return this publication's `SourcePackageRelease`."""
68+ return spph.sourcepackagerelease
69+
70+
71+class BinaryPublicationTraits:
72+ """Basic generalized attributes for `BinaryPackagePublishingHistory`.
73+
74+ Used by `GeneralizedPublication` to hide the differences from
75+ `SourcePackagePublishingHistory`.
76+ """
77+ @staticmethod
78+ def getPackageName(bpph):
79+ """Return the name of this publication's binary package."""
80+ return bpph.binarypackagerelease.binarypackagename.name
81+
82+ @staticmethod
83+ def getPackageRelease(bpph):
84+ """Return this publication's `BinaryPackageRelease`."""
85+ return bpph.binarypackagerelease
86+
87+
88+class GeneralizedPublication:
89+ """Generalize handling of publication records.
90+
91+ This allows us to write code that can be dealing with either
92+ `SourcePackagePublishingHistory`s or `BinaryPackagePublishingHistory`s
93+ without caring which. Differences are abstracted away in a traits
94+ class.
95+ """
96+ def __init__(self, is_source=True):
97+ if is_source:
98+ self.traits = SourcePublicationTraits
99+ else:
100+ self.traits = BinaryPublicationTraits
101+
102+ def getPackageName(self, pub):
103+ """Get the package's name."""
104+ return self.traits.getPackageName(pub)
105+
106+ def getPackageVersion(self, pub):
107+ """Obtain the version string for a publicaiton record."""
108+ return self.traits.getPackageRelease(pub).version
109+
110+ def compare(self, pub1, pub2):
111+ """Compare publications by version.
112+
113+ If both publications are for the same version, their creation dates
114+ break the tie.
115+ """
116+ version_comparison = apt_pkg.VersionCompare(
117+ self.getPackageVersion(pub1), self.getPackageVersion(pub2))
118+
119+ if version_comparison == 0:
120+ # Use dates as tie breaker.
121+ return cmp(pub1.datecreated, pub2.datecreated)
122+ else:
123+ return version_comparison
124
125
126 class Dominator:
127@@ -125,11 +199,10 @@
128 """
129 self.logger.debug("Dominating packages...")
130
131- for name in pubs.keys():
132- assert pubs[name], (
133- "Empty list of publications for %s" % name)
134- for pubrec in pubs[name][1:]:
135- pubrec.supersede(pubs[name][0], logger=self.logger)
136+ for name, publications in pubs.iteritems():
137+ assert publications, "Empty list of publications for %s." % name
138+ for pubrec in publications[1:]:
139+ pubrec.supersede(publications[0], logger=self.logger)
140
141 def _sortPackages(self, pkglist, is_source=True):
142 """Map out packages by name, and sort by descending version.
143@@ -139,27 +212,20 @@
144 :param is_source: Whether this call involves source package
145 publications. If so, work with `SourcePackagePublishingHistory`.
146 If not, work with `BinaryPackagepublishingHistory`.
147- :return: A dict mapping each package name (as UTF-8 encoded string)
148- to a list of publications from `pkglist`, newest first.
149+ :return: A dict mapping each package name to a list of publications
150+ from `pkglist`, newest first.
151 """
152 self.logger.debug("Sorting packages...")
153
154- if is_source:
155- get_release = operator.attrgetter("sourcepackagerelease")
156- get_name = operator.attrgetter("sourcepackagename")
157- else:
158- get_release = operator.attrgetter("binarypackagerelease")
159- get_name = operator.attrgetter("binarypackagename")
160+ generalization = GeneralizedPublication(is_source)
161
162 outpkgs = {}
163 for inpkg in pkglist:
164- key = get_name(get_release(inpkg)).name.encode('utf-8')
165+ key = generalization.getPackageName(inpkg)
166 outpkgs.setdefault(key, []).append(inpkg)
167
168- sort_order = functools.partial(
169- _compare_packages_by_version_and_date, get_release)
170 for package_pubs in outpkgs.itervalues():
171- package_pubs.sort(cmp=sort_order, reverse=True)
172+ package_pubs.sort(cmp=generalization.compare, reverse=True)
173
174 return outpkgs
175
176@@ -312,7 +378,7 @@
177 ),
178 group_by=BinaryPackageName.id,
179 having=Count(BinaryPackagePublishingHistory.id) > 1)
180- binaries = IMasterStore(BinaryPackagePublishingHistory).find(
181+ binaries = IStore(BinaryPackagePublishingHistory).find(
182 BinaryPackagePublishingHistory,
183 BinaryPackageRelease.id ==
184 BinaryPackagePublishingHistory.binarypackagereleaseID,
185@@ -324,6 +390,19 @@
186 self.logger.debug("Dominating binaries...")
187 self._dominatePublications(self._sortPackages(binaries, False))
188
189+ def _composeActiveSourcePubsCondition(self, distroseries, pocket):
190+ """Compose ORM condition for restricting relevant source pubs."""
191+ # Avoid circular imports.
192+ from lp.soyuz.model.publishing import SourcePackagePublishingHistory
193+
194+ return And(
195+ SourcePackagePublishingHistory.status ==
196+ PackagePublishingStatus.PUBLISHED,
197+ SourcePackagePublishingHistory.distroseries == distroseries,
198+ SourcePackagePublishingHistory.archive == self.archive,
199+ SourcePackagePublishingHistory.pocket == pocket,
200+ )
201+
202 def dominateSources(self, distroseries, pocket):
203 """Perform domination on source package publications.
204
205@@ -332,34 +411,27 @@
206 """
207 # Avoid circular imports.
208 from lp.soyuz.model.publishing import SourcePackagePublishingHistory
209+
210 self.logger.debug(
211 "Performing domination across %s/%s (Source)",
212 distroseries.name, pocket.title)
213- spph_location_clauses = And(
214- SourcePackagePublishingHistory.status ==
215- PackagePublishingStatus.PUBLISHED,
216- SourcePackagePublishingHistory.distroseries == distroseries,
217- SourcePackagePublishingHistory.archive == self.archive,
218- SourcePackagePublishingHistory.pocket == pocket,
219- )
220+
221+ spph_location_clauses = self._composeActiveSourcePubsCondition(
222+ distroseries, pocket)
223+ having_multiple_active_publications = (
224+ Count(SourcePackagePublishingHistory.id) > 1)
225 candidate_source_names = Select(
226 SourcePackageName.id,
227- And(
228- SourcePackageRelease.sourcepackagenameID ==
229- SourcePackageName.id,
230- SourcePackagePublishingHistory.sourcepackagereleaseID ==
231- SourcePackageRelease.id,
232- spph_location_clauses,
233- ),
234+ And(join_spr_spn(), join_spph_spr(), spph_location_clauses),
235 group_by=SourcePackageName.id,
236- having=Count(SourcePackagePublishingHistory.id) > 1)
237- sources = IMasterStore(SourcePackagePublishingHistory).find(
238+ having=having_multiple_active_publications)
239+ sources = IStore(SourcePackagePublishingHistory).find(
240 SourcePackagePublishingHistory,
241- SourcePackageRelease.id ==
242- SourcePackagePublishingHistory.sourcepackagereleaseID,
243+ join_spph_spr(),
244 SourcePackageRelease.sourcepackagenameID.is_in(
245 candidate_source_names),
246 spph_location_clauses)
247+
248 self.logger.debug("Dominating sources...")
249 self._dominatePublications(self._sortPackages(sources))
250 flush_database_updates()
251
252=== modified file 'lib/lp/archivepublisher/tests/test_dominator.py'
253--- lib/lp/archivepublisher/tests/test_dominator.py 2011-02-04 05:11:00 +0000
254+++ lib/lp/archivepublisher/tests/test_dominator.py 2011-09-05 12:08:08 +0000
255@@ -1,4 +1,4 @@
256-# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
257+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
258 # GNU Affero General Public License version 3 (see the file LICENSE).
259
260 """Tests for domination.py."""
261@@ -7,12 +7,20 @@
262
263 import datetime
264
265+import apt_pkg
266+
267 from canonical.database.sqlbase import flush_database_updates
268-from lp.archivepublisher.domination import Dominator, STAY_OF_EXECUTION
269+from canonical.testing.layers import ZopelessDatabaseLayer
270+from lp.archivepublisher.domination import (
271+ Dominator,
272+ GeneralizedPublication,
273+ STAY_OF_EXECUTION,
274+ )
275 from lp.archivepublisher.publishing import Publisher
276 from lp.registry.interfaces.series import SeriesStatus
277 from lp.soyuz.enums import PackagePublishingStatus
278 from lp.soyuz.tests.test_publishing import TestNativePublishingBase
279+from lp.testing import TestCaseWithFactory
280
281
282 class TestDominator(TestNativePublishingBase):
283@@ -200,3 +208,143 @@
284 TestDomination.setUp(self)
285 self.ubuntutest['breezy-autotest'].status = (
286 SeriesStatus.OBSOLETE)
287+
288+
289+class TestGeneralizedPublication(TestCaseWithFactory):
290+ """Test publication generalization helpers."""
291+
292+ layer = ZopelessDatabaseLayer
293+
294+ def makeSPPHsForVersions(self, versions):
295+ """Create publication records for each of `versions`.
296+
297+ They records are created in the same order in which they are
298+ specified. Make the order irregular to prove that version ordering
299+ is not a coincidence of object creation order etc.
300+
301+ Versions may also be identical; each publication record will still
302+ have its own package release.
303+ """
304+ distroseries = self.factory.makeDistroSeries()
305+ pocket = self.factory.getAnyPocket()
306+ sprs = [
307+ self.factory.makeSourcePackageRelease(version=version)
308+ for version in versions]
309+ return [
310+ self.factory.makeSourcePackagePublishingHistory(
311+ distroseries=distroseries, pocket=pocket,
312+ sourcepackagerelease=spr)
313+ for spr in sprs]
314+
315+ def listSourceVersions(self, spphs):
316+ """Extract the versions from `spphs` as a list, in the same order."""
317+ return [spph.sourcepackagerelease.version for spph in spphs]
318+
319+ def listCreaitonDates(self, spphs):
320+ """Extract creation dates from `spphs` into a list."""
321+ return [spph.datecreated for spph in spphs]
322+
323+ def alterCreationDates(self, spphs, ages):
324+ """Set `datecreated` on each of `spphs` according to `ages`.
325+
326+ :param spphs: Iterable of `SourcePackagePublishingHistory`. Their
327+ respective creation dates will be offset by the respective ages
328+ found in `ages` (with the two being matched up in the same order).
329+ :param ages: Iterable of ages. Must provide the same number of items
330+ as `spphs`. Ages are `timedelta` objects that will be subtracted
331+ from the creation dates on the respective records in `spph`.
332+ """
333+ for spph, age in zip(spphs, ages):
334+ spph.datecreated -= age
335+
336+ def test_getPackageVersion_gets_source_version(self):
337+ spph = self.factory.makeSourcePackagePublishingHistory()
338+ self.assertEqual(
339+ spph.sourcepackagerelease.version,
340+ GeneralizedPublication(is_source=True).getPackageVersion(spph))
341+
342+ def test_getPackageVersion_gets_binary_version(self):
343+ bpph = self.factory.makeBinaryPackagePublishingHistory()
344+ self.assertEqual(
345+ bpph.binarypackagerelease.version,
346+ GeneralizedPublication(is_source=False).getPackageVersion(bpph))
347+
348+ def test_compare_sorts_versions(self):
349+ versions = [
350+ '1.1v2',
351+ '1.1v1',
352+ '1.1v3',
353+ ]
354+ spphs = self.makeSPPHsForVersions(versions)
355+ sorted_spphs = sorted(spphs, cmp=GeneralizedPublication().compare)
356+ self.assertEqual(
357+ sorted(versions),
358+ self.listSourceVersions(sorted_spphs))
359+
360+ def test_compare_orders_versions_by_debian_rules(self):
361+ versions = [
362+ '1.1.0',
363+ '1.10',
364+ '1.1',
365+ '1.1ubuntu0',
366+ ]
367+ spphs = self.makeSPPHsForVersions(versions)
368+
369+ debian_sorted_versions = sorted(versions, cmp=apt_pkg.VersionCompare)
370+
371+ # Assumption: in this case, Debian version ordering is not the
372+ # same as alphabetical version ordering.
373+ self.assertNotEqual(sorted(versions), debian_sorted_versions)
374+
375+ # The compare method produces the Debian ordering.
376+ sorted_spphs = sorted(spphs, cmp=GeneralizedPublication().compare)
377+ self.assertEqual(
378+ sorted(versions, cmp=apt_pkg.VersionCompare),
379+ self.listSourceVersions(sorted_spphs))
380+
381+ def test_compare_breaks_tie_with_creation_date(self):
382+ # When two publications are tied for comparison because they are
383+ # for the same package release, they are ordered by creation
384+ # date.
385+ distroseries = self.factory.makeDistroSeries()
386+ pocket = self.factory.getAnyPocket()
387+ spr = self.factory.makeSourcePackageRelease()
388+ ages = [
389+ datetime.timedelta(2),
390+ datetime.timedelta(1),
391+ datetime.timedelta(3),
392+ ]
393+ spphs = [
394+ self.factory.makeSourcePackagePublishingHistory(
395+ sourcepackagerelease=spr, distroseries=distroseries,
396+ pocket=pocket)
397+ for counter in xrange(len(ages))]
398+ self.alterCreationDates(spphs, ages)
399+
400+ self.assertEqual(
401+ [spphs[2], spphs[0], spphs[1]],
402+ sorted(spphs, cmp=GeneralizedPublication().compare))
403+
404+ def test_compare_breaks_tie_for_releases_with_same_version(self):
405+ # When two publications are tied for comparison because they
406+ # belong to releases with the same version string, they are
407+ # ordered by creation date.
408+ version = "1.%d" % self.factory.getUniqueInteger()
409+ ages = [
410+ datetime.timedelta(2),
411+ datetime.timedelta(1),
412+ datetime.timedelta(3),
413+ ]
414+ distroseries = self.factory.makeDistroSeries()
415+ pocket = self.factory.getAnyPocket()
416+ spphs = [
417+ self.factory.makeSourcePackagePublishingHistory(
418+ distroseries=distroseries, pocket=pocket,
419+ sourcepackagerelease=self.factory.makeSourcePackageRelease(
420+ version=version))
421+ for counter in xrange(len(ages))]
422+ self.alterCreationDates(spphs, ages)
423+
424+ self.assertEqual(
425+ [spphs[2], spphs[0], spphs[1]],
426+ sorted(spphs, cmp=GeneralizedPublication().compare))
427
428=== modified file 'lib/lp/soyuz/interfaces/publishing.py'
429--- lib/lp/soyuz/interfaces/publishing.py 2011-09-02 04:51:25 +0000
430+++ lib/lp/soyuz/interfaces/publishing.py 2011-09-05 12:08:08 +0000
431@@ -195,9 +195,6 @@
432 the field name and value is the value string.
433 """
434
435- def supersede():
436- """Supersede this publication."""
437-
438 def requestObsolescence():
439 """Make this publication obsolete.
440
441
442=== modified file 'lib/lp/soyuz/model/publishing.py'
443--- lib/lp/soyuz/model/publishing.py 2011-08-31 04:40:44 +0000
444+++ lib/lp/soyuz/model/publishing.py 2011-09-05 12:08:08 +0000
445@@ -327,8 +327,8 @@
446 fields = self.buildIndexStanzaFields()
447 return fields.makeOutput()
448
449- def supersede(self):
450- """See `IPublishing`."""
451+ def setSuperseded(self):
452+ """Set to SUPERSEDED status."""
453 self.status = PackagePublishingStatus.SUPERSEDED
454 self.datesuperseded = UTC_NOW
455
456@@ -742,7 +742,7 @@
457 "Should not dominate unpublished source %s" %
458 self.sourcepackagerelease.title)
459
460- super(SourcePackagePublishingHistory, self).supersede()
461+ self.setSuperseded()
462
463 if dominant is not None:
464 if logger is not None:
465@@ -1126,7 +1126,7 @@
466 self.distroarchseries.architecturetag))
467 return
468
469- super(BinaryPackagePublishingHistory, self).supersede()
470+ self.setSuperseded()
471
472 if dominant is not None:
473 # DDEBs cannot themselves be dominant; they are always dominated
474
475=== modified file 'scripts/gina.py'
476--- scripts/gina.py 2011-08-23 08:35:13 +0000
477+++ scripts/gina.py 2011-09-05 12:08:08 +0000
478@@ -209,9 +209,8 @@
479 npacks = len(packages_map.src_map)
480 log.info('%i Source Packages to be imported', npacks)
481
482- for list_source in sorted(
483- packages_map.src_map.values(), key=lambda x: x[0].get("Package")):
484- for source in list_source:
485+ for package in sorted(packages_map.src_map.iterkeys()):
486+ for source in packages_map.src_map[package]:
487 count += 1
488 attempt_source_package_import(
489 source, kdb, package_root, keyrings, importer_handler)
490@@ -244,10 +243,9 @@
491 log.info(
492 '%i Binary Packages to be imported for %s', npacks, archtag)
493 # Go over binarypackages importing them for this architecture
494- for binary in sorted(packages_map.bin_map[archtag].values(),
495- key=lambda x: x.get("Package")):
496+ for package_name in sorted(packages_map.bin_map[archtag].iterkeys()):
497+ binary = packages_map.bin_map[archtag][package_name]
498 count += 1
499- package_name = binary.get("Package", "unknown")
500 try:
501 try:
502 do_one_binarypackage(binary, archtag, kdb, package_root,