← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~adeuring/launchpad/bug-739065 into lp:launchpad

 

Abel Deuring has proposed merging lp:~adeuring/launchpad/bug-739065 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~adeuring/launchpad/bug-739065/+merge/61555

This branch is a first step to fix bug 739065: SourcePackage:+index timeouts 

The OOPS reports from that bug show that two SQL queries which return only one row each are repeated quite often; this branch lets the query in DistributionSourcePackageRelease.sample_binary_packages prejoin DistroSeriesPackageCache records.

Since it can happen that no DistroSeriesPackageCache exists for a given DistroSeriesBinaryPackage, I also changed its constructor so that passing the optional parameter cache=None means "no cache available", instead of "parameter not specified or no cache available".

A test run of the new query in DistributionSourcePackageRelease.sample_binary_packages indicates that it is slightly faster than the old query; avoiding the retrieval DistroSeriesPackageCache records in separate queries would save more than 3 seconds in OOPS 1904E1683.

DistributionSourcePackageRelease.sample_binary_packages was not tested at all, so I added a few general tests.

tests:

./bin/test soyuz -vvt test_distributionsourcepackagerelease
./bin/test soyuz -vvt test_distroseriesbinarypackage

no lint
-- 
https://code.launchpad.net/~adeuring/launchpad/bug-739065/+merge/61555
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~adeuring/launchpad/bug-739065 into lp:launchpad.
=== modified file 'lib/lp/soyuz/model/distributionsourcepackagerelease.py'
--- lib/lp/soyuz/model/distributionsourcepackagerelease.py	2010-08-20 20:31:18 +0000
+++ lib/lp/soyuz/model/distributionsourcepackagerelease.py	2011-05-19 12:41:50 +0000
@@ -12,11 +12,20 @@
     ]
 
 from lazr.delegates import delegates
-from storm.expr import Desc
+from storm.expr import (
+    And,
+    Desc,
+    Join,
+    LeftJoin,
+    SQL,
+    )
 from zope.component import getUtility
 from zope.interface import implements
 
 from canonical.database.sqlbase import sqlvalues
