← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~lifeless/launchpad/milestones into lp:launchpad/devel

 

Robert Collins has proposed merging lp:~lifeless/launchpad/milestones into lp:launchpad/devel.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)


Hopefully fix the current instantiation of https://bugs.edge.launchpad.net/launchpad-registry/+bug/447418.

Private bug access checking is rather slow: https://bugs.edge.launchpad.net/malone/+bug/619039.

Solution - a cached attribute recording people that we've calculated should have view access, prepopulated by BugTaskSet.search().

Added tests for the low level behaviour and that it fixes the observed scaling problem in a simple milestone view.

\o/
-- 
https://code.launchpad.net/~lifeless/launchpad/milestones/+merge/32855
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~lifeless/launchpad/milestones into lp:launchpad/devel.
=== modified file 'lib/canonical/launchpad/security.py'
--- lib/canonical/launchpad/security.py	2010-08-13 01:43:26 +0000
+++ lib/canonical/launchpad/security.py	2010-08-17 09:41:19 +0000
@@ -920,24 +920,8 @@
     usedfor = IHasBug
 
     def checkAuthenticated(self, user):
-
-        if user.in_admin:
-            # Admins can always edit bugtasks, whether they're reported on a
-            # private bug or not.
-            return True
-
-        if not self.obj.bug.private:
-            # This is a public bug, so anyone can edit it.
-            return True
-        else:
-            # This is a private bug, and we know the user isn't an admin, so
-            # we'll only allow editing if the user is explicitly subscribed to
-            # this bug.
-            for subscription in self.obj.bug.subscriptions:
-                if user.inTeam(subscription.person):
-                    return True
-
-            return False
+        # Delegated entirely to the bug.
+        return self.obj.bug.userCanView(user)
 
 
 class PublicToAllOrPrivateToExplicitSubscribersForBugTask(AuthorizationBase):
@@ -959,21 +943,10 @@
 
     def checkAuthenticated(self, user):
         """Allow any logged in user to edit a public bug, and only
-        explicit subscribers to edit private bugs.
+        explicit subscribers to edit private bugs. Any bug that can be seen can
+        be edited.
         """
-        if not self.obj.private:
-            # This is a public bug.
-            return True
-        elif user.in_admin:
-            # Admins can edit all bugs.
-            return True
-        else:
-            # This is a private bug. Only explicit subscribers may edit it.
-            for subscription in self.obj.subscriptions:
-                if user.inTeam(subscription.person):
-                    return True
-
-        return False
+        return self.obj.userCanView(user)
 
     def checkUnauthenticated(self):
         """Never allow unauthenticated users to edit a bug."""

=== modified file 'lib/canonical/launchpad/utilities/personroles.py'
--- lib/canonical/launchpad/utilities/personroles.py	2010-01-14 11:28:51 +0000
+++ lib/canonical/launchpad/utilities/personroles.py	2010-08-17 09:41:19 +0000
@@ -35,6 +35,10 @@
         except AttributeError:
             raise AttributeError(errortext)
 
+    @property
+    def id(self):
+        return self.person.id
+
     def isOwner(self, obj):
         """See IPersonRoles."""
         return self.person.inTeam(obj.owner)

=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py	2010-08-07 14:54:40 +0000
+++ lib/lp/bugs/model/bug.py	2010-08-17 09:41:19 +0000
@@ -42,6 +42,10 @@
     ObjectCreatedEvent, ObjectDeletedEvent, ObjectModifiedEvent)
 from lazr.lifecycle.snapshot import Snapshot
 
+from canonical.cachedproperty import (
+    cachedproperty,
+    clear_property,
+    )
 from canonical.config import config
 from canonical.database.constants import UTC_NOW
 from canonical.database.datetimecol import UtcDateTimeCol
@@ -555,6 +559,7 @@
                 # disabled see the change.
                 store.flush()
                 self.updateHeat()
+                clear_property(self, '_cached_viewers')
                 return
 
     def unsubscribeFromDupes(self, person, unsubscribed_by):
@@ -1547,21 +1552,32 @@
             self, self.messages[comment_number])
         bug_message.visible = visible
 
