Merge lp:~jtv/launchpad/bug-845326 into lp:launchpad

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Julian Edwards
Approved revision: no longer in the source branch.
Merged at revision: 13911
Proposed branch: lp:~jtv/launchpad/bug-845326
Merge into: lp:launchpad
Prerequisite: lp:~jtv/launchpad/transitional-published
Diff against target: 288 lines (+177/-22)
3 files modified
lib/lp/archivepublisher/domination.py (+19/-4)
lib/lp/archivepublisher/tests/test_dominator.py (+152/-17)
lib/lp/soyuz/model/publishing.py (+6/-1)
To merge this branch: bzr merge lp:~jtv/launchpad/bug-845326
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Review via email: mp+74741@code.launchpad.net

This proposal supersedes a proposal from 2011-09-09.

Commit message

[r=julian-edwards][bug=845326] Make newer [BS]PPHs supersede older ones for same version.

Description of the change

= Summary =

I'm re-doing the domination process so that one Dominator can serve the needs of traditional, latest-publication-record-wins domination; versions-we-see-in-Sources-lists-live domination as needed in Gina; and the as-yet unspecified multiple-versions-can-live-side-by-side domination we are supposed to get in the future.

My code for this was not a full replacement for traditional domination. A single package release can have multiple active publications in the same archive, distroseries, and pocket (e.g. when the package is being re-published into a different component) and traditional domination would mark the newer publication as superseding the older one. The challenge for this branch was to fix that, in a way that was still general enough to serve Gina's needs. All that Gina really knows is what version numbers should survive.

== Proposed fix ==

Extend the new-style domination algorithm: when domination finds multiple publication records for the same package (and archive etc.), for a version that should stay published, then have the newest one supersede the older ones.

(My previous generalized code would keep all publications for the version published. The classic dominator code would only keep the newest publication of the very latest version published and supersede all older ones.)

== Pre-implementation notes ==

William seems to think the solution is sane. As far as we're aware right now, this is the last step before we can land and deploy transitional Gina domination.

== Implementation details ==

The fix itself is fairly straightforward. It's in the first file in the diff. It involves one redundant variable and an extra case in an if/elif cadence, but overall I think it works out pretty cleanly.

== Tests ==

Besides a unit test for the new behaviour, for good measure I also added a massive test for complex combined data. This is not meant to replace proper unit tests; it's too complex for that. But it may reveal any breakage that the unit tests might miss.

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

== Demo and Q/A ==

A bunch of branches are going to land together. We'll have to make sure that domination still works; that Gina still works; and that Gina now does proper domination.

= Launchpad lint =

There's some pre-existing lint in the dependent branches that either can't be fixed, or would increase the risk of conflicts too much. I did not create any lint of my own, however, and left less than I found.

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/archivepublisher/domination.py
  lib/lp/soyuz/scripts/gina/dominate.py
  lib/lp/soyuz/model/publishing.py
  lib/lp/soyuz/doc/gina.txt
  lib/lp/soyuz/interfaces/publishing.py
  scripts/gina.py
  lib/lp/archivepublisher/tests/test_dominator.py
  lib/lp/soyuz/scripts/tests/test_gina.py
  lib/lp/soyuz/scripts/gina/handlers.py

./lib/lp/soyuz/doc/gina.txt
     113: narrative exceeds 78 characters.
     162: want exceeds 78 characters.
     179: want exceeds 78 characters.
     189: narrative uses a moin header.
     221: want exceeds 78 characters.
     234: want exceeds 78 characters.
     240: want exceeds 78 characters.
     295: source exceeds 78 characters.
     324: narrative uses a moin header.
     342: narrative exceeds 78 characters.
     354: narrative uses a moin header.
     360: narrative exceeds 78 characters.
     361: narrative exceeds 78 characters.
     459: narrative uses a moin header.
     461: narrative exceeds 78 characters.
     462: narrative exceeds 78 characters.
     477: narrative uses a moin header.
     563: narrative exceeds 78 characters.
     600: narrative uses a moin header.
     657: narrative uses a moin header.
     746: narrative uses a moin header.
     767: narrative uses a moin header.
     780: narrative uses a moin header.
