Merge lp:~lifeless/launchpad/bug-627701 into lp:launchpad

Proposed by Robert Collins
Status: Merged
Approved by: Jonathan Lange
Approved revision: no longer in the source branch.
Merged at revision: 11668
Proposed branch: lp:~lifeless/launchpad/bug-627701
Merge into: lp:launchpad
Diff against target: 285 lines (+125/-20)
5 files modified
lib/canonical/launchpad/doc/timeout.txt (+38/-5)
lib/canonical/launchpad/webapp/adapter.py (+48/-10)
lib/canonical/launchpad/webapp/ftests/test_adapter.py (+19/-1)
lib/canonical/launchpad/webapp/publication.py (+17/-0)
lib/lp/services/features/flags.py (+3/-4)
To merge this branch: bzr merge lp:~lifeless/launchpad/bug-627701
Reviewer Review Type Date Requested Status
Jonathan Lange (community) Approve
Review via email: mp+37094@code.launchpad.net

Commit message

Permit controlling timeouts via feature flags.

Description of the change

Permit controlling timeouts via feature flags - a collaboration between Diogo and I.

To post a comment you must log in.
Revision history for this message
Jonathan Lange (jml) wrote :

 * Line 137 of the diff. If getFeatureFlag raises, then timeout_str is never set and the "if timeout_str" check will fail with a NameError. This leads me to believe the code was not TDDd.

 * The documentation in timeout.txt doesn't make it clear how you would use this feature flag for the purposes stated in the text (e.g. pages for which there are a few specific users). As far as I can tell from the test, it's a global thing. It would be nice to point to some place the reader could learn more.

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

if getFeatureFlag raises, timeout_str is not evaluated. The finally only guards the reentrancy check.

I'll add some more to the prose to point folk at feature flags info.

Revision history for this message
Jonathan Lange (jml) wrote :

