Merge lp:~elachuni/piston-mini-client/timeouts into lp:piston-mini-client

Proposed by Anthony Lenton
Status: Merged
Approved by: Anthony Lenton
Approved revision: 55
Merged at revision: 53
Proposed branch: lp:~elachuni/piston-mini-client/timeouts
Merge into: lp:piston-mini-client
Diff against target: 268 lines (+137/-8)
6 files modified
doc/envvars.rst (+3/-0)
doc/tuning.rst (+17/-0)
piston_mini_client/__init__.py (+31/-8)
piston_mini_client/consts.py (+1/-0)
piston_mini_client/failhandlers.py (+12/-0)
piston_mini_client/tests/test_timeout.py (+73/-0)
To merge this branch: bzr merge lp:~elachuni/piston-mini-client/timeouts
Reviewer Review Type Date Requested Status
software-store-developers Pending
Review via email: mp+100417@code.launchpad.net

Description of the change

This branch adds code for configuring and handling socket timeouts, to fix bug #937865

While I was there, I also wrapped other socket exceptions so that all errors are raised as APIError (or subclasses of that).

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

Thanks for looking into this Anthony.

A few thoughts,

 * Does the timeout span the whole request & response, or just the time it takes to get a connected socket? If the former, then a timeout per request would be very helpful.

 * Have you had a chance to test this with a pathological endpoint as described in bug 937865? I don't know off the top of my head how to make such a thing, but would be happy to figure it out together with you. I don't think such a thing needs to be added to the automatic test suite, but it would be good to have at least once-off verification that the patch achieves its purpose.

 * It would be helpful if the timeout exception included the time taken and a pointer to some docs on how to change the timeout.

Thanks again,
jml

Revision history for this message
Michael Vogt (mvo) wrote :

On Mon, Apr 02, 2012 at 02:12:26PM -0000, Jonathan Lange wrote:
[..]
> * Have you had a chance to test this with a pathological endpoint as described in bug 937865? I don't know off the top of my head how to make such a thing, but would be happy to figure it out together with you. I don't think such a thing needs to be added to the automatic test suite, but it would be good to have at least once-off verification that the patch achieves its purpose.
[..]

We recently added some code to software-center to test slow network,
it maybe useful for this test as well (we borrowed it from bzr iirc):

http://bazaar.launchpad.net/~software-store-developers/software-center/trunk/view/head:/contrib/simulate-slow-network.sh

Use: "tc qdisc del dev eth0 root" (or wlan0 or whatnot) to reset the
slowness again.

Cheers,
 Michael

54. By Anthony Lenton

Changed default default_timeout from 0 to None.

55. By Anthony Lenton

Added a test to check default default_timeout.

Revision history for this message
Anthony Lenton (elachuni) wrote :

> Thanks for looking into this Anthony.
>
> A few thoughts,
>
> * Does the timeout span the whole request & response, or just the time it
> takes to get a connected socket? If the former, then a timeout per request
> would be very helpful.

It affects both connect, read and write, but I afaik *each* of these operations gets the same timeout, so it's not a timeout for the whole API-level operation. Testing with a reasonably slow api and long request (staging ubuntu-recommender's "recommend-all-apps" call), setting timeout=0.5 makes the call return with an error after a few seconds:

Python 2.7.3rc2 (default, Mar 21 2012, 21:13:11)
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import timeit
>>> import sreclient
>>>
>>> api = sreclient.SoftwareCenterRecommenderAPI(
... service_root='http://rec.staging.ubuntu.com/api/1.0',
... timeout=0.5, log_filename='/tmp/api.log')
>>>
>>> def stmt():
... try:
... api._get('recommend_all_apps',
... extra_headers={'cache-control': 'no-cache'})
... except:
... print "Operation timed out!"
...
>>>
>>> timeit.repeat(stmt, number=1, repeat=3)
Operation timed out!
Operation timed out!
Operation timed out!
[3.7237908840179443, 2.8568179607391357, 2.0180060863494873]

re: Adding a timeout per request, I think this would make sense but httplib2 doesn't offer a per-request timeout, as it does its best to reuse connections between requests and only sets the timeout when it instantiates the connection (although I believe you can update the timeout on a socket no prob, so there should be a way to do this). I would be interested in looking into this option a bit more.

> * Have you had a chance to test this with a pathological endpoint as
> described in bug 937865? I don't know off the top of my head how to make such
> a thing, but would be happy to figure it out together with you. I don't think
> such a thing needs to be added to the automatic test suite, but it would be
> good to have at least once-off verification that the patch achieves its
> purpose.

The test above seems to say that it *doesn't* provide the kind of timeout you were asking for :/

> * It would be helpful if the timeout exception included the time taken and a
> pointer to some docs on how to change the timeout.

