← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/mirror-preload into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/mirror-preload into lp:launchpad.

Commit message:
Ensure that the various mirror views have constant query counts in the number of mirrors and countries involved.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1415447 in Launchpad itself: "Distribution:+disabledmirrors times out"
  https://bugs.launchpad.net/launchpad/+bug/1415447

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/mirror-preload/+merge/250185

Only some of the various mirror views were query-optimised.  This takes care of the rest.  I had to turn DistributionMirror.cdimage_series into a cachedproperty, which entailed a few changes in various places to cope with it returning a list rather than a ResultSet.

A TAL subtlety I hadn't previously known about: <element tal:condition="cond" tal:define="key context/method" /> will evaluate context.method() even if cond is False.  To avoid the evaluation, we must use an additional nested element.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/mirror-preload into lp:launchpad.
=== modified file 'lib/lp/registry/browser/distribution.py'
--- lib/lp/registry/browser/distribution.py	2014-10-22 19:25:39 +0000
+++ lib/lp/registry/browser/distribution.py	2015-02-18 18:49:05 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2014 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Browser views for distributions."""
@@ -1104,7 +1104,7 @@
 
     @cachedproperty
     def mirror_count(self):
-        return self.mirrors.count()
+        return len(self.mirrors)
 
     def _sum_throughput(self, mirrors):
         """Given a list of mirrors, calculate the total bandwidth
@@ -1181,10 +1181,6 @@
     def mirrors(self):
         return self.context.archive_mirrors_by_country
 
-    @cachedproperty
-    def mirror_count(self):
-        return len(self.mirrors)
-
 
 class DistributionSeriesMirrorsView(DistributionMirrorsView):
 
@@ -1197,10 +1193,6 @@
     def mirrors(self):
         return self.context.cdimage_mirrors_by_country
 
-    @cachedproperty
-    def mirror_count(self):
-        return len(self.mirrors)
-
 
 class DistributionMirrorsRSSBaseView(LaunchpadView):
     """A base class for RSS feeds of distribution mirrors."""

=== modified file 'lib/lp/registry/browser/tests/test_distribution_views.py'
--- lib/lp/registry/browser/tests/test_distribution_views.py	2014-08-01 06:04:58 +0000
+++ lib/lp/registry/browser/tests/test_distribution_views.py	2015-02-18 18:49:05 +0000
@@ -1,23 +1,34 @@
-# Copyright 2009-2014 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
 
 import soupmatchers
-from testtools.matchers import MatchesStructure
+from testtools.matchers import (
+    Equals,
+    MatchesStructure,
+    )
 from zope.component import getUtility
 
 from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet
 from lp.registry.browser.distribution import DistributionPublisherConfigView
 from lp.registry.interfaces.distribution import IDistributionSet
+from lp.registry.interfaces.distributionmirror import (
+    MirrorContent,
+    MirrorStatus,
+    )
 from lp.services.webapp.servers import LaunchpadTestRequest
+from lp.services.worlddata.interfaces.country import ICountrySet
 from lp.soyuz.interfaces.processor import IProcessorSet
 from lp.testing import (
     login,
     login_celebrity,
+    login_person,
+    record_two_runs,
     TestCaseWithFactory,
     )
 from lp.testing.layers import DatabaseFunctionalLayer
+from lp.testing.matchers import HasQueryCount
 from lp.testing.sampledata import LAUNCHPAD_ADMIN
 from lp.testing.views import create_initialized_view
 
@@ -333,3 +344,84 @@
                 'Header should say maintainer (not owner)', 'h1',
                 text='Change the maintainer of Boobuntu'))
         self.assertThat(view.render(), header_match)
