Merge lp:~adeuring/launchpad/bug-739052-8 into lp:launchpad

Proposed by Abel Deuring
Status: Merged
Approved by: Aaron Bentley
Approved revision: no longer in the source branch.
Merged at revision: 13956
Proposed branch: lp:~adeuring/launchpad/bug-739052-8
Merge into: lp:launchpad
Diff against target: 135 lines (+70/-4)
3 files modified
lib/canonical/launchpad/webapp/batching.py (+37/-3)
lib/canonical/launchpad/webapp/tests/test_batching.py (+32/-0)
versions.cfg (+1/-1)
To merge this branch: bzr merge lp:~adeuring/launchpad/bug-739052-8
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+75372@code.launchpad.net

Commit message

[r=abentley][bug=739052][incr] new property StormResultSet.rough_length; bump version number of lazr.batchnavigator

Description of the change

This branch is the stap preparatory step required to use StormRangeFactory. (At least I hope it...)

It increases the version number of lazr.batchnavigator to 1.2.10 (which I released and included in download-cache today.)

It also adds a new property StormRangeFactory.rough_length, which run an "EXPLAIN SELECT ..." query, as a surrogate of a sometimes very expensive "SELECT count(*)..." query.

lazr.batchnavigator uses this property only to display the last number in "6 -> 10 of 123 results", i.e., the exact value is not very important. (We might though consider to change the text to "6 -> 10 of approximately 123 results".)

The tests test_rough_length() and test_rough_length_first_sort_column_desc() would pass too with an assertEqual(5, estimated_length) (5 is the exact length of the result set), but the third test, test_rough_length_decorated_result_set(), is an example that rough_length is indeed not always excat: It return 4, while the real result length is 5.

test: ./bin/test canonical -vvt canonical.launchpad.webapp.tests.test_batching

no lint

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :

In the future, it might make sense to move most of rough_length into a generic function. But this is good for now.

review: Approve
Revision history for this message
Robert Collins (lifeless) wrote :

There is a count estimator function in Launchpad already - was it
unsuitable for use by this?

Revision history for this message
Abel Deuring (adeuring) wrote :

On 14.09.2011 21:50, Robert Collins wrote:
> There is a count estimator function in Launchpad already - was it
> unsuitable for use by this?

Well, I _could_ have tweaked it: It wants a traditional psycopg cursor,
while we have Storm result set.

Aaron already suggested to make a general function from the method -- I
was simply too lazy to do that: proper test setup etc etc.

I am already working a bit too long on the batching stuff, considering
that I naively started with the intent to fix "just" bug 739052 ;) OK,
there were a few interruptions...

And before the Orange squad starts again feature work, I'd like to go
back to the main interruption: the HWDB problems we had recently.

I think we need urgently a much closer "feedback loop" for checkbox (the
HWDB client that submits the reports): We should process the reports
immediately when checkbox submits the data so that the client can notice
any problems immediately.