+    @cachedproperty('_cached_viewers')
+    def _known_viewers(self):
+        """A dict of of known persons able to view this bug."""
+        return set()
+
     def userCanView(self, user):
-        """See `IBug`."""
+        """See `IBug`.
+        
+        Note that Editing is also controlled by this check,
+        because we permit editing of any bug one can see.
+        """
+        if user.id in self._known_viewers:
+            return True
         admins = getUtility(ILaunchpadCelebrities).admin
         if not self.private:
             # This is a public bug.
             return True
-        elif user.inTeam(admins):
+        elif user.in_admin:
             # Admins can view all bugs.
             return True
         else:
             # This is a private bug. Only explicit subscribers may view it.
             for subscription in self.subscriptions:
                 if user.inTeam(subscription.person):
+                    self._known_viewers.add(user.id)
                     return True
-
         return False
 
     def linkHWSubmission(self, submission):

=== modified file 'lib/lp/bugs/model/bugtask.py'
--- lib/lp/bugs/model/bugtask.py	2010-08-03 08:49:19 +0000
+++ lib/lp/bugs/model/bugtask.py	2010-08-17 09:41:19 +0000
@@ -42,7 +42,7 @@
 from lazr.enum import DBItem
 
 from canonical.config import config
-
+from canonical.cachedproperty import cache_property
 from canonical.database.sqlbase import (
     SQLBase, block_implicit_flushes, convert_storm_clause_to_string, cursor,
     quote, quote_like, sqlvalues)
@@ -51,6 +51,7 @@
 from canonical.database.nl_search import nl_phrase_search
 from canonical.database.enumcol import EnumCol
 
+from canonical.launchpad.components.decoratedresultset import DecoratedResultSet
 from canonical.launchpad.interfaces.lpstorm import IStore
 from canonical.launchpad.webapp.interfaces import ILaunchBag
 
@@ -1534,11 +1535,15 @@
         """Build and return an SQL query with the given parameters.
 
         Also return the clauseTables and orderBy for the generated query.
+
+        :return: A query, the tables to query, ordering expression and a
+            decorator to call on each returned row.
         """
         assert isinstance(params, BugTaskSearchParams)
 
         extra_clauses = ['Bug.id = BugTask.bug']
         clauseTables = ['BugTask', 'Bug']
+        decorators = []
 
         # These arguments can be processed in a loop without any other
         # special handling.
@@ -1814,6 +1819,11 @@
         clause = get_bug_privacy_filter(params.user)
         if clause:
             extra_clauses.append(clause)
+            userid = params.user.id
+            def cache_user_can_view_bug(bugtask):
+                cache_property(bugtask.bug, '_cached_viewers', set([userid]))
+                return bugtask
+            decorators.append(cache_user_can_view_bug)
 
         hw_clause = self._buildHardwareRelatedClause(params)
         if hw_clause is not None:
@@ -1842,7 +1852,15 @@
         orderby_arg = self._processOrderBy(params)
 
         query = " AND ".join(extra_clauses)
