Merge lp:~jtv/launchpad/bug-884649-branch-2 into lp:launchpad

Proposed by Jeroen T. Vermeulen
Status: Superseded
Proposed branch: lp:~jtv/launchpad/bug-884649-branch-2
Merge into: lp:launchpad
Diff against target: 949 lines (+490/-194)
3 files modified
lib/lp/archivepublisher/domination.py (+272/-155)
lib/lp/archivepublisher/tests/test_dominator.py (+216/-27)
lib/lp/soyuz/model/publishing.py (+2/-12)
To merge this branch: bzr merge lp:~jtv/launchpad/bug-884649-branch-2
Reviewer Review Type Date Requested Status
Launchpad code reviewers Pending
Review via email: mp+81025@code.launchpad.net

This proposal has been superseded by a proposal from 2011-11-02.

Commit message

Move special-casing for publication liveness out of inner domination loop; specialize binary passes.

Description of the change

= Summary =

This is part of the Dominator optimization work needed for bug 884649.

Binary domination needs 2 passes. This is because publications for the "all" architecture, which are copied into each architecture, can't be superseded until all architecture-specific publications coming from the same source package release have been superseded. Only then can we decide whether arch-all publications can be superseded. This is because a binary package from the same source package release may still depend on the arch-all binary package, and may not have finished building yet.

The code for dealing with this subtlety is a bit hard to place, because it's deep down in the inner loop as a special case. The special case is applied to both binary-domination passes, although there's really no point: the first pass may conceivably supersede a few arch-all publications but it won't deal with all of them. Meanwhile we pay a hefty price for the extra check.

== Proposed fix ==

Hoist the decision of which publications must stay live out of dominatePackage. It's a policy decision for the caller; the API lets the caller specify which versions stay live.

With that, we can specialize the passes more, and cut out complexity that is slowing things down.

You'll see three new functions explaining exactly what liveness criteria each domination pass uses. The source pass is easy. The first binary pass deals with architecture-specific publications; arch-all ones must be considered, but they are simply kept alive. And once all architectures have gone through first-pass binary domination, superseding as many arch-specific publications as possible, the second binary-domination pass considers just what needs to happen to those arch-all publications.

== Pre-implementation notes ==

Discussed in detail with Julian; see bug 884649 for the plan we're following in addressing this. We should also be able to re-use liveness decisions across architectures (and probably binary packages building from the same source package).

Raphaƫl suggested another clever improvement: Steve K. has just initialized a new, denormalized column on BinaryPackagePublishingHistory that contains the package name. That should enable a few shortcuts as well.

By the way, note that we can't just dominate arch-all and arch-specific publications separately. Packages may vacillate between the two, producing successive releases that need to be considered together even if domination of some publications may need to be postponed.

== Implementation details ==

I had to ditch _dominatePublications, because the discovery of live versions went into the middle of it. I reasoned that if removing it introduced too much code duplication, it could return with a callback for live-version discovery. But the loss turned out to be pretty small: it's really just a 3-line loop, including log output.

== Tests ==

{{{
./bin/test -vvc lp.archivepublisher.tests.test_dominator
}}}

== Demo and Q/A ==

Dominate Ubuntu and you'll see.

= Launchpad lint =

Checking for conflicts and issues in changed files.

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

