launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #09861
lp:~wallyworld/launchpad/sharing-details-page-branch-queries-1021622 into lp:launchpad
Ian Booth has proposed merging lp:~wallyworld/launchpad/sharing-details-page-branch-queries-1021622 into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1021622 in Launchpad itself: "PillarPersonSharingView uses O(n) queries for branch access"
https://bugs.launchpad.net/launchpad/+bug/1021622
For more details, see:
https://code.launchpad.net/~wallyworld/launchpad/sharing-details-page-branch-queries-1021622/+merge/114577
== Implementation ==
The sharing details page was using O(n) queries to load the branches it wanted to display. This is because once the branches were loaded by IBranchCollection.getGranches(), a query was used by the zope security check each time it called branch.visibleByUser().
So I implemented a caching mechanism similar to that used for bugs (I copied the known_viewers property including docstring). There is a known_viewers set, and the getBranches() call populates the _known_viewers.
== Tests ==
Revert the TestProductSharingDetailsView.test_view_query_count() to check for the correct number of queries (12) instead of the temporarily inflated number used to make the test pass.
Add a new test test_getBranches_caches_viewers() test_branchcollection.
== Lint ==
Checking for conflicts and issues in changed files.
Linting changed files:
lib/lp/code/model/branch.py
lib/lp/code/model/branchcollection.py
lib/lp/code/model/tests/test_branchcollection.py
lib/lp/registry/browser/tests/test_pillar_sharing.py
--
https://code.launchpad.net/~wallyworld/launchpad/sharing-details-page-branch-queries-1021622/+merge/114577
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wallyworld/launchpad/sharing-details-page-branch-queries-1021622 into lp:launchpad.
=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py 2012-07-11 09:43:04 +0000
+++ lib/lp/code/model/branch.py 2012-07-12 07:31:24 +0000
@@ -1276,12 +1276,30 @@
job.celeryRunOnCommit()
return job
+ @cachedproperty
+ def _known_viewers(self):
+ """A set of known persons able to view this branch.
+
+ This method must return an empty set or branch searches will trigger
+ late evaluation. Any 'should be set on load' properties must be done by
+ the branch search.
+
+ If you are tempted to change this method, don't. Instead see
+ visibleByUser which defines the just-in-time policy for branch
+ visibility, and IBranchCollection which honours visibility rules.
+ """
+ return set()
+
def visibleByUser(self, user, checked_branches=None):
"""See `IBranch`."""
if checked_branches is None:
checked_branches = []
if self.information_type in PUBLIC_INFORMATION_TYPES:
can_access = True
+ elif user is None:
+ can_access = False
+ elif user.id in self._known_viewers:
+ can_access = True
else:
can_access = not getUtility(IAllBranches).withIds(
self.id).visibleByUser(user).is_empty()
=== modified file 'lib/lp/code/model/branchcollection.py'
--- lib/lp/code/model/branchcollection.py 2012-07-09 10:35:00 +0000
+++ lib/lp/code/model/branchcollection.py 2012-07-12 07:31:24 +0000
@@ -295,8 +295,6 @@
tables = [Branch] + list(all_tables)
expressions = self._getBranchExpressions()
resultset = self.store.using(*tables).find(Branch, *expressions)
- if not eager_load:
- return resultset
def do_eager_load(rows):
branch_ids = set(branch.id for branch in rows)
@@ -309,7 +307,17 @@
load_related(Person, rows,
['ownerID', 'registrantID', 'reviewerID'])
load_referencing(BugBranch, rows, ['branchID'])
- return DecoratedResultSet(resultset, pre_iter_hook=do_eager_load)
+
+ def cache_permission(branch):
+ if self._user:
+ get_property_cache(branch)._known_viewers = (
+ set([self._user.id]))
+ return branch
+
+ eager_load_hook = do_eager_load if eager_load else None
+ return DecoratedResultSet(
+ resultset, pre_iter_hook=eager_load_hook,
+ result_decorator=cache_permission)
def getMergeProposals(self, statuses=None, for_branches=None,
target_branch=None, merged_revnos=None,
=== modified file 'lib/lp/code/model/tests/test_branchcollection.py'
--- lib/lp/code/model/tests/test_branchcollection.py 2012-07-05 04:59:52 +0000
+++ lib/lp/code/model/tests/test_branchcollection.py 2012-07-12 07:31:24 +0000
@@ -9,10 +9,12 @@
import pytz
from storm.store import EmptyResultSet
+from testtools.matchers import Equals
from zope.component import getUtility
from zope.security.proxy import removeSecurityProxy
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
+from lp.app.interfaces.services import IService
from lp.code.enums import (
BranchLifecycleStatus,
BranchMergeProposalStatus,
@@ -41,9 +43,11 @@
from lp.testing import (
person_logged_in,
run_with_login,
+ StormStatementRecorder,
TestCaseWithFactory,
)
from lp.testing.layers import DatabaseFunctionalLayer
+from lp.testing.matchers import HasQueryCount
class TestBranchCollectionAdaptation(TestCaseWithFactory):
@@ -132,6 +136,24 @@
self.store, [Branch.product == branch.product])
self.assertEqual([branch], list(collection.getBranches()))
+ def test_getBranches_caches_viewers(self):
+ # getBranches() caches the user as a known viewer so that
+ # branch.visibleByUser() does not have to hit the database.
+ collection = GenericBranchCollection(self.store)
+ owner = self.factory.makePerson()
+ product = self.factory.makeProduct()
+ branch = self.factory.makeProductBranch(
+ owner=owner,
+ product=product,
+ information_type=InformationType.USERDATA)
+ someone = self.factory.makePerson()
+ getUtility(IService, 'sharing').ensureAccessGrants(
+ [someone], owner, branches=[branch], ignore_permissions=True)
+ [branch] = list(collection.visibleByUser(someone).getBranches())
+ with StormStatementRecorder() as recorder:
+ self.assertTrue(branch.visibleByUser(someone))
+ self.assertThat(recorder, HasQueryCount(Equals(0)))
+
def test_count(self):
# The 'count' property of a collection is the number of elements in
# the collection.
=== modified file 'lib/lp/registry/browser/tests/test_pillar_sharing.py'
--- lib/lp/registry/browser/tests/test_pillar_sharing.py 2012-07-06 07:39:40 +0000
+++ lib/lp/registry/browser/tests/test_pillar_sharing.py 2012-07-12 07:31:24 +0000
@@ -241,7 +241,7 @@
IStore(self.pillar).invalidate()
with StormStatementRecorder() as recorder:
create_initialized_view(pillarperson, '+index')
- self.assertThat(recorder, HasQueryCount(LessThan(26)))
+ self.assertThat(recorder, HasQueryCount(LessThan(12)))
def test_view_write_enabled_without_feature_flag(self):
# Test that sharing_write_enabled is not set without the feature flag.
Follow ups