+
+
+class TestDistributionMirrorsViewMixin:
+    """Mixin to help test a distribution mirrors view."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_query_count(self):
+        # The number of queries required to render the mirror table is
+        # constant in the number of mirrors.
+        person = self.factory.makePerson()
+        distro = self.factory.makeDistribution(owner=person)
+        login_celebrity("admin")
+        distro.supports_mirrors = True
+        login_person(person)
+        distro.mirror_admin = person
+        countries = iter(getUtility(ICountrySet))
+
+        def render_mirrors():
+            text = create_initialized_view(
+                distro, self.view, principal=person).render()
+            self.assertNotIn("We don't know of any", text)
+            return text
+
+        def create_mirror():
+            mirror = self.factory.makeMirror(
+                distro, country=next(countries), official_candidate=True)
+            self.configureMirror(mirror)
+
+        recorder1, recorder2 = record_two_runs(
+            render_mirrors, create_mirror, 10)
+        self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
+
+
+class TestDistributionArchiveMirrorsView(
+    TestDistributionMirrorsViewMixin, TestCaseWithFactory):
+
+    view = "+archivemirrors"
+
+    def configureMirror(self, mirror):
+        mirror.enabled = True
+        mirror.status = MirrorStatus.OFFICIAL
+
+
+class TestDistributionSeriesMirrorsView(
+    TestDistributionMirrorsViewMixin, TestCaseWithFactory):
+
+    view = "+cdmirrors"
+
+    def configureMirror(self, mirror):
+        mirror.enabled = True
+        mirror.content = MirrorContent.RELEASE
+        mirror.status = MirrorStatus.OFFICIAL
+
+
+class TestDistributionDisabledMirrorsView(
+    TestDistributionMirrorsViewMixin, TestCaseWithFactory):
+
+    view = "+disabledmirrors"
+
+    def configureMirror(self, mirror):
+        mirror.enabled = False
+        mirror.status = MirrorStatus.OFFICIAL
+
+
+class TestDistributionUnofficialMirrorsView(
+    TestDistributionMirrorsViewMixin, TestCaseWithFactory):
+
+    view = "+unofficialmirrors"
+
+    def configureMirror(self, mirror):
+        mirror.status = MirrorStatus.UNOFFICIAL
+
+
+class TestDistributionPendingReviewMirrorsView(
+    TestDistributionMirrorsViewMixin, TestCaseWithFactory):
+
+    view = "+pendingreviewmirrors"
+
+    def configureMirror(self, mirror):
+        mirror.status = MirrorStatus.PENDING_REVIEW

=== modified file 'lib/lp/registry/model/distribution.py'
--- lib/lp/registry/model/distribution.py	2015-02-13 10:43:03 +0000
+++ lib/lp/registry/model/distribution.py	2015-02-18 18:49:05 +0000
@@ -9,6 +9,7 @@
     'DistributionSet',
     ]
 
+from collections import defaultdict
 import itertools
 from operator import itemgetter
 
@@ -113,6 +114,7 @@
 from lp.registry.model.announcement import MakesAnnouncements
 from lp.registry.model.distributionmirror import (
     DistributionMirror,
+    MirrorCDImageDistroSeries,
     MirrorDistroArchSeries,
     MirrorDistroSeriesSource,
     )
@@ -130,6 +132,7 @@
 from lp.registry.model.oopsreferences import referenced_oops
 from lp.registry.model.pillar import HasAliasMixin
 from lp.registry.model.sourcepackagename import SourcePackageName
+from lp.services.database.bulk import load_referencing
 from lp.services.database.constants import UTC_NOW
 from lp.services.database.datetimecol import UtcDateTimeCol
 from lp.services.database.decoratedresultset import DecoratedResultSet
@@ -422,17 +425,21 @@
         else:
             return [archive.id]
 
-    def _getActiveMirrors(self, mirror_content_type,
-            by_country=False, needs_fresh=False):
+    def _getMirrors(self, content=None, enabled=True,
+                    status=MirrorStatus.OFFICIAL, by_country=False,
+                    needs_fresh=False, needs_cdimage_series=False):
         """Builds the query to get the mirror data for various purposes."""
-        mirrors = list(Store.of(self).find(
-            DistributionMirror,
-            And(
-                DistributionMirror.distribution == self.id,
-                DistributionMirror.content == mirror_content_type,
-                DistributionMirror.enabled == True,
-                DistributionMirror.status == MirrorStatus.OFFICIAL,
-                DistributionMirror.official_candidate == True)))
+        clauses = [
+            DistributionMirror.distribution == self.id,
+            DistributionMirror.status == status,
+            ]
+        if content is not None:
+            clauses.append(DistributionMirror.content == content)
+        if enabled is not None:
+            clauses.append(DistributionMirror.enabled == enabled)
+        if status != MirrorStatus.UNOFFICIAL:
+            clauses.append(DistributionMirror.official_candidate == True)
+        mirrors = list(Store.of(self).find(DistributionMirror, And(*clauses)))
 
         if by_country and mirrors:
             # Since country data is needed, fetch countries into the cache.
@@ -472,59 +479,71 @@
                     mirror.id, None)
                 cache.source_mirror_freshness = source_mirror_freshness.get(
                     mirror.id, None)
+
+        if needs_cdimage_series and mirrors:
+            all_cdimage_series = load_referencing(
+                MirrorCDImageDistroSeries, mirrors, ["distribution_mirrorID"])
+            cdimage_series = defaultdict(list)
+            for series in all_cdimage_series:
+                cdimage_series[series.distribution_mirrorID].append(series)
+            for mirror in mirrors:
+                cache = get_property_cache(mirror)
+                cache.cdimage_series = cdimage_series.get(mirror.id, [])
+
         return mirrors
 
     @property
     def archive_mirrors(self):
         """See `IDistribution`."""
-        return self._getActiveMirrors(MirrorContent.ARCHIVE)
+        return self._getMirrors(content=MirrorContent.ARCHIVE)
 
     @property
     def archive_mirrors_by_country(self):
         """See `IDistribution`."""
-        return self._getActiveMirrors(
-            MirrorContent.ARCHIVE,
+        return self._getMirrors(
+            content=MirrorContent.ARCHIVE,
             by_country=True,
             needs_fresh=True)
 
     @property
     def cdimage_mirrors(self, by_country=False):
         """See `IDistribution`."""
-        return self._getActiveMirrors(MirrorContent.RELEASE)
+        return self._getMirrors(
+            content=MirrorContent.RELEASE,
+            needs_cdimage_series=True)
 
     @property
     def cdimage_mirrors_by_country(self):
         """See `IDistribution`."""
-        return self._getActiveMirrors(
-            MirrorContent.RELEASE,
-            by_country=True)
+        return self._getMirrors(
+            content=MirrorContent.RELEASE,
+            by_country=True,
+            needs_cdimage_series=True)
 
     @property
     def disabled_mirrors(self):
         """See `IDistribution`."""
-        return Store.of(self).find(
-            DistributionMirror,
-            distribution=self,
+        return self._getMirrors(
             enabled=False,
-            status=MirrorStatus.OFFICIAL,
-            official_candidate=True)
+            by_country=True,
+            needs_fresh=True)
 
     @property
     def unofficial_mirrors(self):
         """See `IDistribution`."""
-        return Store.of(self).find(
-            DistributionMirror,
-            distribution=self,
-            status=MirrorStatus.UNOFFICIAL)
+        return self._getMirrors(
+            enabled=None,
+            status=MirrorStatus.UNOFFICIAL,
+            by_country=True,
+            needs_fresh=True)
 
     @property
     def pending_review_mirrors(self):
         """See `IDistribution`."""
-        return Store.of(self).find(
-            DistributionMirror,
-            distribution=self,
-            status=MirrorStatus.PENDING_REVIEW,
-            official_candidate=True)
+        return self._getMirrors(
+            enabled=None,
+            by_country=True,
+            status=MirrorStatus.PENDING_REVIEW)
 
     @property
     def drivers(self):

=== modified file 'lib/lp/registry/model/distributionmirror.py'
--- lib/lp/registry/model/distributionmirror.py	2013-06-20 05:50:00 +0000
+++ lib/lp/registry/model/distributionmirror.py	2015-02-18 18:49:05 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Module docstring goes here."""
@@ -82,7 +82,10 @@
     format_address,
     simple_sendmail,
     )