To post a comment you must log in.

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-10-18 11:56:09 +0000
3+++ lib/lp/archivepublisher/domination.py 2011-11-02 14:39:30 +0000
4@@ -52,7 +52,12 @@
5
6 __all__ = ['Dominator']
7
8+from collections import defaultdict
9 from datetime import timedelta
10+from operator import (
11+ attrgetter,
12+ itemgetter,
13+ )
14
15 import apt_pkg
16 from storm.expr import (
17@@ -67,7 +72,11 @@
18 flush_database_updates,
19 sqlvalues,
20 )
21+from canonical.launchpad.components.decoratedresultset import (
22+ DecoratedResultSet,
23+ )
24 from canonical.launchpad.interfaces.lpstorm import IStore
25+from canonical.launchpad.utilities.orderingcheck import OrderingCheck
26 from lp.registry.model.sourcepackagename import SourcePackageName
27 from lp.services.database.bulk import load_related
28 from lp.soyuz.enums import (
29@@ -188,6 +197,104 @@
30 else:
31 return version_comparison
32
33+ def sortPublications(self, publications):
34+ """Sort publications from most to least current versions."""
35+ # Listify; we want to iterate this twice, which won't do for a
36+ # non-persistent sequence.
37+ sorted_publications = list(publications)
38+ # Batch-load associated package releases; we'll be needing them
39+ # to compare versions.
40+ self.load_releases(sorted_publications)
41+ # Now sort. This is that second iteration. An in-place sort
42+ # won't hurt the original, because we're working on a copy of
43+ # the original iterable.
44+ sorted_publications.sort(cmp=self.compare, reverse=True)
45+ return sorted_publications
46+
47+
48+def find_live_source_versions(publications):
49+ """Find versions out of Published `publications` that should stay live.
50+
51+ This particular notion of liveness applies to source domination: the
52+ latest version stays live, and that's it.
53+
54+ :param publications: An iterable of `SourcePackagePublishingHistory`
55+ sorted by descending package version.
56+ :return: A list of live versions.
57+ """
58+ # Given the required sort order, the latest version is at the head
59+ # of the list.
60+ return [publications[0].sourcepackagerelease.version]
61+
62+
63+def get_binary_versions(binary_publications):
64+ """List versions for sequence of `BinaryPackagePublishingHistory`."""
65+ return [pub.binarypackagerelease.version for pub in binary_publications]
66+
67+
68+def find_live_binary_versions_first_pass(publications):
69+ """Find versions out of Published `publications` that should stay live.
70+
71+ This particular notion of liveness applies to first-pass binary
72+ domination: the latest version stays live, and so do publications of
73+ binary packages for the "all" architecture.
74+
75+ :param publications: An iterable of `BinaryPackagePublishingHistory`,
76+ sorted by descending package version.
77+ :return: A list of live versions.
78+ """
79+ publications = list(publications)
80+ latest = publications.pop(0)
81+ return get_binary_versions(
82+ [latest] + [
83+ pub for pub in publications if not pub.architecture_specific])
84+
85+
86+def find_live_binary_versions_second_pass(publications):
87+ """Find versions out of Published `publications` that should stay live.
88+
89+ This particular notion of liveness applies to second-pass binary
90+ domination: the latest version stays live, and architecture-specific
91+ publications stay live (i.e, ones that are not for the "all"
92+ architecture).
93+
94+ More importantly, any publication for binary packages of the "all"
95+ architecture stay live if any of the non-"all" binary packages from
96+ the same source package release are still active -- even if they are
97+ for other architectures.
98+
99+ This is the raison d'etre for the two-pass binary domination algorithm:
100+ to let us see which architecture-independent binary publications can be
101+ superseded without rendering any architecture-specific binaries from the
102+ same source package release uninstallable.
103+
104+ (Note that here, "active" includes Published publications but also
105+ Pending ones. This is standard nomenclature in Soyuz. Some of the
106+ domination code confuses matters by using the term "active" to mean only
107+ Published publications).
108+
109+ :param publications: An iterable of `BinaryPackagePublishingHistory`,
110+ sorted by descending package version.
111+ :return: A list of live versions.
112+ """
113+ publications = list(publications)
114+ latest = publications.pop(0)
115+ is_arch_specific = attrgetter('architecture_specific')
116+ arch_specific_pubs = filter(is_arch_specific, publications)
117+ arch_indep_pubs = filter(
118+ lambda pub: not is_arch_specific(pub),
119+ publications)
120+
121+ # XXX JeroenVermeulen 2011-11-01 bug=884649: This is likely to be
122+ # costly, and the result could be reused for all builds of the same
123+ # source package release to all architectures.
124+ reprieved_pubs = [
125+ pub
126+ for pub in arch_indep_pubs
127+ if pub.getOtherPublicationsForSameSource().any()]
128+
129+ return get_binary_versions([latest] + arch_specific_pubs + reprieved_pubs)
130+
131
132 class Dominator:
133 """Manage the process of marking packages as superseded.
134@@ -205,27 +312,6 @@
135 self.logger = logger
136 self.archive = archive
137
138- def _checkArchIndep(self, publication):
139- """Return True if the binary publication can be superseded.
140-
141- If the publication is an arch-indep binary, we can only supersede
142- it if all the binaries from the same source are also superseded,
143- else those binaries may become uninstallable.
144- See bug 34086.
145- """
146- binary = publication.binarypackagerelease
147- if not binary.architecturespecific:
148- # getOtherPublicationsForSameSource returns PENDING in
149- # addition to PUBLISHED binaries, and we rely on this since
150- # they must also block domination.
151- others = publication.getOtherPublicationsForSameSource()
152- if others.any():
153- # Don't dominate this arch:all binary as there are
154- # other arch-specific binaries from the same build
155- # that are still active.
156- return False
157- return True
158-
159 def dominatePackage(self, publications, live_versions, generalization):
160 """Dominate publications for a single package.
161
162@@ -243,48 +329,47 @@
163
164 :param publications: Iterable of publications for the same package,
165 in the same archive, series, and pocket, all with status
166- `PackagePublishingStatus.PUBLISHED`.
167- :param live_versions: Iterable of version strings that are still
168- considered live for this package. The given publications will
169- remain active insofar as they represent any of these versions;
170- older publications will be marked as superseded and newer ones
171- as deleted.
172+ `PackagePublishingStatus.PUBLISHED`. They must be sorted from
173+ most current to least current, as would be the result of
174+ `generalization.sortPublications`.
175+ :param live_versions: Iterable of versions that are still considered
176+ "live" for this package. For any of these, the latest publication
177+ among `publications` will remain Published. Publications for
178+ older releases, as well as older publications of live versions,
179+ will be marked as Superseded. Publications of newer versions than
180+ are listed in `live_versions` are marked as Deleted.
181 :param generalization: A `GeneralizedPublication` helper representing
182- the kind of publications these are--source or binary.
183+ the kind of publications these are: source or binary.
184 """
185- publications = list(publications)
186- generalization.load_releases(publications)
187-
188- # Go through publications from latest version to oldest. This
189- # makes it easy to figure out which release superseded which:
190- # the dominant is always the oldest live release that is newer
191- # than the one being superseded.
192- publications = sorted(
193- publications, cmp=generalization.compare, reverse=True)
194+ live_versions = frozenset(live_versions)
195
196 self.logger.debug(
197 "Package has %d live publication(s). Live versions: %s",
198 len(publications), live_versions)
199
200+ # Verify that the publications are really sorted properly.
201+ check_order = OrderingCheck(cmp=generalization.compare, reverse=True)
202+
203 current_dominant = None
204 dominant_version = None
205
206 for pub in publications:
207+ check_order.check(pub)
208+
209 version = generalization.getPackageVersion(pub)
210 # There should never be two published releases with the same
211- # version. So this comparison is really a string
212- # comparison, not a version comparison: if the versions are
213- # equal by either measure, they're from the same release.
214- if dominant_version is not None and version == dominant_version:
215+ # version. So it doesn't matter whether this comparison is
216+ # really a string comparison or a version comparison: if the
217+ # versions are equal by either measure, they're from the same
218+ # release.
219+ if version == dominant_version:
220 # This publication is for a live version, but has been
221 # superseded by a newer publication of the same version.
222 # Supersede it.
223 pub.supersede(current_dominant, logger=self.logger)
224 self.logger.debug2(
225 "Superseding older publication for version %s.", version)
226- elif (version in live_versions or
227- (not generalization.is_source and
228- not self._checkArchIndep(pub))):
229+ elif version in live_versions:
230 # This publication stays active; if any publications
231 # that follow right after this are to be superseded,
232 # this is the release that they are superseded by.
233@@ -303,50 +388,32 @@
234 pub.supersede(current_dominant, logger=self.logger)
235 self.logger.debug2("Superseding version %s.", version)
236
237- def _dominatePublications(self, pubs, generalization):
238- """Perform dominations for the given publications.
239-
240- Keep the latest published version for each package active,
241- superseding older versions.
242-
243- :param pubs: A dict mapping names to a list of publications. Every
244- publication must be PUBLISHED or PENDING, and the first in each
245- list will be treated as dominant (so should be the latest).
246- :param generalization: A `GeneralizedPublication` helper representing
247- the kind of publications these are--source or binary.
248- """
249- self.logger.debug("Dominating packages...")
250- for name, publications in pubs.iteritems():
251- assert publications, "Empty list of publications for %s." % name
252- # Since this always picks the latest version as the live
253- # one, this dominatePackage call will never result in a
254- # deletion.
255- latest_version = generalization.getPackageVersion(publications[0])
256- self.logger.debug2("Dominating %s" % name)
257- self.dominatePackage(
258- publications, [latest_version], generalization)
259-
260- def _sortPackages(self, pkglist, generalization):
261- """Map out packages by name, and sort by descending version.
262-
263- :param pkglist: An iterable of `SourcePackagePublishingHistory` or
264- `BinaryPackagePublishingHistory`.
265- :param generalization: A `GeneralizedPublication` helper representing
266- the kind of publications these are--source or binary.
267- :return: A dict mapping each package name to a list of publications
268- from `pkglist`, newest first.
269- """
270- self.logger.debug("Sorting packages...")
271-
272- outpkgs = {}
273- for inpkg in pkglist:
274- key = generalization.getPackageName(inpkg)
275- outpkgs.setdefault(key, []).append(inpkg)
276-
277- for package_pubs in outpkgs.itervalues():
278- package_pubs.sort(cmp=generalization.compare, reverse=True)
279-
280- return outpkgs
281+ def _sortPackages(self, publications, generalization):
282+ """Partition publications by package name, and sort them.
283+
284+ The publications are sorted from most current to least current,
285+ as required by `dominatePackage` etc.
286+
287+ :param publications: An iterable of `SourcePackagePublishingHistory`
288+ or of `BinaryPackagePublishingHistory`.
289+ :param generalization: A `GeneralizedPublication` helper representing
290+ the kind of publications these are: source or binary.
291+ :return: A dict mapping each package name to a sorted list of
292+ publications from `publications`.
293+ """
294+ pubs_by_package = defaultdict(list)
295+ for pub in publications:
296+ pubs_by_package[generalization.getPackageName(pub)].append(pub)
297+
298+ # Sort the publication lists. This is not an in-place sort, so
299+ # it involves altering the dict while we iterate it. Listify
300+ # the keys so that we can be sure that we're not altering the
301+ # iteration order while iteration is underway.
302+ for package in list(pubs_by_package.keys()):
303+ pubs_by_package[package] = generalization.sortPublications(
304+ pubs_by_package[package])
305+
306+ return pubs_by_package
307
308 def _setScheduledDeletionDate(self, pub_record):
309 """Set the scheduleddeletiondate on a publishing record.
310@@ -442,72 +509,98 @@
311 # always equals to "scheduleddeletiondate - quarantine".
312 pub_record.datemadepending = UTC_NOW
313
314+ def findBinariesForDomination(self, distroarchseries, pocket):
315+ """Find binary publications that need dominating.
316+
317+ This is only for traditional domination, where the latest published
318+ publication is always kept published. It will ignore publications
319+ that have no other publications competing for the same binary package.
320+ """
321+ # Avoid circular imports.
322+ from lp.soyuz.model.publishing import BinaryPackagePublishingHistory
323+
324+ bpph_location_clauses = [
325+ BinaryPackagePublishingHistory.status ==
326+ PackagePublishingStatus.PUBLISHED,
327+ BinaryPackagePublishingHistory.distroarchseries ==
328+ distroarchseries,
329+ BinaryPackagePublishingHistory.archive == self.archive,
330+ BinaryPackagePublishingHistory.pocket == pocket,
331+ ]
332+ candidate_binary_names = Select(
333+ BinaryPackageName.id,
334+ And(
335+ BinaryPackageRelease.binarypackagenameID ==
336+ BinaryPackageName.id,
337+ BinaryPackagePublishingHistory.binarypackagereleaseID ==
338+ BinaryPackageRelease.id,
339+ bpph_location_clauses,
340+ ),
341+ group_by=BinaryPackageName.id,
342+ having=Count(BinaryPackagePublishingHistory.id) > 1)
343+ main_clauses = [
344+ BinaryPackageRelease.id ==
345+ BinaryPackagePublishingHistory.binarypackagereleaseID,
346+ BinaryPackageRelease.binarypackagenameID.is_in(
347+ candidate_binary_names),
348+ BinaryPackageRelease.binpackageformat !=
349+ BinaryPackageFormat.DDEB,
350+ ]
351+ main_clauses.extend(bpph_location_clauses)
352+
353+ store = IStore(BinaryPackagePublishingHistory)
354+
355+ # We're going to access the BPRs as well. Since we make the
356+ # database look them up anyway, and since there won't be many
357+ # duplications among them, load them alongside the publications.
358+ # We'll also want their BinaryPackageNames, but adding those to
359+ # the join would complicate the query.
360+ query = store.find(
361+ (BinaryPackagePublishingHistory, BinaryPackageRelease),
362+ *main_clauses)
363+ return DecoratedResultSet(query, itemgetter(0))
364+
365 def dominateBinaries(self, distroseries, pocket):
366 """Perform domination on binary package publications.
367
368 Dominates binaries, restricted to `distroseries`, `pocket`, and
369 `self.archive`.
370 """
371- # Avoid circular imports.
372- from lp.soyuz.model.publishing import BinaryPackagePublishingHistory
373-
374 generalization = GeneralizedPublication(is_source=False)
375
376 for distroarchseries in distroseries.architectures:
377 self.logger.info(
378 "Performing domination across %s/%s (%s)",
379- distroseries.name, pocket.title,
380+ distroarchseries.distroseries.name, pocket.title,
381 distroarchseries.architecturetag)
382
383- bpph_location_clauses = [
384- BinaryPackagePublishingHistory.status ==
385- PackagePublishingStatus.PUBLISHED,
386- BinaryPackagePublishingHistory.distroarchseries ==
387- distroarchseries,
388- BinaryPackagePublishingHistory.archive == self.archive,
389- BinaryPackagePublishingHistory.pocket == pocket,
390- ]
391- candidate_binary_names = Select(
392- BinaryPackageName.id,
393- And(
394- BinaryPackageRelease.binarypackagenameID ==
395- BinaryPackageName.id,
396- BinaryPackagePublishingHistory.binarypackagereleaseID ==
397- BinaryPackageRelease.id,
398- bpph_location_clauses,
399- ),
400- group_by=BinaryPackageName.id,
401- having=Count(BinaryPackagePublishingHistory.id) > 1)
402- main_clauses = [
403- BinaryPackagePublishingHistory,
404- BinaryPackageRelease.id ==
405- BinaryPackagePublishingHistory.binarypackagereleaseID,
406- BinaryPackageRelease.binarypackagenameID.is_in(
407- candidate_binary_names),
408- BinaryPackageRelease.binpackageformat !=
409- BinaryPackageFormat.DDEB,
410- ]
411- main_clauses.extend(bpph_location_clauses)
412-
413- def do_domination(pass2_msg=""):
414- msg = "binaries..." + pass2_msg
415- self.logger.info("Finding %s" % msg)
416- bins = IStore(
417- BinaryPackagePublishingHistory).find(*main_clauses)
418- self.logger.info("Dominating %s" % msg)
419- sorted_packages = self._sortPackages(bins, generalization)
420- self._dominatePublications(sorted_packages, generalization)
421-
422- do_domination()
423-
424- # We need to make a second pass to cover the cases where:
425- # * arch-specific binaries were not all dominated before arch-all
426- # ones that depend on them
427- # * An arch-all turned into an arch-specific, or vice-versa
428- # * A package is completely schizophrenic and changes all of
429- # its binaries between arch-all and arch-any (apparently
430- # occurs sometimes!)
431- do_domination("(2nd pass)")
432+ self.logger.info("Finding binaries...")
433+ bins = self.findBinariesForDomination(distroarchseries, pocket)
434+ sorted_packages = self._sortPackages(bins, generalization)
435+ self.logger.info("Dominating binaries...")
436+ for name, pubs in sorted_packages.iteritems():
437+ self.logger.debug("Dominating %s" % name)
438+ assert len(pubs) > 0, "Dominating zero binaries!"
439+ live_versions = find_live_binary_versions_first_pass(pubs)
440+ self.dominatePackage(pubs, live_versions, generalization)
441+
442+ # We need to make a second pass to cover the cases where:
443+ # * arch-specific binaries were not all dominated before arch-all
444+ # ones that depend on them
445+ # * An arch-all turned into an arch-specific, or vice-versa
446+ # * A package is completely schizophrenic and changes all of
447+ # its binaries between arch-all and arch-any (apparently
448+ # occurs sometimes!)
449+ for distroarchseries in distroseries.architectures:
450+ self.logger.info("Finding binaries...(2nd pass)")
451+ bins = self.findBinariesForDomination(distroarchseries, pocket)
452+ sorted_packages = self._sortPackages(bins, generalization)
453+ self.logger.info("Dominating binaries...(2nd pass)")
454+ for name, pubs in sorted_packages.iteritems():
455+ self.logger.debug("Dominating %s" % name)
456+ assert len(pubs) > 0, "Dominating zero binaries in 2nd pass!"
457+ live_versions = find_live_binary_versions_second_pass(pubs)
458+ self.dominatePackage(pubs, live_versions, generalization)
459
460 def _composeActiveSourcePubsCondition(self, distroseries, pocket):
461 """Compose ORM condition for restricting relevant source pubs."""
462@@ -522,21 +615,16 @@
463 SourcePackagePublishingHistory.pocket == pocket,
464 )
465
466- def dominateSources(self, distroseries, pocket):
467- """Perform domination on source package publications.
468+ def findSourcesForDomination(self, distroseries, pocket):
469+ """Find binary publications that need dominating.
470
471- Dominates sources, restricted to `distroseries`, `pocket`, and
472- `self.archive`.
473+ This is only for traditional domination, where the latest published
474+ publication is always kept published. It will ignore publications
475+ that have no other publications competing for the same binary package.
476 """
477 # Avoid circular imports.
478 from lp.soyuz.model.publishing import SourcePackagePublishingHistory
479
480- generalization = GeneralizedPublication(is_source=True)
481-
482- self.logger.debug(
483- "Performing domination across %s/%s (Source)",
484- distroseries.name, pocket.title)
485-
486 spph_location_clauses = self._composeActiveSourcePubsCondition(
487 distroseries, pocket)
488 having_multiple_active_publications = (
489@@ -546,16 +634,44 @@
490 And(join_spph_spr(), join_spr_spn(), spph_location_clauses),
491 group_by=SourcePackageName.id,
492 having=having_multiple_active_publications)
493- sources = IStore(SourcePackagePublishingHistory).find(
494- SourcePackagePublishingHistory,
495+
496+ # We'll also access the SourcePackageReleases associated with
497+ # the publications we find. Since they're in the join anyway,
498+ # load them alongside the publications.
499+ # Actually we'll also want the SourcePackageNames, but adding
500+ # those to the (outer) query would complicate it, and
501+ # potentially slow it down.
502+ query = IStore(SourcePackagePublishingHistory).find(
503+ (SourcePackagePublishingHistory, SourcePackageRelease),
504 join_spph_spr(),
505 SourcePackageRelease.sourcepackagenameID.is_in(
506 candidate_source_names),
507 spph_location_clauses)
508+ return DecoratedResultSet(query, itemgetter(0))
509+
510+ def dominateSources(self, distroseries, pocket):
511+ """Perform domination on source package publications.
512+
513+ Dominates sources, restricted to `distroseries`, `pocket`, and
514+ `self.archive`.
515+ """
516+ self.logger.debug(
517+ "Performing domination across %s/%s (Source)",
518+ distroseries.name, pocket.title)
519+
520+ generalization = GeneralizedPublication(is_source=True)
521+
522+ self.logger.debug("Finding sources...")
523+ sources = self.findSourcesForDomination(distroseries, pocket)
524+ sorted_packages = self._sortPackages(sources, generalization)
525
526 self.logger.debug("Dominating sources...")
527- self._dominatePublications(
528- self._sortPackages(sources, generalization), generalization)
529+ for name, pubs in sorted_packages.iteritems():
530+ self.logger.debug("Dominating %s" % name)
531+ assert len(pubs) > 0, "Dominating zero binaries!"
532+ live_versions = find_live_source_versions(pubs)
533+ self.dominatePackage(pubs, live_versions, generalization)
534+
535 flush_database_updates()
536
537 def findPublishedSourcePackageNames(self, distroseries, pocket):
538@@ -615,6 +731,7 @@
539 """
540 generalization = GeneralizedPublication(is_source=True)
541 pubs = self.findPublishedSPPHs(distroseries, pocket, package_name)
542+ pubs = generalization.sortPublications(pubs)
543 self.dominatePackage(pubs, live_versions, generalization)
544
545 def judge(self, distroseries, pocket):
546
547=== modified file 'lib/lp/archivepublisher/tests/test_dominator.py'
548--- lib/lp/archivepublisher/tests/test_dominator.py 2011-10-27 11:36:13 +0000
549+++ lib/lp/archivepublisher/tests/test_dominator.py 2011-11-02 14:39:30 +0000
550@@ -30,6 +30,7 @@
551 StormStatementRecorder,
552 TestCaseWithFactory,
553 )
554+from lp.testing.fakemethod import FakeMethod
555 from lp.testing.matchers import HasQueryCount
556
557
558@@ -72,13 +73,9 @@
559 is_source=ISourcePackagePublishingHistory.providedBy(dominant))
560 dominator = Dominator(self.logger, self.ubuntutest.main_archive)
561
562- # The _dominate* test methods require a dictionary where the
563- # package name is the key. The key's value is a list of
564- # source or binary packages representing dominant, the first element
565- # and dominated, the subsequents.
566- pubs = {'foo': [dominant, dominated]}
567-
568- dominator._dominatePublications(pubs, generalization)
569+ pubs = [dominant, dominated]
570+ live_versions = [generalization.getPackageVersion(dominant)]
571+ dominator.dominatePackage(pubs, live_versions, generalization)
572 flush_database_updates()
573
574 # The dominant version remains correctly published.
575@@ -158,16 +155,30 @@
576 [foo_10_source] + foo_10_binaries,
577 PackagePublishingStatus.SUPERSEDED)
578
579- def testEmptyDomination(self):
580- """Domination asserts for not empty input list."""
581- dominator = Dominator(self.logger, self.ubuntutest.main_archive)
582- pubs = {'foo': []}
583- # This isn't a really good exception. It should probably be
584- # something more indicative of bad input.
585- self.assertRaises(
586- AssertionError,
587- dominator._dominatePublications,
588- pubs, GeneralizedPublication(True))
589+ def test_dominateBinaries_rejects_empty_publication_list(self):
590+ """Domination asserts for non-empty input list."""
591+ package = self.factory.makeBinaryPackageName()
592+ dominator = Dominator(self.logger, self.ubuntutest.main_archive)
593+ dominator.mapPackages = FakeMethod({package.name: []})
594+ # This isn't a really good exception. It should probably be
595+ # something more indicative of bad input.
596+ self.assertRaises(
597+ AssertionError,
598+ dominator.dominateBinaries,
599+ self.factory.makeDistroArchSeries().distroseries,
600+ self.factory.getAnyPocket())
601+
602+ def test_dominateSources_rejects_empty_publication_list(self):
603+ """Domination asserts for non-empty input list."""
604+ package = self.factory.makeSourcePackageName()
605+ dominator = Dominator(self.logger, self.ubuntutest.main_archive)
606+ dominator.mapPackages = FakeMethod({package.name: []})
607+ # This isn't a really good exception. It should probably be
608+ # something more indicative of bad input.
609+ self.assertRaises(
610+ AssertionError,
611+ dominator.dominateSources,
612+ self.factory.makeDistroSeries(), self.factory.getAnyPocket())
613
614 def test_archall_domination(self):
615 # Arch-all binaries should not be dominated when a new source
616@@ -361,7 +372,10 @@
617 def make_spphs_for_versions(factory, versions):
618 """Create publication records for each of `versions`.
619
620- They records are created in the same order in which they are specified.
621+ All these publications will be in the same source package, archive,
622+ distroseries, and pocket. They will all be in Published status.
623+
624+ The records are created in the same order in which they are specified.
625 Make the order irregular to prove that version ordering is not a
626 coincidence of object creation order etc.
627
628@@ -371,6 +385,7 @@
629 spn = factory.makeSourcePackageName()
630 distroseries = factory.makeDistroSeries()
631 pocket = factory.getAnyPocket()
632+ archive = distroseries.main_archive
633 sprs = [
634 factory.makeSourcePackageRelease(
635 sourcepackagename=spn, version=version)
636@@ -378,11 +393,34 @@
637 return [
638 factory.makeSourcePackagePublishingHistory(
639 distroseries=distroseries, pocket=pocket,
640- sourcepackagerelease=spr,
641+ sourcepackagerelease=spr, archive=archive,
642 status=PackagePublishingStatus.PUBLISHED)
643 for spr in sprs]
644
645
646+def make_bpphs_for_versions(factory, versions):
647+ """Create publication records for each of `versions`.
648+
649+ All these publications will be in the same binary package, source
650+ package, archive, distroarchseries, and pocket. They will all be in
651+ Published status.
652+ """
653+ bpn = factory.makeBinaryPackageName()
654+ spn = factory.makeSourcePackageName()
655+ das = factory.makeDistroArchSeries()
656+ archive = das.distroseries.main_archive
657+ pocket = factory.getAnyPocket()
658+ bprs = [
659+ factory.makeBinaryPackageRelease(binarypackagename=bpn)
660+ for version in versions]
661+ return [
662+ factory.makeBinaryPackagePublishingHistory(
663+ binarypackagerelease=bpr, binarypackagename=bpn,
664+ distroarchseries=das, pocket=pocket, archive=archive,
665+ sourcepackagename=spn, status=PackagePublishingStatus.PUBLISHED)
666+ for bpr in bprs]
667+
668+
669 def list_source_versions(spphs):
670 """Extract the versions from `spphs` as a list, in the same order."""
671 return [spph.sourcepackagerelease.version for spph in spphs]
672@@ -564,9 +602,10 @@
673 def test_dominatePackage_supersedes_older_pub_with_newer_live_pub(self):
674 # When marking a package as superseded, dominatePackage
675 # designates a newer live version as the superseding version.
676+ generalization = GeneralizedPublication(True)
677 pubs = make_spphs_for_versions(self.factory, ['1.0', '1.1'])
678 self.makeDominator(pubs).dominatePackage(
679- pubs, ['1.1'], GeneralizedPublication(True))
680+ generalization.sortPublications(pubs), ['1.1'], generalization)
681 self.assertEqual(PackagePublishingStatus.SUPERSEDED, pubs[0].status)
682 self.assertEqual(pubs[1].sourcepackagerelease, pubs[0].supersededby)
683 self.assertEqual(PackagePublishingStatus.PUBLISHED, pubs[1].status)
684@@ -574,10 +613,11 @@
685 def test_dominatePackage_only_supersedes_with_live_pub(self):
686 # When marking a package as superseded, dominatePackage will
687 # only pick a live version as the superseding one.
688+ generalization = GeneralizedPublication(True)
689 pubs = make_spphs_for_versions(
690 self.factory, ['1.0', '2.0', '3.0', '4.0'])
691 self.makeDominator(pubs).dominatePackage(
692- pubs, ['3.0'], GeneralizedPublication(True))
693+ generalization.sortPublications(pubs), ['3.0'], generalization)
694 self.assertEqual([
695 pubs[2].sourcepackagerelease,
696 pubs[2].sourcepackagerelease,
697@@ -589,23 +629,27 @@
698 def test_dominatePackage_supersedes_with_oldest_newer_live_pub(self):
699 # When marking a package as superseded, dominatePackage picks
700 # the oldest of the newer, live versions as the superseding one.
701+ generalization = GeneralizedPublication(True)
702 pubs = make_spphs_for_versions(self.factory, ['2.7', '2.8', '2.9'])
703 self.makeDominator(pubs).dominatePackage(
704- pubs, ['2.8', '2.9'], GeneralizedPublication(True))
705+ generalization.sortPublications(pubs), ['2.8', '2.9'],
706+ generalization)
707 self.assertEqual(pubs[1].sourcepackagerelease, pubs[0].supersededby)
708
709 def test_dominatePackage_only_supersedes_with_newer_live_pub(self):
710 # When marking a package as superseded, dominatePackage only
711 # considers a newer version as the superseding one.
712+ generalization = GeneralizedPublication(True)
713 pubs = make_spphs_for_versions(self.factory, ['0.1', '0.2'])
714 self.makeDominator(pubs).dominatePackage(
715- pubs, ['0.1'], GeneralizedPublication(True))
716+ generalization.sortPublications(pubs), ['0.1'], generalization)
717 self.assertEqual(None, pubs[1].supersededby)
718 self.assertEqual(PackagePublishingStatus.DELETED, pubs[1].status)
719
720 def test_dominatePackage_supersedes_replaced_pub_for_live_version(self):
721 # Even if a publication record is for a live version, a newer
722 # one for the same version supersedes it.
723+ generalization = GeneralizedPublication(True)
724 spr = self.factory.makeSourcePackageRelease()
725 series = self.factory.makeDistroSeries()
726 pocket = PackagePublishingPocket.RELEASE
727@@ -622,7 +666,8 @@
728 ])
729
730 self.makeDominator(pubs).dominatePackage(
731- pubs, [spr.version], GeneralizedPublication(True))
732+ generalization.sortPublications(pubs), [spr.version],
733+ generalization)
734 self.assertEqual([
735 PackagePublishingStatus.SUPERSEDED,
736 PackagePublishingStatus.SUPERSEDED,
737@@ -634,12 +679,13 @@
738
739 def test_dominatePackage_is_efficient(self):
740 # dominatePackage avoids issuing too many queries.
741+ generalization = GeneralizedPublication(True)
742 versions = ["1.%s" % revision for revision in xrange(5)]
743 pubs = make_spphs_for_versions(self.factory, versions)
744 with StormStatementRecorder() as recorder:
745 self.makeDominator(pubs).dominatePackage(
746- pubs, versions[2:-1],
747- GeneralizedPublication(True))
748+ generalization.sortPublications(pubs), versions[2:-1],
749+ generalization)
750 self.assertThat(recorder, HasQueryCount(LessThan(5)))
751
752 def test_dominatePackage_advanced_scenario(self):
753@@ -650,6 +696,7 @@
754 # don't just patch up the code or this test. Create unit tests
755 # that specifically cover the difference, then change the code
756 # and/or adapt this test to return to harmony.
757+ generalization = GeneralizedPublication(True)
758 series = self.factory.makeDistroSeries()
759 package = self.factory.makeSourcePackageName()
760 pocket = PackagePublishingPocket.RELEASE
761@@ -696,7 +743,8 @@
762
763 all_pubs = sum(pubs_by_version.itervalues(), [])
764 Dominator(DevNullLogger(), series.main_archive).dominatePackage(
765- all_pubs, live_versions, GeneralizedPublication(True))
766+ generalization.sortPublications(all_pubs), live_versions,
767+ generalization)
768
769 for version in reversed(versions):
770 pubs = pubs_by_version[version]
771@@ -921,3 +969,144 @@
772 [],
773 dominator.findPublishedSPPHs(
774 spph.distroseries, spph.pocket, other_package.name))
775+
776+ def test_findBinariesForDomination_finds_published_publications(self):
777+ bpphs = make_bpphs_for_versions(self.factory, ['1.0', '1.1'])
778+ dominator = self.makeDominator(bpphs)
779+ self.assertContentEqual(
780+ bpphs, dominator.findBinariesForDomination(
781+ bpphs[0].distroarchseries, bpphs[0].pocket))
782+
783+ def test_findBinariesForDomination_skips_single_pub_packages(self):
784+ # The domination algorithm that uses findBinariesForDomination
785+ # always keeps the latest version live. Thus, a single
786+ # publication isn't worth dominating. findBinariesForDomination
787+ # won't return it.
788+ bpphs = make_bpphs_for_versions(self.factory, ['1.0'])
789+ dominator = self.makeDominator(bpphs)
790+ self.assertContentEqual(
791+ [], dominator.findBinariesForDomination(
792+ bpphs[0].distroarchseries, bpphs[0].pocket))
793+
794+ def test_findBinariesForDomination_ignores_other_distroseries(self):
795+ bpphs = make_bpphs_for_versions(self.factory, ['1.0', '1.1'])
796+ dominator = self.makeDominator(bpphs)
797+ das = bpphs[0].distroarchseries
798+ other_series = self.factory.makeDistroSeries(
799+ distribution=das.distroseries.distribution)
800+ other_das = self.factory.makeDistroArchSeries(
801+ distroseries=other_series, architecturetag=das.architecturetag,
802+ processorfamily=das.processorfamily)
803+ self.assertContentEqual(
804+ [], dominator.findBinariesForDomination(
805+ other_das, bpphs[0].pocket))
806+
807+ def test_findBinariesForDomination_ignores_other_architectures(self):
808+ bpphs = make_bpphs_for_versions(self.factory, ['1.0', '1.1'])
809+ dominator = self.makeDominator(bpphs)
810+ other_das = self.factory.makeDistroArchSeries(
811+ distroseries=bpphs[0].distroseries)
812+ self.assertContentEqual(
813+ [], dominator.findBinariesForDomination(
814+ other_das, bpphs[0].pocket))
815+
816+ def test_findBinariesForDomination_ignores_other_archive(self):
817+ bpphs = make_bpphs_for_versions(self.factory, ['1.0', '1.1'])
818+ dominator = self.makeDominator(bpphs)
819+ dominator.archive = self.factory.makeArchive()
820+ self.assertContentEqual(
821+ [], dominator.findBinariesForDomination(
822+ bpphs[0].distroarchseries, bpphs[0].pocket))
823+
824+ def test_findBinariesForDomination_ignores_other_pocket(self):
825+ bpphs = make_bpphs_for_versions(self.factory, ['1.0', '1.1'])
826+ dominator = self.makeDominator(bpphs)
827+ for bpph in bpphs:
828+ removeSecurityProxy(bpph).pocket = PackagePublishingPocket.UPDATES
829+ self.assertContentEqual(
830+ [], dominator.findBinariesForDomination(
831+ bpphs[0].distroarchseries, PackagePublishingPocket.SECURITY))
832+
833+ def test_findBinariesForDomination_ignores_other_status(self):
834+ # If we have one BPPH for each possible status, plus one
835+ # Published one to stop findBinariesForDomination from skipping
836+ # the package, findBinariesForDomination returns only the
837+ # Published ones.
838+ versions = [
839+ '1.%d' % self.factory.getUniqueInteger()
840+ for status in PackagePublishingStatus.items] + ['0.9']
841+ bpphs = make_bpphs_for_versions(self.factory, versions)
842+ dominator = self.makeDominator(bpphs)
843+
844+ for bpph, status in zip(bpphs, PackagePublishingStatus.items):
845+ bpph.status = status
846+
847+ # These are the Published publications. The other ones will all
848+ # be ignored.
849+ published_bpphs = [
850+ bpph
851+ for bpph in bpphs
852+ if bpph.status == PackagePublishingStatus.PUBLISHED]
853+
854+ self.assertContentEqual(
855+ published_bpphs,
856+ dominator.findBinariesForDomination(
857+ bpphs[0].distroarchseries, bpphs[0].pocket))
858+
859+ def test_findSourcesForDomination_finds_published_publications(self):
860+ spphs = make_spphs_for_versions(self.factory, ['2.0', '2.1'])
861+ dominator = self.makeDominator(spphs)
862+ self.assertContentEqual(
863+ spphs, dominator.findSourcesForDomination(
864+ spphs[0].distroseries, spphs[0].pocket))
865+
866+ def test_findSourcesForDomination_skips_single_pub_packages(self):
867+ # The domination algorithm that uses findSourcesForDomination
868+ # always keeps the latest version live. Thus, a single
869+ # publication isn't worth dominating. findSourcesForDomination
870+ # won't return it.
871+ spphs = make_spphs_for_versions(self.factory, ['2.0'])
872+ dominator = self.makeDominator(spphs)
873+ self.assertContentEqual(
874+ [], dominator.findSourcesForDomination(
875+ spphs[0].distroseries, spphs[0].pocket))
876+
877+ def test_findSourcesForDomination_ignores_other_distroseries(self):
878+ spphs = make_spphs_for_versions(self.factory, ['2.0', '2.1'])
879+ dominator = self.makeDominator(spphs)
880+ other_series = self.factory.makeDistroSeries(
881+ distribution=spphs[0].distroseries.distribution)
882+ self.assertContentEqual(
883+ [], dominator.findSourcesForDomination(
884+ other_series, spphs[0].pocket))
885+
886+ def test_findSourcesForDomination_ignores_other_pocket(self):
887+ spphs = make_spphs_for_versions(self.factory, ['2.0', '2.1'])
888+ dominator = self.makeDominator(spphs)
889+ for spph in spphs:
890+ removeSecurityProxy(spph).pocket = PackagePublishingPocket.UPDATES
891+ self.assertContentEqual(
892+ [], dominator.findSourcesForDomination(
893+ spphs[0].distroseries, PackagePublishingPocket.SECURITY))
894+
895+ def test_findSourcesForDomination_ignores_other_status(self):
896+ versions = [
897+ '1.%d' % self.factory.getUniqueInteger()
898+ for status in PackagePublishingStatus.items] + ['0.9']
899+ spphs = make_spphs_for_versions(self.factory, versions)
900+ dominator = self.makeDominator(spphs)
901+
902+ for spph, status in zip(spphs, PackagePublishingStatus.items):
903+ spph.status = status
904+
905+ # These are the Published publications. The other ones will all
906+ # be ignored.
907+ published_spphs = [
908+ spph
909+ for spph in spphs
910+ if spph.status == PackagePublishingStatus.PUBLISHED]
911+
912+ self.assertContentEqual(
913+ published_spphs,
914+ dominator.findSourcesForDomination(
915+ spphs[0].distroseries, spphs[0].pocket))
916
917=== modified file 'lib/lp/soyuz/model/publishing.py'
918--- lib/lp/soyuz/model/publishing.py 2011-10-23 02:58:56 +0000
919+++ lib/lp/soyuz/model/publishing.py 2011-11-02 14:39:30 +0000
920@@ -33,7 +33,6 @@
921 Desc,
922 LeftJoin,
923 Or,
924- Select,
925 Sum,
926 )
927 from storm.store import Store
928@@ -1154,19 +1153,10 @@
929 # Avoid circular wotsits.
930 from lp.soyuz.model.binarypackagebuild import BinaryPackageBuild
931 from lp.soyuz.model.distroarchseries import DistroArchSeries
932- from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
933- source_select = Select(
934- SourcePackageRelease.id,
935- And(
936- BinaryPackageBuild.source_package_release_id ==
937- SourcePackageRelease.id,
938- BinaryPackageRelease.build == BinaryPackageBuild.id,
939- self.binarypackagereleaseID == BinaryPackageRelease.id,
940- ))
941+
942 pubs = [
943 BinaryPackageBuild.source_package_release_id ==
944- SourcePackageRelease.id,
945- SourcePackageRelease.id.is_in(source_select),
946+ self.binarypackagerelease.build.source_package_release_id,
947 BinaryPackageRelease.build == BinaryPackageBuild.id,
948 BinaryPackagePublishingHistory.binarypackagereleaseID ==
949 BinaryPackageRelease.id,