Merge lp:~rvb/launchpad/branches-timeout-bug-827935 into lp:launchpad

Proposed by Raphaël Badin
Status: Merged
Approved by: Raphaël Badin
Approved revision: no longer in the source branch.
Merged at revision: 14225
Proposed branch: lp:~rvb/launchpad/branches-timeout-bug-827935
Merge into: lp:launchpad
Diff against target: 420 lines (+227/-15)
6 files modified
lib/lp/code/browser/branchlisting.py (+83/-4)
lib/lp/code/browser/tests/test_branchlisting.py (+100/-3)
lib/lp/code/interfaces/branchcollection.py (+4/-1)
lib/lp/code/model/branchcollection.py (+10/-6)
lib/lp/code/templates/person-codesummary.pt (+26/-1)
lib/lp/services/features/flags.py (+4/-0)
To merge this branch: bzr merge lp:~rvb/launchpad/branches-timeout-bug-827935
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+80875@code.launchpad.net

Commit message

[r=gmb][bug=827935] Add a new simplified menu branch without the counts.

Description of the change

This branch adds a new "simplified" branch menu. The new menu does not include the number of {owned,registered,subscribed} branches which is the main reason why the page https://code.launchpad.net/~ubuntu-branches times out. The display of the new menu is protected by a feature flag so that we will be able to see (with real data) if the performance boost is worth the simplification of the design.

= Details =

The performance for this page was analysed by lifeless, jtv and myself (https://lp-oops.canonical.com/oops.py/?oopsid=OOPS-2124AO73). Obviously, only the teams/persons with a very large number of owned branches is a problem (~ubuntu-branches has ~300k branches). As usual, most of the time is SQL time (~10s out of a total of ~11s for the whole page rendering).

After trying to see if the individual queries could be improved lifeless suggested this —rather dramatic— approach because it was clear that the performance problems were coming from the count queries used to display the number of {owned,registered,subscribed} branches: one of the count queries is taking ~3s and the others ~1s.

= UI =

The new UI will look like this: http://people.canonical.com/~rvb/new_menu_branches.png where the old menu looks like this http://people.canonical.com/~rvb/prev_menu_branches.png.

On the bright side, this will unify the branch menu looks with the bug menu looks (this is how the bug menu looks atm http://people.canonical.com/~rvb/current_bug_menu.png).

= Testing =

Right now, the display of this new menu is protected by a feature flag: code.simplified_branches_menu.enabled. My goal is to:
a) make sure that the performance boost is worth the change
b) ask the list to see if everyone is okay with the change

Also, I've recoded the testing done in lib/lp/code/stories/branches/xx-person-branches.txt so if we decide that this can land for real (i.e. without the FF), this doctest can be removed.

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

Hi Raphaël,

Nice branch! I've got a few comments but they're all cosmetic really. Otherwise, r=me.

[1]

62 +
63 + if self.simplified_branch_menu:
64 + return (
65 + self.registered_branch_not_empty or
66 + self.owned_branch_not_empty or
67 + self.subscribed_branch_not_empty or
68 + self.active_review_not_empty
69 + )
70 + else:
71 + return (self.owned_branch_count or
72 + self.registered_branch_count or
73 + self.subscribed_branch_count or
74 + self.active_review_count)

Slightly incosistent formatting here: The form you've used from
64-69 (newline after the opening parenthesis) is the correct one, though
the closing paren should go on the last line as you've done on line 74.

[2]

89 + @cachedproperty
90 + def owned_branch_not_empty(self):
91 + """False if the number of branches owned by self.person is zero."""
92 + return not self._getCountCollection().ownedBy(self.person).is_empty()

I get the feeling this should be named "owned_branches_not_empty" since
you're talking about a collection but the property name suggests you're
talking about a single branch.

[3]

94 + @cachedproperty
95 + def subscribed_branch_not_empty(self):

... And by the same token this should be subscribed_branches_not_empty.

[4]

103 + @cachedproperty
104 + def active_review_not_empty(self):

active_reviews_not_empty

[5]

224 + registered_branches_matcher = soupmatchers.HTMLContains(
225 + soupmatchers.Tag(
226 + 'Registered link', 'a', text='Registered branches',
227 + attrs={'href': 'http://launchpad.dev/~barney'
228 + '/+registeredbranches'}))
229 +

