← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:built-using-domination into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:built-using-domination into launchpad:master with ~cjwatson/launchpad:built-using-guard-deletion as a prerequisite.

Commit message:
Handle Built-Using references in the dominator

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1868558 in Launchpad itself: "Honour Built-Using field"
  https://bugs.launchpad.net/launchpad/+bug/1868558

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/381240

Keep source publications with Built-Using references from active binary publications.  This may extend to reinstating the source publication (via a copy) if it had already been superseded or deleted.

It's possible for this to cause confusing effects if a manual deletion races with a build that produces binaries with a Built-Using reference to the deleted source.  I've guarded against this as best I can, and hope the remaining cases will be rare, but err on the side of honouring the reference.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:built-using-domination into launchpad:master.
diff --git a/lib/lp/archivepublisher/domination.py b/lib/lp/archivepublisher/domination.py
index a02fd78..1e896e0 100644
--- a/lib/lp/archivepublisher/domination.py
+++ b/lib/lp/archivepublisher/domination.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2019 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2020 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Archive Domination class.
@@ -68,10 +68,12 @@ from storm.expr import (
     And,
     Count,
     Desc,
+    Or,
     Select,
     )
 from zope.component import getUtility
 
+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.registry.model.sourcepackagename import SourcePackageName
 from lp.services.database.bulk import load_related
 from lp.services.database.constants import UTC_NOW