Sure, not having an general "length estimation function" for Storm
result sets is wart -- but a much smaller one than the HWDB issues...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/webapp/batching.py'
--- lib/canonical/launchpad/webapp/batching.py 2011-08-25 15:13:59 +0000
+++ lib/canonical/launchpad/webapp/batching.py 2011-09-14 15:54:12 +0000
@@ -12,6 +12,7 @@
12import lazr.batchnavigator12import lazr.batchnavigator
13from lazr.batchnavigator.interfaces import IRangeFactory13from lazr.batchnavigator.interfaces import IRangeFactory
14from operator import isSequenceType14from operator import isSequenceType
15import re
15import simplejson16import simplejson
16from storm import Undef17from storm import Undef
17from storm.expr import (18from storm.expr import (
@@ -23,7 +24,10 @@
23 )24 )
24from storm.properties import PropertyColumn25from storm.properties import PropertyColumn
25from storm.zope.interfaces import IResultSet26from storm.zope.interfaces import IResultSet
26from zope.component import adapts27from zope.component import (
28 adapts,
29 getUtility,
30 )
27from zope.interface import implements31from zope.interface import implements
28from zope.interface.common.sequence import IFiniteSequence32from zope.interface.common.sequence import IFiniteSequence
29from zope.security.proxy import (33from zope.security.proxy import (
@@ -33,15 +37,23 @@
33 )37 )
3438
35from canonical.config import config39from canonical.config import config
36from canonical.database.sqlbase import sqlvalues40from canonical.database.sqlbase import (
41 convert_storm_clause_to_string,
42 sqlvalues,
43 )
44
37from canonical.launchpad.components.decoratedresultset import (45from canonical.launchpad.components.decoratedresultset import (
38 DecoratedResultSet,46 DecoratedResultSet,
39 )47 )
40from canonical.launchpad.webapp.interfaces import (48from canonical.launchpad.webapp.interfaces import (
49 IStoreSelector,
50 MAIN_STORE,
51 SLAVE_FLAVOR,
52 StormRangeFactoryError,
41 ITableBatchNavigator,53 ITableBatchNavigator,
42 StormRangeFactoryError,
43 )54 )
44from canonical.launchpad.webapp.publisher import LaunchpadView55from canonical.launchpad.webapp.publisher import LaunchpadView
56from lp.services.propertycache import cachedproperty
4557
4658
47class FiniteSequenceAdapter:59class FiniteSequenceAdapter:
@@ -559,3 +571,25 @@
559 """See `IRangeFactory."""571 """See `IRangeFactory."""
560 sliced = self.resultset[start:end]572 sliced = self.resultset[start:end]
561 return ShadowedList(list(sliced), list(sliced.get_plain_result_set()))573 return ShadowedList(list(sliced), list(sliced.get_plain_result_set()))
574
575 @cachedproperty
576 def rough_length(self):
577 """See `IRangeFactory."""
578 # get_select_expr() requires at least one column as a parameter.
579 # getorderBy() already knows about columns that can appear
580 # in the result set, let's take just the first one.
581 column = self.getOrderBy()[0]
582 if zope_isinstance(column, Desc):
583 column = column.expr
584 select = removeSecurityProxy(self.plain_resultset).get_select_expr(
585 column)
586 explain = 'EXPLAIN ' + convert_storm_clause_to_string(select)
587 store = getUtility(IStoreSelector).get(MAIN_STORE, SLAVE_FLAVOR)
588 result = store.execute(explain)
589 _rows_re = re.compile("rows=(\d+)\swidth=")
590 first_line = result.get_one()[0]
591 match = _rows_re.search(first_line)
592 if match is None:
593 raise RuntimeError(
594 "Unexpected EXPLAIN output %s" % repr(first_line))
595 return int(match.group(1))
562596
=== modified file 'lib/canonical/launchpad/webapp/tests/test_batching.py'
--- lib/canonical/launchpad/webapp/tests/test_batching.py 2011-08-25 15:13:59 +0000
+++ lib/canonical/launchpad/webapp/tests/test_batching.py 2011-09-14 15:54:12 +0000
@@ -12,6 +12,10 @@
12 compile,12 compile,
13 Desc,13 Desc,
14 )14 )
15from testtools.matchers import (
16 LessThan,
17 Not,
18 )
15from zope.security.proxy import isinstance as zope_isinstance19from zope.security.proxy import isinstance as zope_isinstance
1620
17from canonical.launchpad.components.decoratedresultset import (21from canonical.launchpad.components.decoratedresultset import (
@@ -715,3 +719,31 @@
715 self.assertEqual(2, shadow_list[0])719 self.assertEqual(2, shadow_list[0])
716 self.assertEqual(0, shadow_list.shadow_values[-1])720 self.assertEqual(0, shadow_list.shadow_values[-1])
717 self.assertEqual(2, shadow_list.shadow_values[0])721 self.assertEqual(2, shadow_list.shadow_values[0])
722
723 def test_rough_length(self):
724 # StormRangeFactory.rough_length returns an estimate of the
725 # length of the result set.
726 resultset = self.makeStormResultSet()
727 resultset.order_by(Person.id)
728 range_factory = StormRangeFactory(resultset)
729 estimated_length = range_factory.rough_length
730 self.assertThat(estimated_length, LessThan(10))
731 self.assertThat(estimated_length, Not(LessThan(1)))
732
733 def test_rough_length_first_sort_column_desc(self):
734 # StormRangeFactory.rough_length can handle result sets where
735 # the first sort column has descendig order.
736 resultset = self.makeStormResultSet()
737 resultset.order_by(Desc(Person.id))
738 range_factory = StormRangeFactory(resultset)
739 estimated_length = range_factory.rough_length
740 self.assertThat(estimated_length, LessThan(10))
741 self.assertThat(estimated_length, Not(LessThan(1)))
742
743 def test_rough_length_decorated_result_set(self):
744 # StormRangeFactory.rough_length can handle DecoratedResultSets.
745 resultset = self.makeDecoratedStormResultSet()
746 range_factory = StormRangeFactory(resultset)
747 estimated_length = range_factory.rough_length
748 self.assertThat(estimated_length, LessThan(10))
749 self.assertThat(estimated_length, Not(LessThan(1)))
718750
=== modified file 'versions.cfg'
--- versions.cfg 2011-09-14 11:08:33 +0000
+++ versions.cfg 2011-09-14 15:54:12 +0000
@@ -30,7 +30,7 @@
30launchpadlib = 1.9.930launchpadlib = 1.9.9
31lazr.amqp = 0.131lazr.amqp = 0.1
32lazr.authentication = 0.1.132lazr.authentication = 0.1.1
33lazr.batchnavigator = 1.2.933lazr.batchnavigator = 1.2.10
34lazr.config = 1.1.334lazr.config = 1.1.3
35lazr.delegates = 1.2.035lazr.delegates = 1.2.0
36lazr.enum = 1.1.336lazr.enum = 1.1.3