-        return query, clauseTables, orderby_arg
+
+        if not decorators:
+            decorator = lambda x:x
+        else:
+            def decorator(obj):
+                for decor in decorators:
+                    obj = decor(obj)
+                return obj
+        return query, clauseTables, orderby_arg, decorator
 
     def _buildUpstreamClause(self, params):
         """Return an clause for returning upstream data if the data exists.
@@ -2074,22 +2092,23 @@
         """See `IBugTaskSet`."""
         store_selector = getUtility(IStoreSelector)
         store = store_selector.get(MAIN_STORE, SLAVE_FLAVOR)
-        query, clauseTables, orderby = self.buildQuery(params)
+        query, clauseTables, orderby, decorator = self.buildQuery(params)
         if len(args) == 0:
             # Do normal prejoins, if we don't have to do any UNION
             # queries.  Prejoins don't work well with UNION, and the way
             # we prefetch objects without prejoins cause problems with
             # COUNT(*) queries, which get inefficient.
-            return BugTask.select(
+            resultset = BugTask.select(
                 query, clauseTables=clauseTables, orderBy=orderby,
                 prejoins=['product', 'sourcepackagename'],
                 prejoinClauseTables=['Bug'])
+            return DecoratedResultSet(resultset, result_decorator=decorator)
 
         bugtask_fti = SQL('BugTask.fti')
         result = store.find((BugTask, bugtask_fti), query,
                             AutoTables(SQL("1=1"), clauseTables))
         for arg in args:
-            query, clauseTables, dummy = self.buildQuery(arg)
+            query, clauseTables, dummy, decorator = self.buildQuery(arg)
             result = result.union(
                 store.find((BugTask, bugtask_fti), query,
                            AutoTables(SQL("1=1"), clauseTables)))
@@ -2108,7 +2127,7 @@
             (BugTask, Bug, Product, SourcePackageName))
         bugtasks = SQLObjectResultSet(BugTask, orderBy=orderby,
                                       prepared_result_set=result)
-        return bugtasks
+        return DecoratedResultSet(bugtasks, result_decorator=decorator)
 
     def getAssignedMilestonesFromSearch(self, search_results):
         """See `IBugTaskSet`."""

=== modified file 'lib/lp/bugs/tests/test_bugtask.py'
--- lib/lp/bugs/tests/test_bugtask.py	2010-07-14 14:11:15 +0000
+++ lib/lp/bugs/tests/test_bugtask.py	2010-08-17 09:41:19 +0000
@@ -20,10 +20,14 @@
 
 from lp.bugs.interfaces.bugtarget import IBugTarget
 from lp.bugs.interfaces.bugtask import (
-    BugTaskImportance, BugTaskSearchParams, BugTaskStatus)
+    BugTaskImportance,
+    BugTaskSearchParams,
+    BugTaskStatus,
+    IBugTaskSet,
+    )
 from lp.bugs.model.bugtask import build_tag_search_clause
 from lp.registry.interfaces.distribution import IDistributionSet
-from lp.registry.interfaces.person import IPersonSet
+from lp.registry.interfaces.person import IPerson, IPersonSet
 from lp.testing import (
     ANONYMOUS, TestCase, TestCaseWithFactory, login, login_person, logout,
     normalize_whitespace)
@@ -792,6 +796,28 @@
         result = target.searchTasks(None, modified_since=date)
         self.assertEqual([task2], list(result))
 
+    def test_private_bug_view_permissions_cached(self):
+        """private bugs from a search know the user can see the bugs."""
+        target = self.makeBugTarget()
+        person = self.login()
+        self.factory.makeBug(product=target, private=True, owner=person)
+        self.factory.makeBug(product=target, private=True, owner=person)
+        # Search style and parameters taken from the milestone index view where
+        # the issue was discovered.
+        login_person(person)
+        tasks =target.searchTasks(BugTaskSearchParams(person, omit_dupes=True,
+            orderby=['status', '-importance', 'id']))
+        # We must be finding the bugs.
+        self.assertEqual(2, tasks.count())
+        # Cache in the storm cache the account->person lookup so its not
+        # distorting what we're testing.
+        _ = IPerson(person.account, None)
+        # One query and only one should be issued to get the tasks, bugs and
+        # allow access to getConjoinedMaster attribute - an attribute that
+        # triggers a permission check (nb: id does not trigger such a check)
+        self.assertStatementCount(1,
+            lambda:[task.getConjoinedMaster for task in tasks])
+
 
 def test_suite():
     suite = unittest.TestSuite()

=== modified file 'lib/lp/registry/browser/tests/test_milestone.py'
--- lib/lp/registry/browser/tests/test_milestone.py	2010-08-16 18:39:11 +0000
+++ lib/lp/registry/browser/tests/test_milestone.py	2010-08-17 09:41:19 +0000
@@ -1,19 +1,30 @@
 # Copyright 2010 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
+from __future__ import with_statement
+
 """Test milestone views."""
 
 __metaclass__ = type
 
-import unittest
-
+from testtools.matchers import LessThan
 from zope.component import getUtility
 
+from canonical.launchpad.webapp import canonical_url
 from canonical.testing.layers import DatabaseFunctionalLayer
 from lp.bugs.interfaces.bugtask import IBugTaskSet
-from lp.testing import ANONYMOUS, login_person, login, TestCaseWithFactory
+from lp.testing import (
+    ANONYMOUS,
+    login,
+    login_person,
+    logout,
+    person_logged_in, 
+    TestCaseWithFactory,
+    )
+from lp.testing.matchers import HasQueryCount
+from lp.testing.memcache import MemcacheTestCase
 from lp.testing.views import create_initialized_view
-from lp.testing.memcache import MemcacheTestCase
+from lp.testing._webservice import QueryCollector
 
 
 class TestMilestoneMemcache(MemcacheTestCase):
@@ -105,5 +116,61 @@
         self.assertEqual(0, product.development_focus.all_bugtasks.count())
 
 
-def test_suite():
-    return unittest.TestLoader().loadTestsFromName(__name__)
+class TestMilestoneIndex(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_more_private_bugs_query_count_is_constant(self):
+        product = self.factory.makeProduct()
+        login_person(product.owner)
+        milestone = self.factory.makeMilestone(
+            productseries=product.development_focus)
+        bug1 = self.factory.makeBug(product=product, private=True,
+            owner=product.owner)
+        bug1.bugtasks[0].transitionToMilestone(milestone, product.owner)
+        # We look at the page as someone who is a member of a team and the team
+        # is subscribed to the bugs, so that we don't get trivial shortcuts
+        # avoiding queries : test the worst case.
+        subscribed_team = self.factory.makeTeam()
+        viewer = self.factory.makePerson(password="test")
+        with person_logged_in(subscribed_team.teamowner):
+            subscribed_team.addMember(viewer, subscribed_team.teamowner)
+        bug1.subscribe(subscribed_team, product.owner)
+        bug1_url = canonical_url(bug1)
+        milestone_url = canonical_url(milestone)
+        browser = self.getUserBrowser(user=viewer)
+        # Seed the cookie cache and any other cross-request state we may gain
+        # in future.  See canonical.launchpad.webapp.serssion: _get_secret.
+        browser.open(milestone_url)
+        collector = QueryCollector()
+        collector.register()
+        self.addCleanup(collector.unregister)
+        # This page is rather high in queries : 46 as a baseline. 20100817
+        page_query_limit = 46
+        browser.open(milestone_url)
+        # Check that the test found the bug
+        self.assertTrue(bug1_url in browser.contents)
+        self.assertThat(collector, HasQueryCount(LessThan(page_query_limit)))
+        with_1_private_bug = collector.count
+        with_1_queries = ["%s: %s" % (pos, stmt[3]) for (pos, stmt) in 
+            enumerate(collector.queries)]
+        login_person(product.owner)
+        bug2 = self.factory.makeBug(product=product, private=True,
+            owner=product.owner)
+        bug2.bugtasks[0].transitionToMilestone(milestone, product.owner)
+        bug2.subscribe(subscribed_team, product.owner)
+        bug2_url = canonical_url(bug2)
+        bug3 = self.factory.makeBug(product=product, private=True,
+            owner=product.owner)
+        bug3.bugtasks[0].transitionToMilestone(milestone, product.owner)
+        bug3.subscribe(subscribed_team, product.owner)
+        logout()
+        browser.open(milestone_url)
+        self.assertTrue(bug2_url in browser.contents)
+        self.assertThat(collector, HasQueryCount(LessThan(page_query_limit)))
+        with_3_private_bugs = collector.count
+        with_3_queries = ["%s: %s" % (pos, stmt[3]) for (pos, stmt) in 
+            enumerate(collector.queries)]
+        self.assertEqual(with_1_private_bug, with_3_private_bugs,
+            "different query count: \n%s\n******************\n%s\n" % (
+            '\n'.join(with_1_queries), '\n'.join(with_3_queries)))

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2010-08-16 21:51:28 +0000
+++ lib/lp/testing/factory.py	2010-08-17 09:41:19 +0000
@@ -7,7 +7,7 @@
 
 """Testing infrastructure for the Launchpad application.
 
-This module should not have any actual tests.
+This module should not contain tests (but it should be tested).
 """
 
 __metaclass__ = type


Follow ups