+from canonical.launchpad.components.decoratedresultset import (
+    DecoratedResultSet,
+    )
 from canonical.launchpad.webapp.interfaces import (
     DEFAULT_FLAVOR,
     IStoreSelector,
@@ -34,6 +43,7 @@
 from lp.soyuz.model.binarypackagename import BinaryPackageName
 from lp.soyuz.model.binarypackagerelease import BinaryPackageRelease
 from lp.soyuz.model.distroseriesbinarypackage import DistroSeriesBinaryPackage
+from lp.soyuz.model.distroseriespackagecache import DistroSeriesPackageCache
 from lp.soyuz.model.publishing import (
     BinaryPackagePublishingHistory,
     SourcePackagePublishingHistory,
@@ -152,33 +162,59 @@
     @property
     def sample_binary_packages(self):
         """See IDistributionSourcePackageRelease."""
-        all_published = BinaryPackagePublishingHistory.select("""
-            BinaryPackagePublishingHistory.distroarchseries =
-                DistroArchSeries.id AND
-            DistroArchSeries.distroseries = DistroSeries.id AND
-            DistroSeries.distribution = %s AND
-            BinaryPackagePublishingHistory.archive IN %s AND
-            BinaryPackagePublishingHistory.binarypackagerelease =
-                BinaryPackageRelease.id AND
-            BinaryPackageRelease.binarypackagename = BinaryPackageName.id AND
-            BinaryPackageRelease.build = BinaryPackageBuild.id AND
-            BinaryPackageBuild.source_package_release = %s
-            """ % sqlvalues(self.distribution,
-                            self.distribution.all_distro_archive_ids,
-                            self.sourcepackagerelease),
-            distinct=True,
-            orderBy=['BinaryPackageName.name'],
-            clauseTables=['DistroArchSeries', 'DistroSeries',
-                          'BinaryPackageRelease', 'BinaryPackageName',
-                          'BinaryPackageBuild'],
-            prejoinClauseTables=['BinaryPackageRelease', 'BinaryPackageName'])
+        #avoid circular imports.
+        from lp.registry.model.distroseries import DistroSeries
+        from lp.soyuz.model.distroarchseries import DistroArchSeries
+        store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
+        archive_ids = list(self.distribution.all_distro_archive_ids)
+        result_row = (
+            SQL('DISTINCT ON(BinaryPackageName.name) 0 AS ignore'),
+            BinaryPackagePublishingHistory, DistroSeriesPackageCache,
+            BinaryPackageRelease, BinaryPackageName)
+        tables = (
+            BinaryPackagePublishingHistory,
+            Join(
+                DistroArchSeries,
+                DistroArchSeries.id ==
+                 BinaryPackagePublishingHistory.distroarchseriesID),
+            Join(
+                DistroSeries,
+                DistroArchSeries.distroseriesID == DistroSeries.id),
+            Join(
+                BinaryPackageRelease,
+                BinaryPackageRelease.id ==
+                BinaryPackagePublishingHistory.binarypackagereleaseID),
+            Join(
+                BinaryPackageName,
+                BinaryPackageName.id ==
+                BinaryPackageRelease.binarypackagenameID),
+            Join(
+                BinaryPackageBuild,
+                BinaryPackageBuild.id == BinaryPackageRelease.buildID),
+            LeftJoin(
+                DistroSeriesPackageCache,
+                And(
+                    DistroSeriesPackageCache.distroseries == DistroSeries.id,
+                    DistroSeriesPackageCache.archiveID.is_in(archive_ids),
+                    DistroSeriesPackageCache.binarypackagename ==
+                    BinaryPackageName.id)))
+
+        all_published = store.using(*tables).find(
+            result_row,
+            DistroSeries.distribution == self.distribution,
+            BinaryPackagePublishingHistory.archiveID.is_in(archive_ids),
+            BinaryPackageBuild.source_package_release ==
+                self.sourcepackagerelease)
+        all_published = all_published.order_by(
+            BinaryPackageName.name)
+        all_published = DecoratedResultSet(
+            all_published, lambda row: row[1:3])
+
         samples = []
-        names = set()
-        for publishing in all_published:
-            if publishing.binarypackagerelease.binarypackagename not in names:
-                names.add(publishing.binarypackagerelease.binarypackagename)
-                samples.append(
-                    DistroSeriesBinaryPackage(
-                        publishing.distroarchseries.distroseries,
-                        publishing.binarypackagerelease.binarypackagename))
+        for publishing, package_cache in all_published:
+            samples.append(
+                DistroSeriesBinaryPackage(
+                    publishing.distroarchseries.distroseries,
+                    publishing.binarypackagerelease.binarypackagename,
+                    package_cache))
         return samples

=== modified file 'lib/lp/soyuz/model/distroseriesbinarypackage.py'
--- lib/lp/soyuz/model/distroseriesbinarypackage.py	2010-10-24 12:46:23 +0000
+++ lib/lp/soyuz/model/distroseriesbinarypackage.py	2011-05-19 12:41:50 +0000
@@ -39,10 +39,12 @@
 
     implements(IDistroSeriesBinaryPackage)
 
-    def __init__(self, distroseries, binarypackagename, cache=None):
+    default = object()
+
+    def __init__(self, distroseries, binarypackagename, cache=default):
         self.distroseries = distroseries
         self.binarypackagename = binarypackagename
-        if cache is not None:
+        if cache is not self.default:
             get_property_cache(self).cache = cache
 
     @property
@@ -69,9 +71,9 @@
             self.distroseries.distribution.all_distro_archive_ids)
         result = store.find(
             DistroSeriesPackageCache,
-            DistroSeriesPackageCache.distroseries==self.distroseries,
+            DistroSeriesPackageCache.distroseries == self.distroseries,
             DistroSeriesPackageCache.archiveID.is_in(archive_ids),
-            (DistroSeriesPackageCache.binarypackagename==
+            (DistroSeriesPackageCache.binarypackagename ==
                 self.binarypackagename))
         return result.any()
 

=== added file 'lib/lp/soyuz/tests/test_distributionsourcepackagerelease.py'
--- lib/lp/soyuz/tests/test_distributionsourcepackagerelease.py	1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/tests/test_distributionsourcepackagerelease.py	2011-05-19 12:41:50 +0000
@@ -0,0 +1,152 @@
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests of DistributionSourcePackageRelease."""
+
+from testtools.matchers import LessThan
+from zope.component import getUtility
+
+from canonical.testing.layers import DatabaseFunctionalLayer
+from lp.soyuz.enums import PackagePublishingStatus
+from lp.soyuz.interfaces.distroarchseries import IDistroArchSeriesSet
+from lp.soyuz.model.distributionsourcepackagerelease import (
+    DistributionSourcePackageRelease,
+    )
+from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
+from lp.testing import (
+    StormStatementRecorder,
+    TestCaseWithFactory,
+    )
+from lp.testing.matchers import HasQueryCount
+
+
+class TestDistributionSourcePackageRelease(TestCaseWithFactory):
+    """Tests for DistributionSourcePackageRelease."""
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestDistributionSourcePackageRelease, self).setUp()
+        self.sourcepackagerelease = self.factory.makeSourcePackageRelease()
+        self.distroarchseries = self.factory.makeDistroArchSeries(
+            distroseries=self.sourcepackagerelease.sourcepackage.distroseries)
+        distribution = self.distroarchseries.distroseries.distribution
+        self.dsp_release = DistributionSourcePackageRelease(
+            distribution, self.sourcepackagerelease)
+
+    def makeBinaryPackageRelease(self, name=None):
+        if name is None:
+            name = self.factory.makeBinaryPackageName()
+        bp_build = self.factory.makeBinaryPackageBuild(
+            source_package_release=self.sourcepackagerelease,
+            distroarchseries=self.distroarchseries)
+        bp_release = self.factory.makeBinaryPackageRelease(
+            build=bp_build, binarypackagename=name)
+        sourcepackagename = self.sourcepackagerelease.sourcepackagename
+        self.factory.makeSourcePackagePublishingHistory(
+            sourcepackagename,
+            sourcepackagerelease=self.sourcepackagerelease,
+            distroseries=self.sourcepackagerelease.sourcepackage.distroseries,
+            status=PackagePublishingStatus.PUBLISHED)
+        self.factory.makeBinaryPackagePublishingHistory(
+            binarypackagerelease=bp_release,
+            distroarchseries=self.distroarchseries)
+
+    def test_sample_binary_packages__no_releases(self):
+        # If no binary releases exist,
+        # DistributionSourcePackageRelease.sample_binary_packages is empty.
+        self.assertEqual([], self.dsp_release.sample_binary_packages)
+
+    def test_sample_binary_packages__one_release(self):
+        # If a binary release exists,
+        # DistributionSourcePackageRelease.sample_binary_packages
+        # returns it.
+        self.makeBinaryPackageRelease(
+            self.factory.makeBinaryPackageName(name='binary-package'))
+        self.assertEqual(
+            ['binary-package'],
+            [release.name
+             for release in self.dsp_release.sample_binary_packages])
+
+    def test_sample_binary_packages__two_releases_one_binary_package(self):
+        # If two binary releases with the same name exist,
+        # DistributionSourcePackageRelease.sample_binary_packages
+        # returns only one.
+        name = self.factory.makeBinaryPackageName(name='binary-package')
+        self.makeBinaryPackageRelease(name)
+        self.makeBinaryPackageRelease(name)
+        self.assertEqual(
+            ['binary-package'],
+            [release.name
+             for release in self.dsp_release.sample_binary_packages])
+
+    def test_sample_binary_packages__two_release_two_binary_packages(self):
+        # If a two binary releases with different names exist,
+        # DistributionSourcePackageRelease.sample_binary_packages
+        # returns both.
+        self.makeBinaryPackageRelease(
+            self.factory.makeBinaryPackageName(name='binary-package'))
+        self.makeBinaryPackageRelease(
+            self.factory.makeBinaryPackageName(name='binary-package-2'))
+        self.assertEqual(
+            ['binary-package', 'binary-package-2'],
+            [release.name
+             for release in self.dsp_release.sample_binary_packages])
+
+    def updateDistroSeriesPackageCache(self):
+        # Create DistroSeriesPackageCache records for new binary
+        # packages.
+        #
+        # SoyuzTestPublisher.updateDistroSeriesPackageCache() creates
+        # a DistroSeriesPackageCache record for the new binary package.
+        # The method closes the current DB connection, making references
+        # to DB objects in other DB objects unusable. Starting with
+        # the distroarchseries, we can create new, valid, instances of
+        # objects required later in the test again.
+        # of the objects we need later.
+        sourcepackagename = self.sourcepackagerelease.sourcepackagename
+        publisher = SoyuzTestPublisher()
+        publisher.updateDistroSeriesPackageCache(
+            self.distroarchseries.distroseries)
+        self.distroarchseries = getUtility(IDistroArchSeriesSet).get(
+            self.distroarchseries.id)
+        distribution = self.distroarchseries.distroseries.distribution
+        releases = distribution.getCurrentSourceReleases([sourcepackagename])
+        [(distribution_sourcepackage, dsp_release)] = releases.items()
+        self.dsp_release = dsp_release
+        self.sourcepackagerelease = dsp_release.sourcepackagerelease
+
+    def test_sample_binary_packages__constant_number_sql_queries(self):
+        # Retrieving
+        # DistributionSourcePackageRelease.sample_binary_packages and
+        # accessing the property "summary" of its items requires a
+        # constant number of SQL queries, regardless of the number
+        # of existing binary package releases.
+        self.makeBinaryPackageRelease()
+        self.updateDistroSeriesPackageCache()
+        with StormStatementRecorder() as recorder:
+            for ds_package in self.dsp_release.sample_binary_packages:
+                ds_package.summary
+        self.assertThat(recorder, HasQueryCount(LessThan(5)))
+        self.assertEqual(1, len(self.dsp_release.sample_binary_packages))
+
+        for iteration in range(5):
+            self.makeBinaryPackageRelease()
+        self.updateDistroSeriesPackageCache()
+        with StormStatementRecorder() as recorder:
+            for ds_package in self.dsp_release.sample_binary_packages:
+                ds_package.summary
+        self.assertThat(recorder, HasQueryCount(LessThan(5)))
+        self.assertEqual(6, len(self.dsp_release.sample_binary_packages))
+
+        # Even if the cache is not updated for binary packages,
+        # DistributionSourcePackageRelease objects do not try to
+        # retrieve DistroSeriesPackageCache records if they know
+        # that such records do not exist.
+        for iteration in range(5):
+            self.makeBinaryPackageRelease()
+        with StormStatementRecorder() as recorder:
+            for ds_package in self.dsp_release.sample_binary_packages:
+                ds_package.summary
+        self.assertThat(recorder, HasQueryCount(LessThan(5)))
+        self.assertEqual(11, len(self.dsp_release.sample_binary_packages))

=== modified file 'lib/lp/soyuz/tests/test_distroseriesbinarypackage.py'
--- lib/lp/soyuz/tests/test_distroseriesbinarypackage.py	2010-12-20 03:21:03 +0000
+++ lib/lp/soyuz/tests/test_distroseriesbinarypackage.py	2011-05-19 12:41:50 +0000
@@ -16,7 +16,10 @@
 from lp.services.log.logger import BufferLogger
 from lp.soyuz.model.distroseriesbinarypackage import DistroSeriesBinaryPackage
 from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
-from lp.testing import TestCaseWithFactory
+from lp.testing import (
+    StormStatementRecorder,
+    TestCaseWithFactory,
+    )
 
 
 class TestDistroSeriesBinaryPackage(TestCaseWithFactory):
@@ -61,3 +64,22 @@
 
         self.failUnlessEqual(
             'Foo is the best', self.distroseries_binary_package.summary)
+
+    def test_none_cache_passed_at_init_counts_as_cached(self):
+        # If the value None is passed as the constructor parameter
+        # "cache", it is considered as a valid value.
+        # Accesing the property DistroSeriesBinaryPackage.cache
+        # later does not lead to the execution of an SQL query to
+        # retrieve a DistroSeriesPackageCache record.
+        binary_package = DistroSeriesBinaryPackage(
+            self.distroseries, self.binary_package_name, cache=None)
+        with StormStatementRecorder() as recorder:
+            binary_package.cache
+        self.assertEqual(0, len(recorder.statements))
+
+        # If the parameter "cache" was not passed, accessing
+        # DistroSeriesBinaryPackage.cache for the first time requires
+        # at least one SQL query.
+        with StormStatementRecorder() as recorder:
+            self.distroseries_binary_package.cache
+        self.assertTrue(len(recorder.statements) > 0)