Skip to content
Browse files

Add time_func parameter to IOLoop, and make it possible to use time.m…

…onotonic.

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)
  • Loading branch information...
1 parent 469e227 commit 20deb5ca6107c0f95c2a1004b7bd13740c2cde4b @bdarnell bdarnell committed
View
2 tornado/curl_httpclient.py
@@ -109,7 +109,7 @@ def _set_timeout(self, msecs):
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
View
36 tornado/ioloop.py
@@ -139,10 +139,11 @@ def configurable_default(cls):
_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 @@ def start(self):
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 @@ def running(self):
"""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 @@ def __init__(self, callback, callback_time, io_loop=None):
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 @@ def _run(self):
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)
View
11 tornado/platform/auto.py
@@ -32,3 +32,14 @@
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
View
2 tornado/platform/twisted.py
@@ -139,7 +139,7 @@ def start_if_necessary():
# 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)
View
6 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 @@ def cleanup(self):
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 @@ def _on_body(self, data):
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)
View
2 tornado/test/ioloop_test.py
@@ -30,7 +30,7 @@ def schedule_callback():
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)
View
2 tornado/test/iostream_test.py
@@ -292,7 +292,7 @@ def test_close_buffered_data(self):
# 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()
View
16 tornado/test/runtests.py
@@ -6,7 +6,7 @@
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 @@ def run(self, test):
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 = {}
View
4 tornado/test/testing_test.py
@@ -20,9 +20,9 @@ def test_subsequent_wait_calls(self):
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)
View
2 tornado/testing.py
@@ -222,7 +222,7 @@ def timeout_func():
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()
View
2 tornado/websocket.py
@@ -668,4 +668,4 @@ def close(self):
# 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)
View
16 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
View
12 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).

0 comments on commit 20deb5c

Please sign in to comment.
Something went wrong with that request. Please try again.