Where would you expect to find the pointer to the docs, when you print the exception? in the docstring? somewhere else?

> Thanks again,
> jml

Cheers,

achuni.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'doc/envvars.rst'
--- doc/envvars.rst 2012-03-30 21:35:01 +0000
+++ doc/envvars.rst 2012-04-02 14:26:18 +0000
@@ -13,6 +13,9 @@
13 ``piston_mini_client`` will log all requests and responses, including13 ``piston_mini_client`` will log all requests and responses, including
14 headers, to this location.14 headers, to this location.
1515
16 * ``PISTON_MINI_CLIENT_DEFAULT_TIMEOUT``: Is used as a socket timeout for
17 instances that don't explicitly set a timeout. Should be in seconds.
18
16 * ``http_proxy`` / ``https_proxy``: ``piston_mini_client`` will check these19 * ``http_proxy`` / ``https_proxy``: ``piston_mini_client`` will check these
17 variables to determine if a proxy should be used for each scheme.20 variables to determine if a proxy should be used for each scheme.
18 The `SocksiPy <http://socksipy.sourceforge.net/>`_ module is needed for21 The `SocksiPy <http://socksipy.sourceforge.net/>`_ module is needed for
1922
=== modified file 'doc/tuning.rst'
--- doc/tuning.rst 2012-03-30 21:35:01 +0000
+++ doc/tuning.rst 2012-04-02 14:26:18 +0000
@@ -176,3 +176,20 @@
176``PISTON_MINI_CLIENT_LOG_FILENAME`` in the environment to point a file,176``PISTON_MINI_CLIENT_LOG_FILENAME`` in the environment to point a file,
177and all ``PistonAPI`` instances will use this location by default. That is,177and all ``PistonAPI`` instances will use this location by default. That is,
178unless they're explicitly being instantiated to log elsewhere.178unless they're explicitly being instantiated to log elsewhere.
179
180=================
181Handling timeouts
182=================
183When you instantiate a ``PistonAPI`` you can provide an optional ``timeout``
184argument that will be used as a socket timeout for the requests that instance
185makes. To explicitly set no timeout, pass in ``timeout=0``. If you leave
186the default ``timeout=None``, the instance will first check for an environment
187variable ``PISTON_MINI_CLIENT_DEFAULT_TIMEOUT``, and if that is undefined or
188invalid, then the class's default timeout will be used; this can be defined
189by setting a ``default_timeout`` class attribute when
190writing the API class. Finally, if the class's default timeout is
191also ``None``, Python's system-wide socket default timeout will be used.
192
193You can't currently define timeouts on a per-request basis. If you need to
194change the timeout used for certain requests, you'll need to use a new
195``PistonAPI`` instance.
179196
=== modified file 'piston_mini_client/__init__.py'
--- piston_mini_client/__init__.py 2012-03-30 21:35:01 +0000
+++ piston_mini_client/__init__.py 2012-04-02 14:26:18 +0000
@@ -16,6 +16,7 @@
1616
17import json17import json
18import os18import os
19import socket
19import urllib20import urllib
20from datetime import datetime21from datetime import datetime
21from functools import wraps22from functools import wraps
@@ -23,13 +24,16 @@
2324
24from piston_mini_client.failhandlers import (25from piston_mini_client.failhandlers import (
25 APIError,26 APIError,
27 ExceptionFailHandler,
26 format_request,28 format_request,
27 format_response,29 format_response,
28 ExceptionFailHandler,30 SocketError,
31 TimeoutError,
29)32)
30from piston_mini_client.consts import (33from piston_mini_client.consts import (
31 DISABLE_SSL_VALIDATION_ENVVAR,34 DISABLE_SSL_VALIDATION_ENVVAR,
32 LOG_FILENAME_ENVVAR,35 LOG_FILENAME_ENVVAR,
36 TIMEOUT_ENVVAR,
33)37)
3438
3539
@@ -195,6 +199,7 @@
195 It's a small wrapper around ``httplib2`` to allow for a bit of state to199 It's a small wrapper around ``httplib2`` to allow for a bit of state to
196 be stored (like the service root) so that you don't need to repeat200 be stored (like the service root) so that you don't need to repeat
197 yourself as much.201 yourself as much.
202
198 It's not intended to be used directly. Children classes should implement203 It's not intended to be used directly. Children classes should implement
199 methods that actually call out to the api methods.204 methods that actually call out to the api methods.
200205
@@ -203,20 +208,16 @@
203 methods provided by this class.208 methods provided by this class.
204 """209 """
205 SUPPORTED_SCHEMAS = ("http", "https")210 SUPPORTED_SCHEMAS = ("http", "https")
206
207 default_service_root = ''211 default_service_root = ''
208
209 default_content_type = 'application/json'212 default_content_type = 'application/json'
210213 default_timeout = None
211 fail_handler = ExceptionFailHandler214 fail_handler = ExceptionFailHandler
212
213 extra_headers = None215 extra_headers = None
214
215 serializers = None216 serializers = None
216217
217 def __init__(self, service_root=None, cachedir=None, auth=None,218 def __init__(self, service_root=None, cachedir=None, auth=None,
218 offline_mode=False, disable_ssl_validation=False,219 offline_mode=False, disable_ssl_validation=False,
219 log_filename=None):220 log_filename=None, timeout=None):
220 """Initialize a ``PistonAPI``.221 """Initialize a ``PistonAPI``.
221222
222 ``service_root`` is the url to the server's service root.223 ``service_root`` is the url to the server's service root.
@@ -241,6 +242,15 @@
241242
242 If you pass in a ``log_filename``, all requests and responses243 If you pass in a ``log_filename``, all requests and responses
243 including headers will be logged to this file.244 including headers will be logged to this file.
245
246 ``timeout`` will be used as a socket timeout for all calls this
247 instance makes. To explicitly set no timeout, set timeout=0. The
248 default timeout=None will first check for an environment variable
249 ``PISTON_MINI_CLIENT_DEFAULT_TIMEOUT`` and try to use that. If this
250 environment variable is not found or it is an invalid float, the
251 class's ``default_timeout`` will be used. Finally, if the class's
252 default is also None, Python's default timeout for sockets will be
253 used. All these should be in seconds.
244 """254 """
245 if service_root is None:255 if service_root is None:
246 service_root = self.default_service_root256 service_root = self.default_service_root
@@ -259,6 +269,12 @@
259 self._auth = auth269 self._auth = auth
260 self._offline_mode = offline_mode270 self._offline_mode = offline_mode
261 self._disable_ssl_validation = disable_ssl_validation271 self._disable_ssl_validation = disable_ssl_validation
272 if timeout is None:
273 try:
274 timeout = float(os.environ.get(TIMEOUT_ENVVAR))
275 except (TypeError, ValueError):
276 timeout = self.default_timeout
277 self._timeout = timeout
262 # create one httplib2.Http object per scheme so that we can278 # create one httplib2.Http object per scheme so that we can
263 # have per-scheme proxy settings (see also Issue 26279 # have per-scheme proxy settings (see also Issue 26
264 # http://code.google.com/p/httplib2/issues/detail?id=26)280 # http://code.google.com/p/httplib2/issues/detail?id=26)
@@ -279,6 +295,7 @@
279 try:295 try:
280 http = httplib2.Http(296 http = httplib2.Http(
281 cache=self._httplib2_cache,297 cache=self._httplib2_cache,
298 timeout=self._timeout,
282 disable_ssl_certificate_validation=True,299 disable_ssl_certificate_validation=True,
283 proxy_info=proxy_info)300 proxy_info=proxy_info)
284 except TypeError:301 except TypeError:
@@ -286,7 +303,7 @@
286 pass303 pass
287 if http is None:304 if http is None:
288 http = httplib2.Http(cache=self._httplib2_cache,305 http = httplib2.Http(cache=self._httplib2_cache,
289 proxy_info=proxy_info)306 timeout=self._timeout, proxy_info=proxy_info)
290 return http307 return http
291308
292 def _get_proxy_info(self, scheme):309 def _get_proxy_info(self, scheme):
@@ -498,6 +515,12 @@
498 raise APIError('Unable to connect to %s' % self._service_root)515 raise APIError('Unable to connect to %s' % self._service_root)
499 else:516 else:
500 raise517 raise
518 except socket.timeout, e:
519 raise TimeoutError('Timed out attempting to connect to %s' %
520 self._service_root)
521 except (socket.gaierror, socket.error), e:
522 raise SocketError('connecting to %s: %s' % (self._service_root,
523 e.message))
501 if self.log_filename:524 if self.log_filename:
502 self._dump_response(response, response_body)525 self._dump_response(response, response_body)
503 handler = self.fail_handler(url, method, body, headers)526 handler = self.fail_handler(url, method, body, headers)
504527
=== modified file 'piston_mini_client/consts.py'
--- piston_mini_client/consts.py 2012-03-30 21:35:01 +0000
+++ piston_mini_client/consts.py 2012-04-02 14:26:18 +0000
@@ -5,3 +5,4 @@
5DISABLE_SSL_VALIDATION_ENVVAR = 'PISTON_MINI_CLIENT_DISABLE_SSL_VALIDATION'5DISABLE_SSL_VALIDATION_ENVVAR = 'PISTON_MINI_CLIENT_DISABLE_SSL_VALIDATION'
6DEBUG_ENVVAR = 'PISTON_MINI_CLIENT_DEBUG'6DEBUG_ENVVAR = 'PISTON_MINI_CLIENT_DEBUG'
7LOG_FILENAME_ENVVAR = 'PISTON_MINI_CLIENT_LOG_FILENAME'7LOG_FILENAME_ENVVAR = 'PISTON_MINI_CLIENT_LOG_FILENAME'
8TIMEOUT_ENVVAR = 'PISTON_MINI_CLIENT_DEFAULT_TIMEOUT'
89
=== modified file 'piston_mini_client/failhandlers.py'
--- piston_mini_client/failhandlers.py 2012-03-30 21:35:01 +0000
+++ piston_mini_client/failhandlers.py 2012-04-02 14:26:18 +0000
@@ -66,6 +66,18 @@
66 return self.msg66 return self.msg
6767
6868
69class SocketError(APIError):
70 def __init__(self, msg):
71 self.msg = msg
72
73 def __str__(self):
74 return self.msg
75
76
77class TimeoutError(SocketError):
78 pass
79
80
69class BaseFailHandler(object):81class BaseFailHandler(object):
70 """A base class for fail handlers.82 """A base class for fail handlers.
7183
7284
=== added file 'piston_mini_client/tests/test_timeout.py'
--- piston_mini_client/tests/test_timeout.py 1970-01-01 00:00:00 +0000
+++ piston_mini_client/tests/test_timeout.py 2012-04-02 14:26:18 +0000
@@ -0,0 +1,73 @@
1# -*- coding: utf-8 -*-
2# Copyright 2010-2012 Canonical Ltd. This software is licensed under the
3# GNU Lesser General Public License version 3 (see the file LICENSE).
4
5import socket
6from unittest import TestCase
7from mock import patch
8from piston_mini_client import PistonAPI
9from piston_mini_client.failhandlers import APIError
10
11
12class LazyAPI(PistonAPI):
13 default_service_root = 'http://test.info/api/1.0/'
14 default_timeout = 42
15
16 def sleep(self, amount):
17 return self._get('/snooze/%s/' % amount)
18
19
20class TimeoutTestCase(TestCase):
21 @patch('os.environ.get')
22 @patch('httplib2.Http')
23 def test_timeout_in_constructor_wins(self, mock_http, mock_get):
24 mock_get.return_value = '3.14'
25 api = LazyAPI(timeout=1)
26 self.assertEqual(1, api._timeout)
27 mock_http.assert_called_with(cache=None, proxy_info=None, timeout=1,
28 disable_ssl_certificate_validation=True)
29
30 @patch('os.environ.get')
31 @patch('httplib2.Http')
32 def test_timeout_in_env_beats_class_default(self, mock_http, mock_get):
33 mock_get.return_value = '3.14'
34 api = LazyAPI()
35 self.assertEqual(3.14, api._timeout)
36 mock_http.assert_called_with(cache=None, proxy_info=None, timeout=3.14,
37 disable_ssl_certificate_validation=True)
38
39 @patch('os.environ.get')
40 @patch('httplib2.Http')
41 def test_no_envvar_falls_back_to_class_default(self, mock_http, mock_get):
42 mock_get.return_value = None
43 api = LazyAPI()
44 self.assertEqual(42, api._timeout)
45 mock_http.assert_called_with(cache=None, proxy_info=None, timeout=42)
46
47 @patch('os.environ.get')
48 @patch('httplib2.Http')
49 def test_no_nothing_falls_back_to_system_default(self, mock_http,
50 mock_get):
51 class DefaultAPI(PistonAPI):
52 default_service_root = 'http://test.info/api/1.0/'
53
54 mock_get.return_value = None
55 api = DefaultAPI()
56 self.assertEqual(None, api._timeout)
57 mock_http.assert_called_with(cache=None, proxy_info=None, timeout=None)
58
59 @patch('os.environ.get')
60 @patch('httplib2.Http')
61 def test_invalid_envvar_uses_class_default(self, mock_http, mock_get):
62 mock_get.return_value = 'invalid'
63 api = LazyAPI()
64 self.assertEqual(42, api._timeout)
65 mock_http.assert_called_with(cache=None, proxy_info=None, timeout=42,
66 disable_ssl_certificate_validation=True)
67
68 @patch('httplib2.HTTPConnectionWithTimeout.connect')
69 def test_timeout_is_handled_by_failhandler(self, mock_connect):
70 mock_connect.side_effect = socket.timeout
71 api = LazyAPI()
72
73 self.assertRaises(APIError, api.sleep, 2)

Subscribers

People subscribed via source and target branches