I misread the code. Approved to land with the documentation point addressed.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/doc/timeout.txt'
--- lib/canonical/launchpad/doc/timeout.txt 2010-09-13 04:45:59 +0000
+++ lib/canonical/launchpad/doc/timeout.txt 2010-10-03 01:22:47 +0000
@@ -37,20 +37,19 @@
37(Set the request to have started a few seconds in the past.)37(Set the request to have started a few seconds in the past.)
3838
39 >>> import time39 >>> import time
40 >>> from canonical.launchpad.webapp.adapter import set_request_started40 >>> from canonical.launchpad.webapp import adapter
41 >>> now = time.time()41 >>> adapter.set_request_started(time.time()-5)
42 >>> set_request_started(now-5)
4342
44So the computed timeout should be more or less 5 seconds (10-5).43So the computed timeout should be more or less 5 seconds (10-5).
4544
46 >>> timeout_func() - now <= 545 >>> timeout_func() <= 5
47 True46 True
4847
49If the timeout is already expired, a RequestExpired error is raised:48If the timeout is already expired, a RequestExpired error is raised:
5049
51 >>> from canonical.launchpad.webapp.adapter import clear_request_started50 >>> from canonical.launchpad.webapp.adapter import clear_request_started
52 >>> clear_request_started()51 >>> clear_request_started()
53 >>> set_request_started(now-12)52 >>> adapter.set_request_started(time.time()-12)
54 >>> timeout_func()53 >>> timeout_func()
55 Traceback (most recent call last):54 Traceback (most recent call last):
56 ...55 ...
@@ -79,6 +78,40 @@
7978
80 >>> wait_a_little()79 >>> wait_a_little()
8180
81= Overriding hard timeouts via FeatureFlags =
82
83It's possible to use FeatureFlags to control the hard timeout. This is used to
84deal with pages that suddenly start performing badly, which are being optimised
85but should not hold back the overall timeout decrease, or for which there are
86only a few specific users and we are willing to have them run for longer
87periods. For more information on feature flags see lp.services.features.
88
89 >>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
90 >>> from lp.services.features.model import FeatureFlag, getFeatureStore
91 >>> from lp.services.features.webapp import ScopesFromRequest
92 >>> from lp.services.features.flags import FeatureController
93 >>> from lp.services.features import per_thread
94
95Install the feature flag to increase the timeout value.
96
97 >>> config.push('flagstimeout', dedent('''\
98 ... [database]
99 ... db_statement_timeout = 10000'''))
100
101 >>> empty_request = LaunchpadTestRequest()
102 >>> per_thread.features = FeatureController(
103 ... ScopesFromRequest(empty_request).lookup)
104 >>> ignore = getFeatureStore().add(FeatureFlag(
105 ... scope=u'default', flag=u'hard_timeout', value=u'20000',
106 ... priority=1))
107
108Now the request can take 20 seconds to complete.
109
110 >>> clear_request_started()
111 >>> adapter.set_request_started(time.time())
112 >>> adapter.set_permit_timeout_from_features(True)
113 >>> abs(adapter._get_request_timeout()-20000) < 0.001
114 True
82115
83== Clean up ===116== Clean up ===
84117
85118
=== modified file 'lib/canonical/launchpad/webapp/adapter.py'
--- lib/canonical/launchpad/webapp/adapter.py 2010-09-14 04:52:39 +0000
+++ lib/canonical/launchpad/webapp/adapter.py 2010-10-03 01:22:47 +0000
@@ -6,7 +6,7 @@
66
7__metaclass__ = type7__metaclass__ = type
88
9import datetime9import logging
10import os10import os
11import re11import re
12import sys12import sys
@@ -47,7 +47,6 @@
47from canonical.config import (47from canonical.config import (
48 config,48 config,
49 DatabaseConfig,49 DatabaseConfig,
50 dbconfig,
51 )50 )
52from canonical.database.interfaces import IRequestExpired51from canonical.database.interfaces import IRequestExpired
53from canonical.launchpad.interfaces import (52from canonical.launchpad.interfaces import (
@@ -67,6 +66,7 @@
67from canonical.launchpad.webapp.opstats import OpStats66from canonical.launchpad.webapp.opstats import OpStats
68from canonical.lazr.utils import get_current_browser_request, safe_hasattr67from canonical.lazr.utils import get_current_browser_request, safe_hasattr
69from canonical.lazr.timeout import set_default_timeout_function68from canonical.lazr.timeout import set_default_timeout_function
69from lp.services import features
70from lp.services.timeline.timeline import Timeline70from lp.services.timeline.timeline import Timeline
71from lp.services.timeline.requesttimeline import (71from lp.services.timeline.requesttimeline import (
72 get_request_timeline,72 get_request_timeline,
@@ -82,8 +82,6 @@
82 'get_request_start_time',82 'get_request_start_time',
83 'get_request_duration',83 'get_request_duration',
84 'get_store_name',84 'get_store_name',
85 'hard_timeout_expired',
86 'launchpad_default_timeout',
87 'soft_timeout_expired',85 'soft_timeout_expired',
88 'StoreSelector',86 'StoreSelector',
89 ]87 ]
@@ -182,13 +180,16 @@
182 if txn is not None:180 if txn is not None:
183 _local.commit_logger = CommitLogger(txn)181 _local.commit_logger = CommitLogger(txn)
184 txn.registerSynch(_local.commit_logger)182 txn.registerSynch(_local.commit_logger)
183 set_permit_timeout_from_features(False)
184
185185
186def clear_request_started():186def clear_request_started():
187 """Clear the request timer. This function should be called when187 """Clear the request timer. This function should be called when
188 the request completes.188 the request completes.
189 """189 """
190 if getattr(_local, 'request_start_time', None) is None:190 if getattr(_local, 'request_start_time', None) is None:
191 warnings.warn('clear_request_started() called outside of a request')191 warnings.warn('clear_request_started() called outside of a request',
192 stacklevel=2)
192 _local.request_start_time = None193 _local.request_start_time = None
193 request = get_current_browser_request()194 request = get_current_browser_request()
194 set_request_timeline(request, Timeline())195 set_request_timeline(request, Timeline())
@@ -257,10 +258,50 @@
257 return now - starttime258 return now - starttime
258259
259260
261def set_permit_timeout_from_features(enabled):
262 """Control request timeouts being obtained from the 'hard_timeout' flag.
263
264 Until we've fully setup a page to render - routed the request to the right
265 object, setup a participation etc, feature flags cannot be completely used;
266 and because doing feature flag lookups will trigger DB access, attempting
267 to do a DB lookup will cause a nested DB lookup (the one being done, and
268 the flags lookup). To resolve all of this, timeouts start as a config file
269 only setting, and are then overridden once the request is ready to execute.
270
271 :param enabled: If True permit looking up request timeouts in feature
272 flags.
273 """
274 _local._permit_feature_timeout = enabled
275
276
277def _get_request_timeout(timeout=None):
278 """Get the timeout value in ms for the current request.
279
280 :param timeout: A custom timeout in ms.
281 :return None or a time in ms representing the budget to grant the request.
282 """
283 if not getattr(_local, 'enable_timeout', True):
284 return None
285 if timeout is None:
286 timeout = config.database.db_statement_timeout
287 if getattr(_local, '_permit_feature_timeout', False):
288 set_permit_timeout_from_features(False)
289 try:
290 timeout_str = features.getFeatureFlag('hard_timeout')
291 finally:
292 set_permit_timeout_from_features(True)
293 if timeout_str:
294 try:
295 timeout = float(timeout_str)
296 except ValueError:
297 logging.error('invalid hard timeout flag %r', timeout_str)
298 return timeout
299
300
260def get_request_remaining_seconds(no_exception=False, now=None, timeout=None):301def get_request_remaining_seconds(no_exception=False, now=None, timeout=None):
261 """Return how many seconds are remaining in the current request budget.302 """Return how many seconds are remaining in the current request budget.
262303
263 If timouts are disabled, None is returned. 304 If timeouts are disabled, None is returned.
264305
265 :param no_exception: If True, do not raise an error if the request306 :param no_exception: If True, do not raise an error if the request
266 is out of time. Instead return a float e.g. -2.0 for 2 seconds over307 is out of time. Instead return a float e.g. -2.0 for 2 seconds over
@@ -269,10 +310,7 @@
269 :param timeout: A custom timeout in ms.310 :param timeout: A custom timeout in ms.
270 :return: None or a float representing the remaining time budget.311 :return: None or a float representing the remaining time budget.
271 """312 """
272 if not getattr(_local, 'enable_timeout', True):313 timeout = _get_request_timeout(timeout=timeout)
273 return None
274 if timeout is None:
275 timeout = config.database.db_statement_timeout
276 if not timeout:314 if not timeout:
277 return None315 return None
278 duration = get_request_duration(now)316 duration = get_request_duration(now)
279317
=== modified file 'lib/canonical/launchpad/webapp/ftests/test_adapter.py'
--- lib/canonical/launchpad/webapp/ftests/test_adapter.py 2010-08-20 20:31:18 +0000
+++ lib/canonical/launchpad/webapp/ftests/test_adapter.py 2010-10-03 01:22:47 +0000
@@ -7,11 +7,28 @@
7import unittest7import unittest
88
9from canonical.launchpad.testing.systemdocs import LayeredDocFileSuite9from canonical.launchpad.testing.systemdocs import LayeredDocFileSuite
10from canonical.launchpad.webapp import adapter
10from canonical.testing import LaunchpadFunctionalLayer11from canonical.testing import LaunchpadFunctionalLayer
12from lp.testing import TestCase
13
14
15class TestTimeout(TestCase):
16
17 def test_set_permit_timeout_from_features(self):
18 adapter.set_permit_timeout_from_features(True)
19 self.assertTrue(adapter._local._permit_feature_timeout)
20 adapter.set_permit_timeout_from_features(False)
21 self.assertFalse(adapter._local._permit_feature_timeout)
22
23 def test_set_request_started_disables_flag_timeout(self):
24 adapter.set_request_started()
25 self.addCleanup(adapter.clear_request_started)
26 self.assertFalse(adapter._local._permit_feature_timeout)
1127
1228
13def test_suite():29def test_suite():
14 return unittest.TestSuite([30 suite = unittest.TestLoader().loadTestsFromName(__name__)
31 suite.addTests([
15 LayeredDocFileSuite(32 LayeredDocFileSuite(
16 'test_adapter.txt',33 'test_adapter.txt',
17 layer=LaunchpadFunctionalLayer),34 layer=LaunchpadFunctionalLayer),
@@ -24,3 +41,4 @@
24 'test_adapter_permissions.txt',41 'test_adapter_permissions.txt',
25 layer=LaunchpadFunctionalLayer),42 layer=LaunchpadFunctionalLayer),
26 ])43 ])
44 return suite
2745
=== modified file 'lib/canonical/launchpad/webapp/publication.py'
--- lib/canonical/launchpad/webapp/publication.py 2010-09-13 06:50:12 +0000
+++ lib/canonical/launchpad/webapp/publication.py 2010-10-03 01:22:47 +0000
@@ -83,6 +83,8 @@
83 IPersonSet,83 IPersonSet,
84 ITeam,84 ITeam,
85 )85 )
86from lp.services import features
87from lp.services.features.flags import NullFeatureController
8688
8789
88METHOD_WRAPPER_TYPE = type({}.__setitem__)90METHOD_WRAPPER_TYPE = type({}.__setitem__)
@@ -454,6 +456,21 @@
454 # And spit the pageid out to our tracelog.456 # And spit the pageid out to our tracelog.
455 tracelog(request, 'p', pageid)457 tracelog(request, 'p', pageid)
456458
459 # For opstats, where we really don't want to have any DB access at all,
460 # ensure that all flag lookups will stop early.
461 if pageid in ('RootObject:OpStats', 'RootObject:+opstats'):
462 request.features = NullFeatureController()
463 features.per_thread.features = request.features
464
465 # Calculate the hard timeout: needed because featureflags can be used
466 # to control the hard timeout, and they trigger DB access, but our
467 # DB tracers are not safe for reentrant use, so we must do this
468 # outside of the SQL stack. We must also do it after traversal so that
469 # the view is known and can be used in scope resolution. As we actually
470 # stash the pageid after afterTraversal, we need to do this even later.
471 da.set_permit_timeout_from_features(True)
472 da._get_request_timeout()
473
457 if isinstance(removeSecurityProxy(ob), METHOD_WRAPPER_TYPE):474 if isinstance(removeSecurityProxy(ob), METHOD_WRAPPER_TYPE):
458 # this is a direct call on a C-defined method such as __repr__ or475 # this is a direct call on a C-defined method such as __repr__ or
459 # dict.__setitem__. Apparently publishing this is possible and476 # dict.__setitem__. Apparently publishing this is possible and
460477
=== modified file 'lib/lp/services/features/flags.py'
--- lib/lp/services/features/flags.py 2010-09-27 08:13:49 +0000
+++ lib/lp/services/features/flags.py 2010-10-03 01:22:47 +0000
@@ -8,6 +8,7 @@
88
99
10from lp.services.features.rulesource import (10from lp.services.features.rulesource import (
11 NullFeatureRuleSource,
11 StormFeatureRuleSource,12 StormFeatureRuleSource,
12 )13 )
1314
@@ -151,7 +152,5 @@
151 """For use in testing: everything is turned off"""152 """For use in testing: everything is turned off"""
152153
153 def __init__(self):154 def __init__(self):
154 FeatureController.__init__(self, lambda scope: None)155 FeatureController.__init__(self, lambda scope: None,
155156 NullFeatureRuleSource())
156 def _loadRules(self):
157 return []