← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~wgrant/launchpad/bugtaskflat-search-2 into lp:launchpad

 

William Grant has proposed merging lp:~wgrant/launchpad/bugtaskflat-search-2 into lp:launchpad with lp:~wgrant/launchpad/bugtaskflat-search-1 as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~wgrant/launchpad/bugtaskflat-search-2/+merge/102421

Due to the perilous performance characteristics of bug search, we want to be able to switch BugTaskFlat usage on or off quickly with a feature flag. This entails *temporarily* making lp.bugs.model.bugtasksearch a hybrid creature, able to operate on either.

This branch introduces the first parts of the actual hybridisation: a feature flag is introduced which causes bugtasksearch to base itself on BugTaskFlat instead. Filtering and sorting is still done entirely on Bug and BugTask, so a few ugly implicit joins are required at this stage, and it will be far too slow for production.

There is a bit of ugliness in search_bugs to make the underlying queries simpler. When using BugTaskFlat to retrieve BugTasks, we actually query out BugTaskFlat.bugtask_id and use DecoratedResultSet to map them to BugTasks efficiently when iterated. This implies some further ugliness for compatibility with the eager-loading pre_iter_hook that the caller can provide, since it expects BugTasks rather than IDs.

test_bugtask_search now runs both flat and non-flat variants of the tests, which means that this already very slow test module will be even slower. But it's only for the short term.
-- 
https://code.launchpad.net/~wgrant/launchpad/bugtaskflat-search-2/+merge/102421
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wgrant/launchpad/bugtaskflat-search-2 into lp:launchpad.
=== added file 'lib/lp/bugs/model/bugtaskflat.py'
--- lib/lp/bugs/model/bugtaskflat.py	1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/model/bugtaskflat.py	2012-04-18 02:18:22 +0000
@@ -0,0 +1,38 @@
+from storm.locals import (
+    Bool,
+    DateTime,
+    Int,
+    )
+
+from lp.registry.enums import InformationType
+from lp.services.database.enumcol import EnumCol
+from lp.bugs.interfaces.bugtask import (
+    BugTaskImportance,
+    BugTaskStatus,
+    BugTaskStatusSearch,
+    )
+
+
+class BugTaskFlat(object):
+
+    __storm_table__ = 'BugTaskFlat'
+
+    bugtask_id = Int(name='bugtask', primary=True)
+    bug_id = Int(name='bug')
+    datecreated = DateTime()
+    duplicateof_id = Int(name='duplicateof')
+    bug_owner_id = Int(name='bug_owner')
+    information_type = EnumCol(enum=InformationType)
+    date_last_updated = DateTime()
+    heat = Int()
+    product_id = Int(name='product')
+    productseries_id = Int(name='productseries')
+    distribution_id = Int(name='distribution')
+    distroseries_id = Int(name='distroseries')
+    sourcepackagename_id = Int(name='sourcepackagename')
+    status = EnumCol(schema=(BugTaskStatus, BugTaskStatusSearch))
+    importance = EnumCol(schema=BugTaskImportance)
+    assignee_id = Int(name='assignee')
+    milestone_id = Int(name='milestone')
+    owner_id = Int(name='owner')
+    active = Bool()

=== modified file 'lib/lp/bugs/model/bugtasksearch.py'
--- lib/lp/bugs/model/bugtasksearch.py	2012-04-18 02:18:22 +0000
+++ lib/lp/bugs/model/bugtasksearch.py	2012-04-18 02:18:22 +0000
@@ -60,6 +60,7 @@
 from lp.bugs.model.bugnomination import BugNomination
 from lp.bugs.model.bugsubscription import BugSubscription
 from lp.bugs.model.bugtask import BugTask
+from lp.bugs.model.bugtaskflat import BugTaskFlat
 from lp.bugs.model.structuralsubscription import StructuralSubscription
 from lp.registry.interfaces.distribution import IDistribution
 from lp.registry.interfaces.distroseries import IDistroSeries
@@ -71,6 +72,7 @@
 from lp.registry.model.milestonetag import MilestoneTag
 from lp.registry.model.person import Person
 from lp.registry.model.product import Product
+from lp.services.database.bulk import load
 from lp.services.database.decoratedresultset import DecoratedResultSet
 from lp.services.database.lpstorm import IStore
 from lp.services.database.sqlbase import (
@@ -81,6 +83,7 @@
     Array,
     NullCount,
     )