-from lp.services.propertycache import cachedproperty
+from lp.services.propertycache import (
+    cachedproperty,
+    get_property_cache,
+    )
 from lp.services.webapp import (
     canonical_url,
     urlappend,
@@ -339,7 +342,7 @@
                     'For series mirrors we need to know the '
                     'expected_file_count in order to tell if it should '
                     'be disabled or not.')
-            if expected_file_count > self.cdimage_series.count():
+            if expected_file_count > len(self.cdimage_series):
                 return True
         else:
             if not (self.source_series or self.arch_series):
@@ -458,6 +461,7 @@
             mirror = MirrorCDImageDistroSeries(
                 distribution_mirror=self, distroseries=distroseries,
                 flavour=flavour)
+        del get_property_cache(self).cdimage_series
         return mirror
 
     def deleteMirrorCDImageSeries(self, distroseries, flavour):
@@ -467,21 +471,24 @@
             flavour=flavour)
         if mirror is not None:
             mirror.destroySelf()
+        del get_property_cache(self).cdimage_series
 
     def deleteAllMirrorCDImageSeries(self):
         """See IDistributionMirror"""
         for mirror in self.cdimage_series:
             mirror.destroySelf()
+        del get_property_cache(self).cdimage_series
 
     @property
     def arch_series(self):
         """See IDistributionMirror"""
         return MirrorDistroArchSeries.selectBy(distribution_mirror=self)
 