@@ -84,8 +86,12 @@ from lp.services.database.sqlbase import (
 from lp.services.orderingcheck import OrderingCheck
 from lp.soyuz.enums import (
     BinaryPackageFormat,
+    BinarySourceReferenceType,
     PackagePublishingStatus,
     )
+from lp.soyuz.interfaces.binarysourcereference import (
+    IBinarySourceReferenceSet,
+    )
 from lp.soyuz.interfaces.publishing import (
     inactive_publishing_status,
     IPublishingSet,
@@ -93,6 +99,7 @@ from lp.soyuz.interfaces.publishing import (
 from lp.soyuz.model.binarypackagebuild import BinaryPackageBuild
 from lp.soyuz.model.binarypackagename import BinaryPackageName
 from lp.soyuz.model.binarypackagerelease import BinaryPackageRelease
+from lp.soyuz.model.binarysourcereference import BinarySourceReference
 from lp.soyuz.model.publishing import (
     BinaryPackagePublishingHistory,
     SourcePackagePublishingHistory,
@@ -207,19 +214,44 @@ class GeneralizedPublication:
         return sorted(publications, cmp=self.compare, reverse=True)
 
 
-def find_live_source_versions(sorted_pubs):
+def get_source_versions(source_publications):
+    """List versions for sequence of `SourcePackagePublishingHistory`.
+
+    :param source_publications: An iterable of
+        `SourcePackagePublishingHistory`.
+    :return: A list of the publications' respective versions.
+    """
+    return [pub.sourcepackagerelease.version for pub in source_publications]
+
+
+def find_live_source_versions(sorted_pubs, built_using=None):
     """Find versions out of Published publications that should stay live.
 
-    This particular notion of liveness applies to source domination: the
-    latest version stays live, and that's it.
+    This particular notion of liveness applies to source domination:
+    normally, the latest version stays live, and that's it.  The exception
+    is if live binary publications have Built-Using fields referring to some
+    source versions, in which case those versions stay live too.
 
     :param sorted_pubs: An iterable of `SourcePackagePublishingHistory`
         sorted by descending package version.
+    :param built_using: An optional collection of `BinarySourceReference`s
+        corresponding to published binary publications with Built-Using
+        references; the SPRs that these refer to should stay live.
     :return: A list of live versions.
     """
+    if built_using is None:
+        built_using = set()
+    built_using_spr_ids = set(
+        bsr.source_package_release_id for bsr in built_using)
+
     # Given the required sort order, the latest version is at the head
     # of the list.
-    return [sorted_pubs[0].sourcepackagerelease.version]
+    sorted_pubs = list(sorted_pubs)
+    latest = sorted_pubs.pop(0)
+    return get_source_versions(
+        [latest] + [
+            pub for pub in sorted_pubs
+            if pub.sourcepackagereleaseID in built_using_spr_ids])
 
 
 def get_binary_versions(binary_publications):
@@ -707,19 +739,22 @@ class Dominator:
 
         execute_plan()
 
-    def _composeActiveSourcePubsCondition(self, distroseries, pocket):
+    def _composeRelevantSourcePubsCondition(self, distroseries, pocket,
+                                            active=True):
         """Compose ORM condition for restricting relevant source pubs."""
         SPPH = SourcePackagePublishingHistory
 
-        return And(
-            SPPH.status == PackagePublishingStatus.PUBLISHED,
+        clauses = [
             SPPH.distroseries == distroseries,
             SPPH.archive == self.archive,
             SPPH.pocket == pocket,
-            )
+            ]
+        if active:
+            clauses.append(SPPH.status == PackagePublishingStatus.PUBLISHED)
+        return And(*clauses)
 
     def findSourcesForDomination(self, distroseries, pocket):
-        """Find binary publications that need dominating.
+        """Find source publications that need dominating.
 
         This is only for traditional domination, where the latest published
         publication is always kept published.  See `find_live_source_versions`
@@ -728,17 +763,32 @@ class Dominator:
         To optimize for that logic, `findSourcesForDomination` will ignore
         publications that have no other publications competing for the same
         binary package.  There'd be nothing to do for those cases.
+
+        This also includes source publications whose source package releases
+        have a Built-Using reference pointing to them, whether active or
+        not.  These may need to be superseded or kept/reinstated depending
+        on whether binary publications referring to them are active.
         """
         SPPH = SourcePackagePublishingHistory
         SPR = SourcePackageRelease
+        BSR = BinarySourceReference
 
-        spph_location_clauses = self._composeActiveSourcePubsCondition(
+        spph_location_clauses = self._composeRelevantSourcePubsCondition(
             distroseries, pocket)
         candidate_source_names = Select(
             SPPH.sourcepackagenameID,
             And(join_spph_spr(), spph_location_clauses),
             group_by=SPPH.sourcepackagenameID,
             having=(Count() > 1))
+        built_using_spph_location_clauses = (
+            self._composeRelevantSourcePubsCondition(
+                distroseries, pocket, active=False))
+        built_using_sprs = Select(
+            SPPH.sourcepackagereleaseID,
+            And(
+                SPPH.sourcepackagereleaseID == BSR.source_package_release_id,
+                BSR.reference_type == BinarySourceReferenceType.BUILT_USING,
+                built_using_spph_location_clauses))
 
         # We'll also access the SourcePackageReleases associated with
         # the publications we find.  Since they're in the join anyway,
@@ -749,8 +799,13 @@ class Dominator:
         query = IStore(SPPH).find(
             (SPPH, SPR),
             join_spph_spr(),
-            SPPH.sourcepackagenameID.is_in(candidate_source_names),
-            spph_location_clauses)
+            Or(
+                And(
+                    SPPH.sourcepackagenameID.is_in(candidate_source_names),
+                    spph_location_clauses),
+                And(
+                    SPPH.sourcepackagereleaseID.is_in(built_using_sprs),
+                    built_using_spph_location_clauses)))
         spphs = DecoratedResultSet(query, itemgetter(0))
         load_related(SourcePackageName, spphs, ['sourcepackagenameID'])
         return spphs
@@ -771,20 +826,55 @@ class Dominator:
         sources = self.findSourcesForDomination(distroseries, pocket)
         sorted_packages = self._sortPackages(sources, generalization)
         supersede = []
+        keep = set()
         delete = []
 
+        # Of the SPRs associated with publications being considered for
+        # domination, find those that have a Built-Using reference pointing
+        # to them from a live binary publication.
+        bsr_set = getUtility(IBinarySourceReferenceSet)
+        built_using = bsr_set.findPublished(
+            self.archive, distroseries, pocket,
+            BinarySourceReferenceType.BUILT_USING,
+            source_package_releases=[
+                pub.sourcepackagerelease for pub in sources])
+
         self.logger.debug("Dominating sources...")
         for name, pubs in sorted_packages.iteritems():
             self.logger.debug("Dominating %s" % name)
             assert len(pubs) > 0, "Dominating zero sources!"
-            live_versions = find_live_source_versions(pubs)
-            cur_supersede, _, cur_delete = self.planPackageDomination(
+            live_versions = find_live_source_versions(pubs, built_using)
+            cur_supersede, cur_keep, cur_delete = self.planPackageDomination(
                 pubs, live_versions, generalization)
             supersede.extend(cur_supersede)
+            keep.update(cur_keep)
             delete.extend(cur_delete)
 
         for pub, dominant in supersede:
             pub.supersede(dominant, logger=self.logger)
+        for pub in keep:
+            if pub.status not in (
+                    PackagePublishingStatus.PENDING,
+                    PackagePublishingStatus.PUBLISHED):
+                # The dominator thinks that we should keep this package, but
+                # it isn't currently published.  This can happen when a
+                # source package has initially been superseded, but then a
+                # binary package that references it in Built-Using is
+                # published, requiring the source package to be reinstated.
+                # To cope with this, we'll copy the source back into place,
+                # thereby creating a new PENDING publication record for it,
+                # which will cause the publisher to put it back on disk.
+                #
+                # In general we'd prefer that the dominator not undo manual
+                # deletions, but Built-Using often expresses a compliance
+                # requirement, so we intentionally don't check that here.
+                # Instead, SPPH.requestDeletion checks for live
+                # BinarySourceReferences, and the uploader rejects uploads
+                # of builds with Built-Using references to source packages
+                # that have been manually deleted.
+                pub.copyTo(
+                    distroseries, pocket, self.archive,
+                    creator=getUtility(ILaunchpadCelebrities).janitor)
         for pub in delete:
             pub.requestDeletion(None)
 
@@ -804,7 +894,7 @@ class Dominator:
             looking_for,
             join_spph_spr(),
             join_spph_spn(),
-            self._composeActiveSourcePubsCondition(distroseries, pocket))
+            self._composeRelevantSourcePubsCondition(distroseries, pocket))
         return result.group_by(SourcePackageName.name)
 
     def findPublishedSPPHs(self, distroseries, pocket, package_name):
@@ -817,7 +907,7 @@ class Dominator:
             join_spph_spr(),
             join_spph_spn(),
             SourcePackageName.name == package_name,
-            self._composeActiveSourcePubsCondition(distroseries, pocket))
+            self._composeRelevantSourcePubsCondition(distroseries, pocket))
         # Sort by descending version (SPR.version has type debversion in
         # the database, so this should be a real proper comparison) so
         # that _sortPackage will have slightly less work to do later.
diff --git a/lib/lp/archivepublisher/tests/test_dominator.py b/lib/lp/archivepublisher/tests/test_dominator.py
index b88da56..bddb5f6 100755
--- a/lib/lp/archivepublisher/tests/test_dominator.py
+++ b/lib/lp/archivepublisher/tests/test_dominator.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2019 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2020 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for domination.py."""
@@ -397,6 +397,113 @@ class TestDominator(TestNativePublishingBase):
         for pub in overrides_2:
             self.assertEqual(PackagePublishingStatus.PUBLISHED, pub.status)
 
+    def test_dominateSources_keeps_built_using_refs(self):
+        foo_10_src = self.getPubSource(
+            sourcename="foo", version="1.0", architecturehintlist="i386",
+            status=PackagePublishingStatus.PUBLISHED)
+        [foo_10_i386_bin] = self.getPubBinaries(
+            binaryname="foo-bin", status=PackagePublishingStatus.PUBLISHED,
+            architecturespecific=True, version="1.0", pub_source=foo_10_src)
+        foo_11_src = self.getPubSource(
+            sourcename="foo", version="1.1", architecturehintlist="i386",
+            status=PackagePublishingStatus.PUBLISHED)
+        [foo_11_i386_bin] = self.getPubBinaries(
+            binaryname="foo-bin", status=PackagePublishingStatus.PUBLISHED,
+            architecturespecific=True, version="1.1", pub_source=foo_11_src)
+        bar_10_src = self.getPubSource(
+            sourcename="bar", version="1.0", architecturehintlist="i386",
+            status=PackagePublishingStatus.PUBLISHED)
+        [bar_10_i386_bin] = self.getPubBinaries(
+            binaryname="bar-bin", status=PackagePublishingStatus.PUBLISHED,
+            architecturespecific=True, version="1.0", pub_source=bar_10_src,
+            built_using="foo (= 1.0)")
+
+        dominator = Dominator(self.logger, self.ubuntutest.main_archive)
+        dominator.judgeAndDominate(
+            foo_10_src.distroseries, foo_10_src.pocket)
+
+        self.checkPublications(
+            [foo_10_src,
+             foo_11_src, foo_11_i386_bin,
+             bar_10_src, bar_10_i386_bin],
+            PackagePublishingStatus.PUBLISHED)
+        self.checkPublication(
+            foo_10_i386_bin, PackagePublishingStatus.SUPERSEDED)
+
+        bar_11_src = self.getPubSource(
+            sourcename="bar", version="1.1", architecturehintlist="i386",
+            status=PackagePublishingStatus.PUBLISHED)
+        [bar_11_i386_bin] = self.getPubBinaries(
+            binaryname="bar-bin", status=PackagePublishingStatus.PUBLISHED,
+            architecturespecific=True, version="1.1", pub_source=bar_11_src,
+            built_using="foo (= 1.1)")
+
+        dominator.judgeAndDominate(
+            foo_10_src.distroseries, foo_10_src.pocket)
+
+        self.checkPublications(
+            [foo_11_src, foo_11_i386_bin,
+             bar_11_src, bar_11_i386_bin],
+            PackagePublishingStatus.PUBLISHED)
+        self.checkPublications(
+            [foo_10_src, foo_10_i386_bin,
+             bar_10_src, bar_10_i386_bin],
+            PackagePublishingStatus.SUPERSEDED)
+
+    def test_dominateSources_reinstates_superseded_built_using_refs(self):
+        foo_10_src = self.getPubSource(
+            sourcename="foo", version="1.0", architecturehintlist="i386",
+            status=PackagePublishingStatus.PUBLISHED)
+        [foo_10_i386_bin] = self.getPubBinaries(
+            binaryname="foo-bin", status=PackagePublishingStatus.PUBLISHED,
+            architecturespecific=True, version="1.0", pub_source=foo_10_src)
+        foo_11_src = self.getPubSource(
+            sourcename="foo", version="1.1", architecturehintlist="i386",
+            status=PackagePublishingStatus.PUBLISHED)
+        [foo_11_i386_bin] = self.getPubBinaries(
+            binaryname="foo-bin", status=PackagePublishingStatus.PUBLISHED,
+            architecturespecific=True, version="1.1", pub_source=foo_11_src)
+        bar_10_src = self.getPubSource(
+            sourcename="bar", version="1.0", architecturehintlist="i386",
+            status=PackagePublishingStatus.PUBLISHED)
+
+        dominator = Dominator(self.logger, self.ubuntutest.main_archive)
+        dominator.judgeAndDominate(
+            foo_10_src.distroseries, foo_10_src.pocket)
+
+        self.checkPublications(
+            [foo_11_src, foo_11_i386_bin,
+             bar_10_src],
+            PackagePublishingStatus.PUBLISHED)
+        self.checkPublications(
+            [foo_10_src, foo_10_i386_bin], PackagePublishingStatus.SUPERSEDED)
+
+        [bar_10_i386_bin] = self.getPubBinaries(
+            binaryname="bar-bin", status=PackagePublishingStatus.PUBLISHED,
+            architecturespecific=True, version="1.0", pub_source=bar_10_src,
+            built_using="foo (= 1.0)")
+
+        dominator.judgeAndDominate(
+            foo_10_src.distroseries, foo_10_src.pocket)
+
+        self.checkPublications(
+            [foo_11_src, foo_11_i386_bin,
+             bar_10_src, bar_10_i386_bin],
+            PackagePublishingStatus.PUBLISHED)
+        self.checkPublications(
+            [foo_10_src, foo_10_i386_bin], PackagePublishingStatus.SUPERSEDED)
+
+        foo_10_srcs = foo_10_src.archive.getPublishedSources(
+            name="foo", version="1.0", distroseries=foo_10_src.distroseries,
+            pocket=foo_10_src.pocket, exact_match=True)
+        foo_10_i386_bins = foo_10_i386_bin.archive.getAllPublishedBinaries(
+            name="foo-bin", version="1.0",
+            distroarchseries=foo_10_i386_bin.distroarchseries,
+            pocket=foo_10_i386_bin.pocket, exact_match=True)
+        self.assertEqual(2, foo_10_srcs.count())
+        self.assertEqual(1, foo_10_i386_bins.count())
+        self.checkPublication(foo_10_srcs[0], PackagePublishingStatus.PENDING)
+
 
 class TestDomination(TestNativePublishingBase):
     """Test overall domination procedure."""
diff --git a/lib/lp/soyuz/interfaces/binarysourcereference.py b/lib/lp/soyuz/interfaces/binarysourcereference.py
index 5f7c321..837b171 100644
--- a/lib/lp/soyuz/interfaces/binarysourcereference.py
+++ b/lib/lp/soyuz/interfaces/binarysourcereference.py
@@ -47,6 +47,14 @@ class IBinarySourceReference(Interface):
         vocabulary=BinarySourceReferenceType,
         required=True, readonly=True)
 
+    # Export IDs for use by the dominator.
+    binary_package_release_id = Int(
+        title=_("The referencing binary package release ID."),
+        required=True, readonly=True)
+    source_package_release_id = Int(
+        title=_("The referencing source package release ID."),
+        required=True, readonly=True)
+
 
 class IBinarySourceReferenceSet(Interface):
     """A set of references from binary packages to source packages."""

Follow ups