+from lp.services.features import getFeatureFlag
 from lp.services.propertycache import get_property_cache
 from lp.services.searchbuilder import (
     all,
@@ -241,39 +244,66 @@
         respected.
     :param just_bug_ids: Return a ResultSet of bug IDs instead of BugTasks.
     """
+    use_flat = bool(getFeatureFlag('bugs.bugtaskflat.search.enabled'))
+
     store = IStore(BugTask)
-    orderby_expression, orderby_joins = _process_order_by(alternatives[0])
+    orderby_expression, orderby_joins = _process_order_by(
+        alternatives[0], use_flat)
     decorators = []
 
-    want = BugTask.bugID if just_bug_ids else BugTask
+    # If we are to use BugTaskFlat, we just return the ID. The
+    # DecoratedResultSet will turn it into the actual BugTask.
+    # If we're not using BugTaskFlat yet, we should still return
+    # the BugTask directly.
+    if use_flat:
+        start = BugTaskFlat
+        if just_bug_ids:
+            want = BugTaskFlat.bug_id
+        else:
+            want = BugTaskFlat.bugtask_id
+            decorators.append(lambda id: IStore(BugTask).get(BugTask, id))
+            orig_pre_iter_hook = pre_iter_hook
+
+            def pre_iter_hook(rows):
+                rows = load(BugTask, rows)
+                if orig_pre_iter_hook:
+                    orig_pre_iter_hook(rows)
+    else:
+        start = BugTask
+        want = BugTask.bugID if just_bug_ids else BugTask
 
     if len(alternatives) == 1:
         [query, clauseTables, bugtask_decorator, join_tables,
-         has_duplicate_results, with_clause] = _build_query(alternatives[0])
+         has_duplicate_results, with_clause] = _build_query(
+             alternatives[0], use_flat)
         if with_clause:
             store = store.with_(with_clause)
         decorators.append(bugtask_decorator)
 
         if has_duplicate_results:
-            origin = _build_origin(join_tables, clauseTables)
-            outer_origin = _build_origin(orderby_joins, [])
-            subquery = Select(BugTask.id, where=query, tables=origin)
+            origin = _build_origin(join_tables, clauseTables, start)
+            outer_origin = _build_origin(orderby_joins, [], start)
+            want_inner = BugTaskFlat.bugtask_id if use_flat else BugTask.id
+            subquery = Select(want_inner, where=query, tables=origin)
             result = store.using(*outer_origin).find(
-                want, In(BugTask.id, subquery))
+                want, In(want_inner, subquery))
         else:
-            origin = _build_origin(join_tables + orderby_joins, clauseTables)
+            origin = _build_origin(
+                join_tables + orderby_joins, clauseTables, start)
             result = store.using(*origin).find(want, query)
     else:
         results = []
 
         for params in alternatives:
             [query, clauseTables, decorator, join_tables,
-             has_duplicate_results, with_clause] = _build_query(params)
-            origin = _build_origin(join_tables, clauseTables)
+             has_duplicate_results, with_clause] = _build_query(
+                 params, use_flat)
+            origin = _build_origin(join_tables, clauseTables, start)
             localstore = store
             if with_clause:
                 localstore = store.with_(with_clause)
-            next_result = localstore.using(*origin).find((BugTask,), query)
+            want_inner = BugTaskFlat if use_flat else BugTask
+            next_result = localstore.using(*origin).find((want_inner,), query)
             results.append(next_result)
             # NB: assumes the decorators are all compatible.
             # This may need revisiting if e.g. searches on behalf of different
@@ -283,7 +313,9 @@
         resultset = reduce(lambda l, r: l.union(r), results)
         origin = _build_origin(
             orderby_joins, [],
-            start_with=Alias(resultset._get_select(), "BugTask"))
+            Alias(
+                resultset._get_select(),
+                "BugTaskFlat" if use_flat else "BugTask"))
         result = store.using(*origin).find(want)
 
     result.order_by(orderby_expression)
@@ -293,7 +325,7 @@
         pre_iter_hook=pre_iter_hook)
 
 
-def _build_origin(join_tables, clauseTables, start_with=BugTask):
+def _build_origin(join_tables, clauseTables, start_with):
     """Build the parameter list for Store.using().
 
     :param join_tables: A sequence of tables that should be joined
@@ -321,7 +353,7 @@
     return origin
 
 
-def _build_query(params):
+def _build_query(params, use_flat):
     """Build and return an SQL query with the given parameters.
 
     Also return the clauseTables and orderBy for the generated query.
@@ -331,9 +363,18 @@
     """
     params = _require_params(params)
 
-    extra_clauses = [Bug.id == BugTask.bugID]
-    clauseTables = [BugTask, Bug]
-    join_tables = []
+    if use_flat:
+        extra_clauses = []
+        clauseTables = []
+        join_tables = [
+            (Bug, Join(Bug, Bug.id == BugTaskFlat.bug_id)),
+            (BugTask, Join(BugTask, BugTask.id == BugTaskFlat.bugtask_id)),
+            ]
+    else:
+        extra_clauses = [Bug.id == BugTask.bugID]
+        clauseTables = [BugTask, Bug]
+        join_tables = []
+
     decorators = []
     has_duplicate_results = False
     with_clauses = []
@@ -759,7 +800,7 @@
         has_duplicate_results, with_clause)
 
 