-    @property
+    @cachedproperty
     def cdimage_series(self):
         """See IDistributionMirror"""
-        return MirrorCDImageDistroSeries.selectBy(distribution_mirror=self)
+        return list(
+            MirrorCDImageDistroSeries.selectBy(distribution_mirror=self))
 
     @property
     def source_series(self):

=== modified file 'lib/lp/registry/templates/distributionmirror-macros.pt'
--- lib/lp/registry/templates/distributionmirror-macros.pt	2011-07-01 10:33:17 +0000
+++ lib/lp/registry/templates/distributionmirror-macros.pt	2015-02-18 18:49:05 +0000
@@ -46,10 +46,11 @@
         <td tal:condition="show_mirror_type">
           <span tal:replace="mirror/content/title" />
         </td>
-        <td tal:condition="show_freshness"
-            tal:define="freshness mirror/getOverallFreshness">
-          <span tal:content="freshness/title"
-            tal:attributes="class string:distromirrorstatus${freshness/name}" />
+        <td tal:condition="show_freshness">
+          <tal:freshness define="freshness mirror/getOverallFreshness">
+            <span tal:content="freshness/title"
+              tal:attributes="class string:distromirrorstatus${freshness/name}" />
+          </tal:freshness>
         </td>
       </tr>
 

=== modified file 'lib/lp/registry/tests/test_distributionmirror.py'
--- lib/lp/registry/tests/test_distributionmirror.py	2013-03-12 05:51:28 +0000
+++ lib/lp/registry/tests/test_distributionmirror.py	2015-02-18 18:49:05 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -77,11 +77,9 @@
             self.hoary, flavour='ubuntu')
         self.cdimage_mirror.ensureMirrorCDImageSeries(
             self.hoary, flavour='edubuntu')
-        self.failUnlessEqual(
-            self.cdimage_mirror.cdimage_series.count(), 2)
+        self.assertEqual(2, len(self.cdimage_mirror.cdimage_series))
         self.cdimage_mirror.deleteAllMirrorCDImageSeries()
-        self.failUnlessEqual(
-            self.cdimage_mirror.cdimage_series.count(), 0)
+        self.assertEqual(0, len(self.cdimage_mirror.cdimage_series))
 
     def test_archive_mirror_without_content_freshness(self):
         self.failIf(self.archive_mirror.source_series or

=== modified file 'lib/lp/registry/tests/test_distributionmirror_prober.py'
--- lib/lp/registry/tests/test_distributionmirror_prober.py	2013-05-02 00:40:14 +0000
+++ lib/lp/registry/tests/test_distributionmirror_prober.py	2015-02-18 18:49:05 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """distributionmirror-prober tests."""
@@ -839,11 +839,11 @@
 
     def test_MirrorCDImageSeries_records_are_deleted_before_probing(self):
         mirror = DistributionMirror.byName('releases-mirror2')
-        self.failUnless(not mirror.cdimage_series.is_empty())
+        self.assertNotEqual(0, len(mirror.cdimage_series))
         # Note that calling this function won't actually probe any mirrors; we
         # need to call reactor.run() to actually start the probing.
         probe_cdimage_mirror(mirror, StringIO(), [], logging)
-        self.failUnless(mirror.cdimage_series.is_empty())
+        self.assertEqual(0, len(mirror.cdimage_series))
 
     def test_archive_mirror_probe_function(self):
         mirror1 = DistributionMirror.byName('archive-mirror')


Follow ups