./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
Julian Edwards (julian-edwards) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/archivepublisher/domination.py'
--- lib/lp/archivepublisher/domination.py 2011-09-09 18:57:14 +0000
+++ lib/lp/archivepublisher/domination.py 2011-09-09 18:57:18 +0000
@@ -193,9 +193,11 @@
193 def dominatePackage(self, publications, live_versions, generalization):193 def dominatePackage(self, publications, live_versions, generalization):
194 """Dominate publications for a single package.194 """Dominate publications for a single package.
195195
196 Active publications for versions in `live_versions` stay active.196 The latest publication for any version in `live_versions` stays
197 Any older versions are marked as superseded by the respective197 active. Any older publications (including older publications for
198 oldest live versions that are newer than the superseded ones.198 live versions with multiple publications) are marked as superseded by
199 the respective oldest live releases that are newer than the superseded
200 ones.
199201
200 Any versions that are newer than anything in `live_versions` are202 Any versions that are newer than anything in `live_versions` are
201 marked as deleted. This should not be possible in Soyuz-native203 marked as deleted. This should not be possible in Soyuz-native
@@ -222,12 +224,25 @@
222 publications, cmp=generalization.compare, reverse=True)224 publications, cmp=generalization.compare, reverse=True)
223225
224 current_dominant = None226 current_dominant = None
227 dominant_version = None
228
225 for pub in publications:229 for pub in publications:
226 if generalization.getPackageVersion(pub) in live_versions:230 version = generalization.getPackageVersion(pub)
231 # There should never be two published releases with the same
232 # version. So this comparison is really a string
233 # comparison, not a version comparison: if the versions are
234 # equal by either measure, they're from the same release.
235 if dominant_version is not None and version == dominant_version:
236 # This publication is for a live version, but has been
237 # superseded by a newer publication of the same version.
238 # Supersede it.
239 pub.supersede(current_dominant, logger=self.logger)
240 elif version in live_versions:
227 # This publication stays active; if any publications241 # This publication stays active; if any publications
228 # that follow right after this are to be superseded,242 # that follow right after this are to be superseded,
229 # this is the release that they are superseded by.243 # this is the release that they are superseded by.
230 current_dominant = pub244 current_dominant = pub
245 dominant_version = version
231 elif current_dominant is None:246 elif current_dominant is None:
232 # This publication is no longer live, but there is no247 # This publication is no longer live, but there is no
233 # newer version to supersede it either. Therefore it248 # newer version to supersede it either. Therefore it
234249
=== modified file 'lib/lp/archivepublisher/tests/test_dominator.py'
--- lib/lp/archivepublisher/tests/test_dominator.py 2011-09-09 18:57:14 +0000
+++ lib/lp/archivepublisher/tests/test_dominator.py 2011-09-09 18:57:18 +0000
@@ -5,9 +5,9 @@
55
6__metaclass__ = type6__metaclass__ = type
77
8import apt_pkg
8import datetime9import datetime
910from operator import attrgetter
10import apt_pkg
11from zope.security.proxy import removeSecurityProxy11from zope.security.proxy import removeSecurityProxy
1212
13from canonical.database.sqlbase import flush_database_updates13from canonical.database.sqlbase import flush_database_updates
@@ -248,24 +248,25 @@
248 return [spph.sourcepackagerelease.version for spph in spphs]248 return [spph.sourcepackagerelease.version for spph in spphs]
249249
250250
251def alter_creation_dates(spphs, ages):
252 """Set `datecreated` on each of `spphs` according to `ages`.
253
254 :param spphs: Iterable of `SourcePackagePublishingHistory`. Their
255 respective creation dates will be offset by the respective ages found
256 in `ages` (with the two being matched up in the same order).
257 :param ages: Iterable of ages. Must provide the same number of items as
258 `spphs`. Ages are `timedelta` objects that will be subtracted from
259 the creation dates on the respective records in `spph`.
260 """
261 for spph, age in zip(spphs, ages):
262 spph.datecreated -= age
263
264
251class TestGeneralizedPublication(TestCaseWithFactory):265class TestGeneralizedPublication(TestCaseWithFactory):
252 """Test publication generalization helpers."""266 """Test publication generalization helpers."""
253267
254 layer = ZopelessDatabaseLayer268 layer = ZopelessDatabaseLayer
255269
256 def alterCreationDates(self, spphs, ages):
257 """Set `datecreated` on each of `spphs` according to `ages`.
258
259 :param spphs: Iterable of `SourcePackagePublishingHistory`. Their
260 respective creation dates will be offset by the respective ages
261 found in `ages` (with the two being matched up in the same order).
262 :param ages: Iterable of ages. Must provide the same number of items
263 as `spphs`. Ages are `timedelta` objects that will be subtracted
264 from the creation dates on the respective records in `spph`.
265 """
266 for spph, age in zip(spphs, ages):
267 spph.datecreated -= age
268
269 def test_getPackageVersion_gets_source_version(self):270 def test_getPackageVersion_gets_source_version(self):
270 spph = self.factory.makeSourcePackagePublishingHistory()271 spph = self.factory.makeSourcePackagePublishingHistory()
271 self.assertEqual(272 self.assertEqual(
@@ -327,7 +328,7 @@
327 sourcepackagerelease=spr, distroseries=distroseries,328 sourcepackagerelease=spr, distroseries=distroseries,
328 pocket=pocket)329 pocket=pocket)
329 for counter in xrange(len(ages))]330 for counter in xrange(len(ages))]
330 self.alterCreationDates(spphs, ages)331 alter_creation_dates(spphs, ages)
331332
332 self.assertEqual(333 self.assertEqual(
333 [spphs[2], spphs[0], spphs[1]],334 [spphs[2], spphs[0], spphs[1]],
@@ -351,13 +352,27 @@
351 sourcepackagerelease=self.factory.makeSourcePackageRelease(352 sourcepackagerelease=self.factory.makeSourcePackageRelease(
352 version=version))353 version=version))
353 for counter in xrange(len(ages))]354 for counter in xrange(len(ages))]
354 self.alterCreationDates(spphs, ages)355 alter_creation_dates(spphs, ages)
355356
356 self.assertEqual(357 self.assertEqual(
357 [spphs[2], spphs[0], spphs[1]],358 [spphs[2], spphs[0], spphs[1]],
358 sorted(spphs, cmp=GeneralizedPublication().compare))359 sorted(spphs, cmp=GeneralizedPublication().compare))
359360
360361
362def jumble(ordered_list):
363 """Jumble the elements of `ordered_list` into a weird order.
364
365 Ordering is very important in domination. We jumble some of our lists to
366 insure against "lucky coincidences" that might give our tests the right
367 answers for the wrong reasons.
368 """
369 even = [
370 item for offset, item in enumerate(ordered_list) if offset % 2 == 0]
371 odd = [
372 item for offset, item in enumerate(ordered_list) if offset % 2 != 0]
373 return list(reversed(odd)) + even
374
375
361class TestDominatorMethods(TestCaseWithFactory):376class TestDominatorMethods(TestCaseWithFactory):
362377
363 layer = ZopelessDatabaseLayer378 layer = ZopelessDatabaseLayer
@@ -435,6 +450,126 @@
435 self.assertEqual(None, pubs[1].supersededby)450 self.assertEqual(None, pubs[1].supersededby)
436 self.assertEqual(PackagePublishingStatus.DELETED, pubs[1].status)451 self.assertEqual(PackagePublishingStatus.DELETED, pubs[1].status)
437452
453 def test_dominatePackage_supersedes_replaced_pub_for_live_version(self):
454 # Even if a publication record is for a live version, a newer
455 # one for the same version supersedes it.
456 spr = self.factory.makeSourcePackageRelease()
457 series = self.factory.makeDistroSeries()
458 pocket = PackagePublishingPocket.RELEASE
459 pubs = [
460 self.factory.makeSourcePackagePublishingHistory(
461 archive=series.main_archive, distroseries=series,
462 pocket=pocket, status=PackagePublishingStatus.PUBLISHED,
463 sourcepackagerelease=spr)
464 for counter in xrange(3)]
465 alter_creation_dates(pubs, [
466 datetime.timedelta(3),
467 datetime.timedelta(2),
468 datetime.timedelta(1),
469 ])
470
471 self.makeDominator(pubs).dominatePackage(
472 pubs, [spr.version], GeneralizedPublication(True))
473 self.assertEqual([
474 PackagePublishingStatus.SUPERSEDED,
475 PackagePublishingStatus.SUPERSEDED,
476 PackagePublishingStatus.PUBLISHED,
477 ],
478 [pub.status for pub in pubs])
479 self.assertEqual(
480 [spr, spr, None], [pub.supersededby for pub in pubs])
481
482 def test_dominatePackage_advanced_scenario(self):
483 # Put dominatePackage through its paces with complex combined
484 # data.
485 # This test should be redundant in theory (which in theory
486 # equates practice but in practice does not). If this fails,
487 # don't just patch up the code or this test. Create unit tests
488 # that specifically cover the difference, then change the code
489 # and/or adapt this test to return to harmony.
490 series = self.factory.makeDistroSeries()
491 package = self.factory.makeSourcePackageName()
492 pocket = PackagePublishingPocket.RELEASE
493
494 versions = ["1.%d" % number for number in xrange(4)]
495
496 # We have one package releases for each version.
497 relevant_releases = dict(
498 (version, self.factory.makeSourcePackageRelease(
499 sourcepackagename=package, version=version))
500 for version in jumble(versions))
501
502 # Each of those releases is subsequently published in
503 # different components.
504 components = jumble(
505 [self.factory.makeComponent() for version in versions])
506
507 # Map versions to lists of publications for that version, from
508 # oldest to newest. Each re-publishing into a different
509 # component is meant to supersede publication into the previous
510 # component.
511 pubs_by_version = dict(
512 (version, [
513 self.factory.makeSourcePackagePublishingHistory(
514 archive=series.main_archive, distroseries=series,
515 pocket=pocket, status=PackagePublishingStatus.PUBLISHED,
516 sourcepackagerelease=relevant_releases[version],
517 component=component)
518 for component in components])
519 for version in jumble(versions))
520
521 ages = jumble(
522 [datetime.timedelta(age) for age in xrange(len(versions))])
523
524 # Actually the "oldest to newest" order on the publications only
525 # applies to their creation dates. Their creation orders are
526 # irrelevant.
527 for pubs_list in pubs_by_version.itervalues():
528 alter_creation_dates(pubs_list, ages)
529 pubs_list.sort(key=attrgetter('datecreated'))
530
531 live_versions = ["1.1", "1.2"]
532 last_version_alive = sorted(live_versions)[-1]
533
534 all_pubs = sum(pubs_by_version.itervalues(), [])
535 Dominator(DevNullLogger(), series.main_archive).dominatePackage(
536 all_pubs, live_versions, GeneralizedPublication(True))
537
538 for version in reversed(versions):
539 pubs = pubs_by_version[version]
540
541 if version in live_versions:
542 # Beware: loop-carried variable. Used locally as well,
543 # but tells later iterations what the highest-versioned
544 # release so far was. This is used in tracking
545 # supersededby links.
546 superseding_release = pubs[-1].sourcepackagerelease
547
548 if version in live_versions:
549 # The live versions' latest publications are Published,
550 # their older ones Superseded.
551 expected_status = (
552 [PackagePublishingStatus.SUPERSEDED] * (len(pubs) - 1) +
553 [PackagePublishingStatus.PUBLISHED])
554 expected_supersededby = (
555 [superseding_release] * (len(pubs) - 1) + [None])
556 elif version < last_version_alive:
557 # The superseded versions older than the last live
558 # version have all been superseded.
559 expected_status = (
560 [PackagePublishingStatus.SUPERSEDED] * len(pubs))
561 expected_supersededby = [superseding_release] * len(pubs)
562 else:
563 # Versions that are newer than any live release have
564 # been deleted.
565 expected_status = (
566 [PackagePublishingStatus.DELETED] * len(pubs))
567 expected_supersededby = [None] * len(pubs)
568
569 self.assertEqual(expected_status, [pub.status for pub in pubs])
570 self.assertEqual(
571 expected_supersededby, [pub.supersededby for pub in pubs])
572
438 def test_dominateRemovedSourceVersions_dominates_publications(self):573 def test_dominateRemovedSourceVersions_dominates_publications(self):
439 # dominateRemovedSourceVersions finds the publications for a574 # dominateRemovedSourceVersions finds the publications for a
440 # package and calls dominatePackage on them.575 # package and calls dominatePackage on them.
441576
=== modified file 'lib/lp/soyuz/model/publishing.py'
--- lib/lp/soyuz/model/publishing.py 2011-09-09 18:57:14 +0000
+++ lib/lp/soyuz/model/publishing.py 2011-09-09 18:57:18 +0000
@@ -1966,12 +1966,17 @@
1966 ]1966 ]
1967 assert publication_class in permitted_classes, "Deleting wrong type."1967 assert publication_class in permitted_classes, "Deleting wrong type."
19681968
1969 if removed_by is None:
1970 removed_by_id = None
1971 else:
1972 removed_by_id = removed_by.id
1973
1969 affected_pubs = IMasterStore(publication_class).find(1974 affected_pubs = IMasterStore(publication_class).find(
1970 publication_class, publication_class.id.is_in(ids))1975 publication_class, publication_class.id.is_in(ids))
1971 affected_pubs.set(1976 affected_pubs.set(
1972 status=PackagePublishingStatus.DELETED,1977 status=PackagePublishingStatus.DELETED,
1973 datesuperseded=UTC_NOW,1978 datesuperseded=UTC_NOW,
1974 removed_byID=removed_by.id,1979 removed_byID=removed_by_id,
1975 removal_comment=removal_comment)1980 removal_comment=removal_comment)
19761981
1977 def requestDeletion(self, sources, removed_by, removal_comment=None):1982 def requestDeletion(self, sources, removed_by, removal_comment=None):