-def _process_order_by(params):
+def _process_order_by(params, use_flat):
     """Process the orderby parameter supplied to search().
 
     This method ensures the sort order will be stable, and converting
@@ -802,7 +843,13 @@
 
     # Translate orderby keys into corresponding Table.attribute
     # strings.
-    extra_joins = []
+    if use_flat:
+        extra_joins = [
+            (Bug, Join(Bug, Bug.id == BugTaskFlat.bug_id)),
+            (BugTask, Join(BugTask, BugTask.id == BugTaskFlat.bugtask_id)),
+            ]
+    else:
+        extra_joins = []
     ambiguous = True
     # Sorting by milestone only is a very "coarse" sort order.
     # If no additional sort order is specified, add the bug task

=== modified file 'lib/lp/bugs/tests/test_bugtask_search.py'
--- lib/lp/bugs/tests/test_bugtask_search.py	2012-04-17 08:01:35 +0000
+++ lib/lp/bugs/tests/test_bugtask_search.py	2012-04-18 02:18:22 +0000
@@ -31,6 +31,7 @@
 from lp.registry.interfaces.person import IPersonSet
 from lp.registry.interfaces.product import IProduct
 from lp.registry.interfaces.sourcepackage import ISourcePackage
+from lp.services.features.testing import FeatureFixture
 from lp.services.searchbuilder import (
     all,
     any,
@@ -1528,6 +1529,15 @@
         return [bugtask.bug.id for bugtask in expected_bugtasks]
 
 
+class UsingFlat:
+    """Use BugTaskFlat for searching."""
+
+    def setUp(self):
+        super(UsingFlat, self).setUp()
+        self.useFixture(
+            FeatureFixture({'bugs.bugtaskflat.search.enabled': 'on'}))
+
+
 class TestMilestoneDueDateFiltering(TestCaseWithFactory):
 
     layer = LaunchpadFunctionalLayer
@@ -1563,17 +1573,21 @@
     for bug_target_search_type_class in (
         PreloadBugtaskTargets, NoPreloadBugtaskTargets, QueryBugIDs):
         for target_mixin in bug_targets_mixins:
-            class_name = 'Test%s%s' % (
-                bug_target_search_type_class.__name__,
-                target_mixin.__name__)
-            class_bases = (
-                target_mixin, bug_target_search_type_class,
-                SearchTestBase, TestCaseWithFactory)
-            # Dynamically build a test class from the target mixin class,
-            # from the search type mixin class, from the mixin class
-            # having all tests and from a unit test base class.
-            test_class = type(class_name, class_bases, {})
-            # Add the new unit test class to the suite.
-            suite.addTest(loader.loadTestsFromTestCase(test_class))
+            for feature_mixin in (None, UsingFlat):
+                class_name = 'Test%s%s%s' % (
+                    bug_target_search_type_class.__name__,
+                    target_mixin.__name__,
+                    feature_mixin.__name__ if feature_mixin else '')
+                mixins = [target_mixin, bug_target_search_type_class]
+                if feature_mixin:
+                    mixins.append(feature_mixin)
+                class_bases = (
+                    tuple(mixins) + (SearchTestBase, TestCaseWithFactory))
+                # Dynamically build a test class from the target mixin class,
+                # from the search type mixin class, from the mixin class
+                # having all tests and from a unit test base class.
+                test_class = type(class_name, class_bases, {})
+                # Add the new unit test class to the suite.
+                suite.addTest(loader.loadTestsFromTestCase(test_class))
     suite.addTest(loader.loadTestsFromName(__name__))
     return suite

=== modified file 'lib/lp/services/features/flags.py'
--- lib/lp/services/features/flags.py	2012-04-11 14:34:28 +0000
+++ lib/lp/services/features/flags.py	2012-04-18 02:18:22 +0000
@@ -63,6 +63,12 @@
      '',
      '',
      'https://bugs.launchpad.net/launchpad/+bug/678090'),
+    ('bugs.bugtaskflat.search.enabled',
+     'boolean',
+     'Use BugTaskFlat for bug searches.',
+     '',
+     '',
+     ''),
     ('bugs.bugtracker_components.enabled',
      'boolean',
      ('Enables the display of bugtracker components.'),