From: Ben Darnell Date: Mon, 1 Oct 2012 06:57:49 +0000 (-0700) Subject: Add time_func parameter to IOLoop, and make it possible to use time.monotonic. X-Git-Tag: v3.0.0~249 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=20deb5ca6107c0f95c2a1004b7bd13740c2cde4b;p=thirdparty%2Ftornado.git Add time_func parameter to IOLoop, and make it possible to use time.monotonic. This means that calls to IOLoop.add_timeout that pass a number must be updated to use IOLoop.time instead of time.time. There are still some places where we use time.time in the code, but they are either places where wall time is desired, or non-critical deltas (e.g. printing elapsed time at the end of a request). Thanks to apenwarr and mgenti for pull requests and discussion relating to this change. (#558 and #583) --- diff --git a/tornado/curl_httpclient.py b/tornado/curl_httpclient.py index 3bdecaa06..df3c7501b 100644 --- a/tornado/curl_httpclient.py +++ b/tornado/curl_httpclient.py @@ -109,7 +109,7 @@ class CurlAsyncHTTPClient(AsyncHTTPClient): if self._timeout is not None: self.io_loop.remove_timeout(self._timeout) self._timeout = self.io_loop.add_timeout( - time.time() + msecs / 1000.0, self._handle_timeout) + self.io_loop.time() + msecs / 1000.0, self._handle_timeout) def _handle_events(self, fd, events): """Called by IOLoop when there is activity on one of our diff --git a/tornado/ioloop.py b/tornado/ioloop.py index f2d84480b..1c7cddee7 100644 --- a/tornado/ioloop.py +++ b/tornado/ioloop.py @@ -139,10 +139,11 @@ class IOLoop(Configurable): _current = threading.local() - def initialize(self, impl): + def initialize(self, impl, time_func=None): self._impl = impl if hasattr(self._impl, 'fileno'): set_close_exec(self._impl.fileno()) + self.time_func = time_func or time.time self._handlers = {} self._events = {} self._callbacks = [] @@ -360,7 +361,7 @@ class IOLoop(Configurable): self._run_callback(callback) if self._timeouts: - now = time.time() + now = self.time() while self._timeouts: if self._timeouts[0].callback is None: # the timeout was cancelled @@ -458,20 +459,35 @@ class IOLoop(Configurable): """Returns true if this IOLoop is currently running.""" return self._running + def time(self): + """Returns the current time according to the IOLoop's clock. + + The return value is a floating-point number relative to an + unspecified time in the past. + + By default, the IOLoop's time function is `time.time`. However, + it may be configured to use e.g. `time.monotonic` instead. + Calls to `add_timeout` that pass a number instead of a + `datetime.timedelta` should use this function to compute the + appropriate time, so they can work no matter what time function + is chosen. + """ + return self.time_func() + def add_timeout(self, deadline, callback): """Calls the given callback at the time deadline from the I/O loop. Returns a handle that may be passed to remove_timeout to cancel. - ``deadline`` may be a number denoting a unix timestamp (as returned - by ``time.time()`` or a ``datetime.timedelta`` object for a deadline - relative to the current time. + ``deadline`` may be a number denoting a time relative to + `IOLoop.time`, or a ``datetime.timedelta`` object for a + deadline relative to the current time. Note that it is not safe to call `add_timeout` from other threads. Instead, you must use `add_callback` to transfer control to the IOLoop's thread, and then call `add_timeout` from there. """ - timeout = _Timeout(deadline, stack_context.wrap(callback)) + timeout = _Timeout(deadline, stack_context.wrap(callback), self) heapq.heappush(self._timeouts, timeout) return timeout @@ -578,11 +594,11 @@ class _Timeout(object): # Reduce memory overhead when there are lots of pending callbacks __slots__ = ['deadline', 'callback'] - def __init__(self, deadline, callback): + def __init__(self, deadline, callback, io_loop): if isinstance(deadline, (int, long, float)): self.deadline = deadline elif isinstance(deadline, datetime.timedelta): - self.deadline = time.time() + _Timeout.timedelta_to_seconds(deadline) + self.deadline = io_loop.time() + _Timeout.timedelta_to_seconds(deadline) else: raise TypeError("Unsupported deadline %r" % deadline) self.callback = callback @@ -622,7 +638,7 @@ class PeriodicCallback(object): def start(self): """Starts the timer.""" self._running = True - self._next_timeout = time.time() + self._next_timeout = self.io_loop.time() self._schedule_next() def stop(self): @@ -643,7 +659,7 @@ class PeriodicCallback(object): def _schedule_next(self): if self._running: - current_time = time.time() + current_time = self.io_loop.time() while self._next_timeout <= current_time: self._next_timeout += self.callback_time / 1000.0 self._timeout = self.io_loop.add_timeout(self._next_timeout, self._run) diff --git a/tornado/platform/auto.py b/tornado/platform/auto.py index 7bfec116d..7199cb5ce 100644 --- a/tornado/platform/auto.py +++ b/tornado/platform/auto.py @@ -32,3 +32,14 @@ if os.name == 'nt': from tornado.platform.windows import set_close_exec else: from tornado.platform.posix import set_close_exec, Waker + +try: + # monotime monkey-patches the time module to have a monotonic function + # in versions of python before 3.3. + import monotime +except ImportError: + pass +try: + from time import monotonic as monotonic_time +except ImportError: + monotonic_time = None diff --git a/tornado/platform/twisted.py b/tornado/platform/twisted.py index 2c094bd8c..1ce0e6bb8 100644 --- a/tornado/platform/twisted.py +++ b/tornado/platform/twisted.py @@ -139,7 +139,7 @@ class TornadoReactor(PosixReactorBase): # IReactorTime def seconds(self): - return time.time() + return self._io_loop.time() def callLater(self, seconds, f, *args, **kw): dc = TornadoDelayedCall(self, seconds, f, *args, **kw) diff --git a/tornado/simple_httpclient.py b/tornado/simple_httpclient.py index cb894468f..0c8ffaa40 100644 --- a/tornado/simple_httpclient.py +++ b/tornado/simple_httpclient.py @@ -127,7 +127,7 @@ class _HTTPConnection(object): def __init__(self, io_loop, client, request, release_callback, final_callback, max_buffer_size): - self.start_time = time.time() + self.start_time = io_loop.time() self.io_loop = io_loop self.client = client self.request = request @@ -324,7 +324,7 @@ class _HTTPConnection(object): except Exception, e: gen_log.warning("uncaught exception", exc_info=True) self._run_callback(HTTPResponse(self.request, 599, error=e, - request_time=time.time() - self.start_time, + request_time=self.io_loop.time() - self.start_time, )) if hasattr(self, "stream"): self.stream.close() @@ -440,7 +440,7 @@ class _HTTPConnection(object): response = HTTPResponse(original_request, self.code, reason=self.reason, headers=self.headers, - request_time=time.time() - self.start_time, + request_time=self.io_loop.time() - self.start_time, buffer=buffer, effective_url=self.request.url) self._run_callback(response) diff --git a/tornado/test/ioloop_test.py b/tornado/test/ioloop_test.py index eec24c48f..023c3a390 100644 --- a/tornado/test/ioloop_test.py +++ b/tornado/test/ioloop_test.py @@ -30,7 +30,7 @@ class TestIOLoop(AsyncTestCase): self.io_loop.add_callback(callback) # Store away the time so we can check if we woke up immediately self.start_time = time.time() - self.io_loop.add_timeout(time.time(), schedule_callback) + self.io_loop.add_timeout(self.io_loop.time(), schedule_callback) self.wait() self.assertAlmostEqual(time.time(), self.start_time, places=2) self.assertTrue(self.called) diff --git a/tornado/test/iostream_test.py b/tornado/test/iostream_test.py index 119a4f6cd..875dddfca 100644 --- a/tornado/test/iostream_test.py +++ b/tornado/test/iostream_test.py @@ -292,7 +292,7 @@ class TestIOStreamMixin(object): # Allow the close to propagate to the client side of the # connection. Using add_callback instead of add_timeout # doesn't seem to work, even with multiple iterations - self.io_loop.add_timeout(time.time() + 0.01, self.stop) + self.io_loop.add_timeout(self.io_loop.time() + 0.01, self.stop) self.wait() client.read_bytes(256, self.stop) data = self.wait() diff --git a/tornado/test/runtests.py b/tornado/test/runtests.py index 103df58eb..be5d696bd 100644 --- a/tornado/test/runtests.py +++ b/tornado/test/runtests.py @@ -6,7 +6,7 @@ import textwrap import sys from tornado.httpclient import AsyncHTTPClient from tornado.ioloop import IOLoop -from tornado.options import define +from tornado.options import define, options, add_parse_callback from tornado.test.util import unittest TEST_MODULES = [ @@ -81,8 +81,18 @@ if __name__ == '__main__': define('httpclient', type=str, default=None, callback=AsyncHTTPClient.configure) - define('ioloop', type=str, default=None, - callback=IOLoop.configure) + define('ioloop', type=str, default=None) + define('ioloop_time_monotonic', default=False) + def configure_ioloop(): + kwargs = {} + if options.ioloop_time_monotonic: + from tornado.platform.auto import monotonic_time + if monotonic_time is None: + raise RuntimeError("monotonic clock not found") + kwargs['time_func'] = monotonic_time + if options.ioloop or kwargs: + IOLoop.configure(options.ioloop, **kwargs) + add_parse_callback(configure_ioloop) import tornado.testing kwargs = {} diff --git a/tornado/test/testing_test.py b/tornado/test/testing_test.py index 943f49f6c..06e86ae77 100644 --- a/tornado/test/testing_test.py +++ b/tornado/test/testing_test.py @@ -20,9 +20,9 @@ class AsyncTestCaseTest(AsyncTestCase): This test makes sure that a second call to wait() clears the first timeout. """ - self.io_loop.add_timeout(time.time() + 0.01, self.stop) + self.io_loop.add_timeout(self.io_loop.time() + 0.01, self.stop) self.wait(timeout=0.02) - self.io_loop.add_timeout(time.time() + 0.03, self.stop) + self.io_loop.add_timeout(self.io_loop.time() + 0.03, self.stop) self.wait(timeout=0.1) diff --git a/tornado/testing.py b/tornado/testing.py index b34b6dac9..602318f8e 100644 --- a/tornado/testing.py +++ b/tornado/testing.py @@ -222,7 +222,7 @@ class AsyncTestCase(unittest.TestCase): self.stop() if self.__timeout is not None: self.io_loop.remove_timeout(self.__timeout) - self.__timeout = self.io_loop.add_timeout(time.time() + timeout, timeout_func) + self.__timeout = self.io_loop.add_timeout(self.io_loop.time() + timeout, timeout_func) while True: self.__running = True self.io_loop.start() diff --git a/tornado/websocket.py b/tornado/websocket.py index 628735518..08f2e0fe6 100644 --- a/tornado/websocket.py +++ b/tornado/websocket.py @@ -668,4 +668,4 @@ class WebSocketProtocol13(WebSocketProtocol): # Give the client a few seconds to complete a clean shutdown, # otherwise just close the connection. self._waiting = self.stream.io_loop.add_timeout( - time.time() + 5, self._abort) + self.stream.io_loop.time() + 5, self._abort) diff --git a/tox.ini b/tox.ini index 9fb6fc112..9525e7959 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,7 @@ [tox] # "-full" variants include optional dependencies, to ensure # that things work both in a bare install and with all the extras. -envlist = py27-full, py27-curl, py25-full, py32, pypy, py25, py26, py26-full, py27, py32-utf8, py33, py27-opt, py32-opt, pypy-full +envlist = py27-full, py27-curl, py25-full, py32, pypy, py25, py26, py26-full, py27, py32-utf8, py33, py27-opt, py32-opt, pypy-full, py27-select, py27-monotonic, py33-monotonic [testenv] commands = python -m tornado.test.runtests {posargs:} @@ -85,6 +85,16 @@ deps = twisted>=12.0.0 commands = python -m tornado.test.runtests --ioloop=tornado.ioloop.SelectIOLoop {posargs:} +[testenv:py27-monotonic] +basepython = python2.7 +# TODO: remove this url when the pypi page is updated. +deps = + http://pypi.python.org/packages/source/M/Monotime/Monotime-1.0.tar.gz + futures + pycurl + twisted +commands = python -m tornado.test.runtests --ioloop_time_monotonic {posargs:} + [testenv:pypy-full] # This configuration works with pypy 1.9. pycurl installs ok but # curl_httpclient doesn't work. Twisted works most of the time, but @@ -116,6 +126,10 @@ setenv = LANG=en_US.utf-8 # tox doesn't yet know "py33" by default basepython = python3.3 +[testenv:py33-monotonic] +basepython = python3.3 +commands = python -m tornado.test.runtests --ioloop_time_monotonic {posargs:} + # Python's optimized mode disables the assert statement, so run the # tests in this mode to ensure we haven't fallen into the trap of relying # on an assertion's side effects or using them for things that should be diff --git a/website/sphinx/releases/next.rst b/website/sphinx/releases/next.rst index f4e8bbaaf..3e8b0f706 100644 --- a/website/sphinx/releases/next.rst +++ b/website/sphinx/releases/next.rst @@ -119,3 +119,15 @@ In progress option namespace. The `tornado.options` module's new callback support now makes it easy to add options from a wrapper script instead of putting all possible options in `tornado.testing.main`. +* The `IOLoop` constructor has a new keyword argument ``time_func``, + which can be used to set the time function used when scheduling callbacks. + This is most useful with the `time.monotonic()` function, introduced + in Python 3.3 and backported to older versions via the ``monotime`` + module. Using a monotonic clock here avoids problems when the system + clock is changed. +* New function `IOLoop.time` returns the current time according to the + IOLoop. To use the new monotonic clock functionality, all calls to + `IOLoop.add_timeout` must be either pass a `datetime.timedelta` or + a time relative to `IOLoop.time`, not `time.time`. (`time.time` will + continue to work only as long as the IOLoop's ``time_func`` argument + is not used).