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
1=== modified file 'lib/canonical/launchpad/webapp/batching.py'
2--- lib/canonical/launchpad/webapp/batching.py 2011-08-25 15:13:59 +0000
3+++ lib/canonical/launchpad/webapp/batching.py 2011-09-14 15:54:12 +0000
4@@ -12,6 +12,7 @@
5 import lazr.batchnavigator
6 from lazr.batchnavigator.interfaces import IRangeFactory
7 from operator import isSequenceType
8+import re
9 import simplejson
10 from storm import Undef
11 from storm.expr import (
12@@ -23,7 +24,10 @@
13 )
14 from storm.properties import PropertyColumn
15 from storm.zope.interfaces import IResultSet
16-from zope.component import adapts
17+from zope.component import (
18+ adapts,
19+ getUtility,
20+ )
21 from zope.interface import implements
22 from zope.interface.common.sequence import IFiniteSequence
23 from zope.security.proxy import (
24@@ -33,15 +37,23 @@
25 )
26
27 from canonical.config import config
28-from canonical.database.sqlbase import sqlvalues
29+from canonical.database.sqlbase import (
30+ convert_storm_clause_to_string,
31+ sqlvalues,
32+ )
33+
34 from canonical.launchpad.components.decoratedresultset import (
35 DecoratedResultSet,
36 )
37 from canonical.launchpad.webapp.interfaces import (
38+ IStoreSelector,
39+ MAIN_STORE,
40+ SLAVE_FLAVOR,
41+ StormRangeFactoryError,
42 ITableBatchNavigator,
43- StormRangeFactoryError,
44 )
45 from canonical.launchpad.webapp.publisher import LaunchpadView
46+from lp.services.propertycache import cachedproperty
47
48
49 class FiniteSequenceAdapter:
50@@ -559,3 +571,25 @@
51 """See `IRangeFactory."""
52 sliced = self.resultset[start:end]
53 return ShadowedList(list(sliced), list(sliced.get_plain_result_set()))
54+
55+ @cachedproperty
56+ def rough_length(self):
57+ """See `IRangeFactory."""
58+ # get_select_expr() requires at least one column as a parameter.
59+ # getorderBy() already knows about columns that can appear
60+ # in the result set, let's take just the first one.
61+ column = self.getOrderBy()[0]
62+ if zope_isinstance(column, Desc):
63+ column = column.expr
64+ select = removeSecurityProxy(self.plain_resultset).get_select_expr(
65+ column)
66+ explain = 'EXPLAIN ' + convert_storm_clause_to_string(select)
67+ store = getUtility(IStoreSelector).get(MAIN_STORE, SLAVE_FLAVOR)
68+ result = store.execute(explain)
69+ _rows_re = re.compile("rows=(\d+)\swidth=")
70+ first_line = result.get_one()[0]
71+ match = _rows_re.search(first_line)
72+ if match is None:
73+ raise RuntimeError(
74+ "Unexpected EXPLAIN output %s" % repr(first_line))
75+ return int(match.group(1))
76
77=== modified file 'lib/canonical/launchpad/webapp/tests/test_batching.py'
78--- lib/canonical/launchpad/webapp/tests/test_batching.py 2011-08-25 15:13:59 +0000
79+++ lib/canonical/launchpad/webapp/tests/test_batching.py 2011-09-14 15:54:12 +0000
80@@ -12,6 +12,10 @@
81 compile,
82 Desc,
83 )
84+from testtools.matchers import (
85+ LessThan,
86+ Not,
87+ )
88 from zope.security.proxy import isinstance as zope_isinstance
89
90 from canonical.launchpad.components.decoratedresultset import (
91@@ -715,3 +719,31 @@
92 self.assertEqual(2, shadow_list[0])
93 self.assertEqual(0, shadow_list.shadow_values[-1])
94 self.assertEqual(2, shadow_list.shadow_values[0])
95+
96+ def test_rough_length(self):
97+ # StormRangeFactory.rough_length returns an estimate of the
98+ # length of the result set.
99+ resultset = self.makeStormResultSet()
100+ resultset.order_by(Person.id)
101+ range_factory = StormRangeFactory(resultset)
102+ estimated_length = range_factory.rough_length
103+ self.assertThat(estimated_length, LessThan(10))
104+ self.assertThat(estimated_length, Not(LessThan(1)))
105+
106+ def test_rough_length_first_sort_column_desc(self):
107+ # StormRangeFactory.rough_length can handle result sets where
108+ # the first sort column has descendig order.
109+ resultset = self.makeStormResultSet()
110+ resultset.order_by(Desc(Person.id))
111+ range_factory = StormRangeFactory(resultset)
112+ estimated_length = range_factory.rough_length
113+ self.assertThat(estimated_length, LessThan(10))
114+ self.assertThat(estimated_length, Not(LessThan(1)))
115+
116+ def test_rough_length_decorated_result_set(self):
117+ # StormRangeFactory.rough_length can handle DecoratedResultSets.
118+ resultset = self.makeDecoratedStormResultSet()
119+ range_factory = StormRangeFactory(resultset)
120+ estimated_length = range_factory.rough_length
121+ self.assertThat(estimated_length, LessThan(10))
122+ self.assertThat(estimated_length, Not(LessThan(1)))
123
124=== modified file 'versions.cfg'
125--- versions.cfg 2011-09-14 11:08:33 +0000
126+++ versions.cfg 2011-09-14 15:54:12 +0000
127@@ -30,7 +30,7 @@
128 launchpadlib = 1.9.9
129 lazr.amqp = 0.1
130 lazr.authentication = 0.1.1
131-lazr.batchnavigator = 1.2.9
132+lazr.batchnavigator = 1.2.10
133 lazr.config = 1.1.3
134 lazr.delegates = 1.2.0
135 lazr.enum = 1.1.3