By convention, if you're going to declare a class attribute, you should
do it at the top of the class, since otherwise the reader (i.e. your
humble bearded reviewer) gets a bit confused :).

review: Approve (code)
Revision history for this message
Raphaël Badin (rvb) wrote :

> [1]
>
[...]
> Slightly incosistent formatting here: The form you've used from
> 64-69 (newline after the opening parenthesis) is the correct one, though
> the closing paren should go on the last line as you've done on line 74.

Right, I've only written the first part, but I should have harmonized it with the copied code indeed.
Fixed.

> [2]
> [3]
> [4]
> [5]

Right. Done.

> 224 + registered_branches_matcher = soupmatchers.HTMLContains(
> 225 + soupmatchers.Tag(
> 226 + 'Registered link', 'a', text='Registered branches',
> 227 + attrs={'href': 'http://launchpad.dev/~barney'
> 228 + '/+registeredbranches'}))
> 229 +
>
> By convention, if you're going to declare a class attribute, you should
> do it at the top of the class, since otherwise the reader (i.e. your
> humble bearded reviewer) gets a bit confused :).

Okay. I thought it might be a good idea to declare it near where it's used but I suppose it's much better to have a sane default for these.

Thanks for the review Graham!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/code/browser/branchlisting.py'
--- lib/lp/code/browser/branchlisting.py 2011-10-14 02:12:06 +0000
+++ lib/lp/code/browser/branchlisting.py 2011-11-01 14:15:30 +0000
@@ -31,6 +31,7 @@
31 ]31 ]
3232
33from operator import attrgetter33from operator import attrgetter
34import urlparse
3435
35from lazr.delegates import delegates36from lazr.delegates import delegates
36from lazr.enum import (37from lazr.enum import (
@@ -41,7 +42,6 @@
41 Asc,42 Asc,
42 Desc,43 Desc,
43 )44 )
44import urlparse
45from z3c.ptcompat import ViewPageTemplateFile45from z3c.ptcompat import ViewPageTemplateFile
46from zope.component import getUtility46from zope.component import getUtility
47from zope.formlib import form47from zope.formlib import form
@@ -135,6 +135,7 @@
135from lp.registry.interfaces.sourcepackage import ISourcePackageFactory135from lp.registry.interfaces.sourcepackage import ISourcePackageFactory
136from lp.registry.model.sourcepackage import SourcePackage136from lp.registry.model.sourcepackage import SourcePackage
137from lp.services.browser_helpers import get_plural_text137from lp.services.browser_helpers import get_plural_text
138from lp.services.features import getFeatureFlag
138from lp.services.propertycache import cachedproperty139from lp.services.propertycache import cachedproperty
139140
140141
@@ -850,7 +851,9 @@
850 usedfor = IPerson851 usedfor = IPerson
851 facet = 'branches'852 facet = 'branches'
852 links = ['registered', 'owned', 'subscribed', 'addbranch',853 links = ['registered', 'owned', 'subscribed', 'addbranch',
853 'active_reviews', 'mergequeues']854 'active_reviews', 'mergequeues',
855 'simplified_subscribed', 'simplified_registered',
856 'simplified_owned', 'simplified_active_reviews']
854 extra_attributes = [857 extra_attributes = [
855 'active_review_count',858 'active_review_count',
856 'owned_branch_count',859 'owned_branch_count',
@@ -858,6 +861,7 @@
858 'show_summary',861 'show_summary',
859 'subscribed_branch_count',862 'subscribed_branch_count',
860 'mergequeue_count',863 'mergequeue_count',
864 'simplified_branches_menu',
861 ]865 ]
862866
863 def _getCountCollection(self):867 def _getCountCollection(self):
@@ -881,13 +885,88 @@
881 """885 """
882 return self.context886 return self.context
883887
888 @cachedproperty
889 def has_branches(self):
890 """Should the template show the summary view with the links."""
891
884 @property892 @property
885 def show_summary(self):893 def show_summary(self):
886 """Should the template show the summary view with the links."""894 """Should the template show the summary view with the links."""
887 return (self.owned_branch_count or895
896 if self.simplified_branches_menu:
897 return (
898 self.registered_branches_not_empty or
899 self.owned_branches_not_empty or
900 self.subscribed_branches_not_empty or
901 self.active_reviews_not_empty
902 )
903 else:
904 return (
905 self.owned_branch_count or
888 self.registered_branch_count or906 self.registered_branch_count or
889 self.subscribed_branch_count or907 self.subscribed_branch_count or
890 self.active_review_count)908 self.active_review_count
909 )
910
911 @cachedproperty
912 def simplified_branches_menu(self):
913 return getFeatureFlag('code.simplified_branches_menu.enabled')
914
915 @cachedproperty
916 def registered_branches_not_empty(self):
917 """False if the number of branches registered by self.person
918 is zero.
919 """
920 return (
921 not self._getCountCollection().registeredBy(
922 self.person).is_empty())
923
924 @cachedproperty
925 def owned_branches_not_empty(self):
926 """False if the number of branches owned by self.person is zero."""
927 return not self._getCountCollection().ownedBy(self.person).is_empty()
928
929 @cachedproperty
930 def subscribed_branches_not_empty(self):
931 """False if the number of branches subscribed to by self.person
932 is zero.
933 """
934 return (
935 not self._getCountCollection().subscribedBy(
936 self.person).is_empty())
937
938 @cachedproperty
939 def active_reviews_not_empty(self):
940 """Return the number of active reviews for self.person's branches."""
941 active_reviews = PersonActiveReviewsView(self.context, self.request)
942 return not active_reviews.getProposals().is_empty()
943
944 def simplified_owned(self):
945 return Link(
946 canonical_url(self.context, rootsite='code'),
947 'Owned branches',
948 enabled=self.owned_branches_not_empty)
949
950 def simplified_registered(self):
951 person_is_individual = (not self.person.is_team)
952 return Link(
953 '+registeredbranches',
954 'Registered branches',
955 enabled=(
956 person_is_individual and
957 self.registered_branches_not_empty))
958
959 def simplified_subscribed(self):
960 return Link(
961 '+subscribedbranches',
962 'Subscribed branches',
963 enabled=self.subscribed_branches_not_empty)
964
965 def simplified_active_reviews(self):
966 return Link(
967 '+activereviews',
968 'Active reviews',
969 enabled=self.active_reviews_not_empty)
891970
892 @cachedproperty971 @cachedproperty
893 def registered_branch_count(self):972 def registered_branch_count(self):
894973
=== modified file 'lib/lp/code/browser/tests/test_branchlisting.py'
--- lib/lp/code/browser/tests/test_branchlisting.py 2011-10-14 02:12:06 +0000
+++ lib/lp/code/browser/tests/test_branchlisting.py 2011-11-01 14:15:30 +0000
@@ -11,18 +11,22 @@
11import re11import re
1212
13from lazr.uri import URI13from lazr.uri import URI
14import soupmatchers
14from storm.expr import (15from storm.expr import (
15 Asc,16 Asc,
16 Desc,17 Desc,
17 )18 )
19from testtools.matchers import Not
18from zope.component import getUtility20from zope.component import getUtility
1921
20from canonical.launchpad.testing.pages import (22from canonical.launchpad.testing.pages import (
21 extract_text,23 extract_text,
24 find_main_content,
22 find_tag_by_id,25 find_tag_by_id,
23 find_main_content)26 )
24from canonical.launchpad.webapp import canonical_url27from canonical.launchpad.webapp import canonical_url
25from canonical.launchpad.webapp.servers import LaunchpadTestRequest28from canonical.launchpad.webapp.servers import LaunchpadTestRequest
29from canonical.testing import LaunchpadFunctionalLayer
26from canonical.testing.layers import DatabaseFunctionalLayer30from canonical.testing.layers import DatabaseFunctionalLayer
27from lp.code.browser.branchlisting import (31from lp.code.browser.branchlisting import (
28 BranchListingSort,32 BranchListingSort,
@@ -31,7 +35,10 @@
31 PersonProductSubscribedBranchesView,35 PersonProductSubscribedBranchesView,
32 SourcePackageBranchesView,36 SourcePackageBranchesView,
33 )37 )
34from lp.code.enums import BranchVisibilityRule38from lp.code.enums import (
39 BranchMergeProposalStatus,
40 BranchVisibilityRule,
41 )
35from lp.code.model.branch import Branch42from lp.code.model.branch import Branch
36from lp.code.model.seriessourcepackagebranch import (43from lp.code.model.seriessourcepackagebranch import (
37 SeriesSourcePackageBranchSet,44 SeriesSourcePackageBranchSet,
@@ -59,8 +66,8 @@
59 COMMERCIAL_ADMIN_EMAIL,66 COMMERCIAL_ADMIN_EMAIL,
60 )67 )
61from lp.testing.views import (68from lp.testing.views import (
69 create_initialized_view,
62 create_view,70 create_view,
63 create_initialized_view,
64 )71 )
6572
6673
@@ -257,6 +264,96 @@
257 self._test_batch_template(self.barney)264 self._test_batch_template(self.barney)
258265
259266
267SIMPLIFIED_BRANCHES_MENU_FLAG = {
268 'code.simplified_branches_menu.enabled': 'on'}
269
270
271class TestSimplifiedPersonOwnedBranchesView(TestCaseWithFactory):
272
273 layer = LaunchpadFunctionalLayer
274
275 registered_branches_matcher = soupmatchers.HTMLContains(
276 soupmatchers.Tag(
277 'Registered link', 'a', text='Registered branches',
278 attrs={'href': 'http://launchpad.dev/~barney'
279 '/+registeredbranches'}))
280
281 def setUp(self):
282 TestCaseWithFactory.setUp(self)
283 self.user = self.factory.makePerson()
284 self.person = self.factory.makePerson(name='barney')
285 self.team = self.factory.makeTeam(owner=self.person)
286 self.product = self.factory.makeProduct(name='bambam')
287
288 def get_branch_list_page(self, page_name='+branches', user=None):
289 if user is None:
290 user = self.person
291 with FeatureFixture(SIMPLIFIED_BRANCHES_MENU_FLAG):
292 with person_logged_in(self.user):
293 return create_initialized_view(
294 user, page_name, rootsite='code',
295 principal=self.user)()
296
297 def test_branch_list_h1(self):
298 page = self.get_branch_list_page()
299 h1_matcher = soupmatchers.HTMLContains(
300 soupmatchers.Tag(
301 'Title', 'h1', text='Bazaar branches owned by Barney'))
302 self.assertThat(page, h1_matcher)
303
304 def test_branch_list_empty(self):
305 page = self.get_branch_list_page()
306 empty_message_matcher = soupmatchers.HTMLContains(
307 soupmatchers.Tag(
308 'Empty message', 'p',
309 text='There are no branches related to Barney '
310 'in Launchpad today.'))
311 self.assertThat(page, empty_message_matcher)
312 self.assertThat(page, Not(self.registered_branches_matcher))
313
314 def test_branch_list_registered_link(self):
315 self.factory.makeAnyBranch(owner=self.person)
316 page = self.get_branch_list_page()
317 self.assertThat(page, self.registered_branches_matcher)
318
319 def test_branch_list_owned_link(self):
320 owned_branches_matcher = soupmatchers.HTMLContains(
321 soupmatchers.Tag(
322 'Owned link', 'a', text='Owned branches',
323 attrs={'href': 'http://code.launchpad.dev/~barney'}))
324 self.factory.makeAnyBranch(owner=self.person)
325 page = self.get_branch_list_page('+subscribedbranches')
326 self.assertThat(page, owned_branches_matcher)
327
328 def test_branch_list_subscribed_link(self):
329 subscribed_branches_matcher = soupmatchers.HTMLContains(
330 soupmatchers.Tag(
331 'Subscribed link', 'a', text='Subscribed branches',
332 attrs={'href': 'http://launchpad.dev/~barney'
333 '/+subscribedbranches'}))
334 self.factory.makeAnyBranch(owner=self.person)
335 page = self.get_branch_list_page()
336 self.assertThat(page, subscribed_branches_matcher)
337
338 def test_branch_list_activereviews_link(self):
339 active_review_matcher = soupmatchers.HTMLContains(
340 soupmatchers.Tag(
341 'Active reviews link', 'a', text='Active reviews',
342 attrs={'href': 'http://launchpad.dev/~barney'
343 '/+activereviews'}))
344 branch = self.factory.makeAnyBranch(owner=self.person)
345 self.factory.makeBranchMergeProposal(
346 target_branch=branch, registrant=self.person,
347 set_state=BranchMergeProposalStatus.NEEDS_REVIEW)
348 page = self.get_branch_list_page()
349 self.assertThat(page, active_review_matcher)
350
351 def test_branch_list_no_registered_link_team(self):
352 self.factory.makeAnyBranch(owner=self.person)
353 page = self.get_branch_list_page(user=self.team)
354 self.assertThat(page, Not(self.registered_branches_matcher))
355
356
260class TestSourcePackageBranchesView(TestCaseWithFactory):357class TestSourcePackageBranchesView(TestCaseWithFactory):
261358
262 layer = DatabaseFunctionalLayer359 layer = DatabaseFunctionalLayer
263360
=== modified file 'lib/lp/code/interfaces/branchcollection.py'
--- lib/lp/code/interfaces/branchcollection.py 2011-08-25 20:37:02 +0000
+++ lib/lp/code/interfaces/branchcollection.py 2011-11-01 14:15:30 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4# pylint: disable-msg=E0211, E02134# pylint: disable-msg=E0211, E0213
@@ -50,6 +50,9 @@
50 def count():50 def count():
51 """The number of branches in this collection."""51 """The number of branches in this collection."""
5252
53 def is_empty():
54 """Is this collection empty?"""
55
53 def ownerCounts():56 def ownerCounts():
54 """Return the number of different branch owners.57 """Return the number of different branch owners.
5558
5659
=== modified file 'lib/lp/code/model/branchcollection.py'
--- lib/lp/code/model/branchcollection.py 2011-10-09 23:35:43 +0000
+++ lib/lp/code/model/branchcollection.py 2011-11-01 14:15:30 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Implementations of `IBranchCollection`."""4"""Implementations of `IBranchCollection`."""
@@ -33,31 +33,31 @@
33 DecoratedResultSet,33 DecoratedResultSet,
34 )34 )
35from canonical.launchpad.interfaces.lpstorm import IStore35from canonical.launchpad.interfaces.lpstorm import IStore
36from canonical.launchpad.searchbuilder import any
36from canonical.launchpad.webapp.interfaces import (37from canonical.launchpad.webapp.interfaces import (
37 DEFAULT_FLAVOR,38 DEFAULT_FLAVOR,
38 IStoreSelector,39 IStoreSelector,
39 MAIN_STORE,40 MAIN_STORE,
40 )41 )
41from canonical.launchpad.searchbuilder import any
42from canonical.launchpad.webapp.vocabulary import CountableIterator42from canonical.launchpad.webapp.vocabulary import CountableIterator
43from lp.bugs.interfaces.bugtask import (43from lp.bugs.interfaces.bugtask import (
44 BugTaskSearchParams,
44 IBugTaskSet,45 IBugTaskSet,
45 BugTaskSearchParams,
46 )46 )
47from lp.bugs.interfaces.bugtaskfilter import filter_bugtasks_by_context47from lp.bugs.interfaces.bugtaskfilter import filter_bugtasks_by_context
48from lp.bugs.model.bugbranch import BugBranch48from lp.bugs.model.bugbranch import BugBranch
49from lp.bugs.model.bugtask import BugTask49from lp.bugs.model.bugtask import BugTask
50from lp.code.enums import BranchMergeProposalStatus
50from lp.code.interfaces.branch import user_has_special_branch_access51from lp.code.interfaces.branch import user_has_special_branch_access
51from lp.code.interfaces.branchcollection import (52from lp.code.interfaces.branchcollection import (
52 IBranchCollection,53 IBranchCollection,
53 InvalidFilter,54 InvalidFilter,
54 )55 )
56from lp.code.interfaces.branchlookup import IBranchLookup
57from lp.code.interfaces.codehosting import LAUNCHPAD_SERVICES
55from lp.code.interfaces.seriessourcepackagebranch import (58from lp.code.interfaces.seriessourcepackagebranch import (
56 IFindOfficialBranchLinks,59 IFindOfficialBranchLinks,
57 )60 )
58from lp.code.enums import BranchMergeProposalStatus
59from lp.code.interfaces.branchlookup import IBranchLookup
60from lp.code.interfaces.codehosting import LAUNCHPAD_SERVICES
61from lp.code.model.branch import Branch61from lp.code.model.branch import Branch
62from lp.code.model.branchmergeproposal import BranchMergeProposal62from lp.code.model.branchmergeproposal import BranchMergeProposal
63from lp.code.model.branchsubscription import BranchSubscription63from lp.code.model.branchsubscription import BranchSubscription
@@ -132,6 +132,10 @@
132 """See `IBranchCollection`."""132 """See `IBranchCollection`."""
133 return self.getBranches(eager_load=False).count()133 return self.getBranches(eager_load=False).count()
134134
135 def is_empty(self):
136 """See `IBranchCollection`."""
137 return self.getBranches(eager_load=False).is_empty()
138
135 def ownerCounts(self):139 def ownerCounts(self):
136 """See `IBranchCollection`."""140 """See `IBranchCollection`."""
137 is_team = Person.teamowner != None141 is_team = Person.teamowner != None
138142
=== modified file 'lib/lp/code/templates/person-codesummary.pt'
--- lib/lp/code/templates/person-codesummary.pt 2010-11-18 12:05:34 +0000
+++ lib/lp/code/templates/person-codesummary.pt 2011-11-01 14:15:30 +0000
@@ -8,7 +8,7 @@
8 features request/features"8 features request/features"
9 tal:condition="menu/show_summary">9 tal:condition="menu/show_summary">
1010
11 <table>11 <table tal:condition="not: menu/simplified_branches_menu">
12 <tr class="code-links">12 <tr class="code-links">
13 <td class="code-count" tal:content="menu/owned_branch_count">100</td>13 <td class="code-count" tal:content="menu/owned_branch_count">100</td>
14 <td tal:content="structure menu/owned/render"14 <td tal:content="structure menu/owned/render"
@@ -34,4 +34,29 @@
34 />34 />
35 </tr>35 </tr>
36 </table>36 </table>
37
38 <table tal:condition="menu/simplified_branches_menu">
39 <tr class="code-links"
40 tal:condition="menu/simplified_owned/enabled">
41 <td tal:content="structure menu/simplified_owned/render" />
42 </tr>
43 <tr class="code-links"
44 tal:condition="menu/simplified_registered/enabled">
45 <td tal:content="structure menu/simplified_registered/render" />
46 </tr>
47 <tr class="code-links"
48 tal:condition="menu/simplified_subscribed/enabled">
49 <td tal:content="structure menu/simplified_subscribed/render" />
50 </tr>
51 <tr class="code-links"
52 tal:condition="menu/simplified_active_reviews/enabled">
53 <td tal:content="structure menu/simplified_active_reviews/render" />
54 </tr>
55 <tr tal:condition="features/code.branchmergequeue" id="mergequeue-counts">
56 <td class="code-count" tal:content="menu/mergequeue_count">5</td>
57 <td tal:condition="menu"
58 tal:content="structure menu/mergequeues/render"
59 />
60 </tr>
61 </table>
37</div>62</div>
3863
=== modified file 'lib/lp/services/features/flags.py'
--- lib/lp/services/features/flags.py 2011-10-27 01:11:39 +0000
+++ lib/lp/services/features/flags.py 2011-11-01 14:15:30 +0000
@@ -64,6 +64,10 @@
64 'boolean',64 'boolean',
65 'Shows incremental diffs on merge proposals.',65 'Shows incremental diffs on merge proposals.',
66 ''),66 ''),
67 ('code.simplified_branches_menu.enabled',
68 'boolean',
69 ('Display a simplified version of the branch menu (omit the counts).'),
70 ''),
67 ('hard_timeout',71 ('hard_timeout',
68 'float',72 'float',
69 'Sets the hard request timeout in milliseconds.',73 'Sets the hard request timeout in milliseconds.',