]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
Add time_func parameter to IOLoop, and make it possible to use time.monotonic.
authorBen Darnell <ben@bendarnell.com>
Mon, 1 Oct 2012 06:57:49 +0000 (23:57 -0700)
committerBen Darnell <ben@bendarnell.com>
Mon, 1 Oct 2012 07:07:28 +0000 (00:07 -0700)
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)

13 files changed:
tornado/curl_httpclient.py
tornado/ioloop.py
tornado/platform/auto.py
tornado/platform/twisted.py
tornado/simple_httpclient.py
tornado/test/ioloop_test.py
tornado/test/iostream_test.py
tornado/test/runtests.py
tornado/test/testing_test.py
tornado/testing.py
tornado/websocket.py
tox.ini
website/sphinx/releases/next.rst

index 3bdecaa064194a8c4ba39ef9317fcb78d7eb2635..df3c7501b46540494bac74249f3dc6b00dda7280 100644 (file)
@@ -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
index f2d84480bad2cb1424919ee736d3e295b1c639c7..1c7cddee7515ff58d56f911ca14a54f9cd72c868 100644 (file)
@@ -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)
index 7bfec116d738b0a09f52134a49f4bd3c181bb74c..7199cb5ce5fa0f709e069b9f266b06ff9660bc27 100644 (file)
@@ -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
index 2c094bd8c2631a21847e9650225baff148647366..1ce0e6bb8f5aa899d735f732f8ec8679db5b9731 100644 (file)
@@ -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)
index cb894468fec313ef4100c26ac50d76cdd60163ff..0c8ffaa40ac6c912970f1685679e89682a360212 100644 (file)
@@ -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)
index eec24c48f28a43ea6ef889cf7a65398b8e0f8bdf..023c3a39003d39be2b84479f4b20a7260e3cf4da 100644 (file)
@@ -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)
index 119a4f6cd8874b28ea108b967a190092dcae9d68..875dddfca91f8b7858c61cd04a9df3bf928ce4a9 100644 (file)
@@ -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()
index 103df58eb302604e1f31715ef74a4a67425a62b7..be5d696bd6bffcaf62bff792fac3a4ecb39a322b 100644 (file)
@@ -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 = {}
index 943f49f6c7d039e21328826fcb0b1293d2cb2d57..06e86ae7739ed9fbe04f74eeb58552f09d3aee7a 100644 (file)
@@ -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)
 
 
index b34b6dac970f5ea4593393e50f0c8b95c450cd04..602318f8e7b1e91434952d51e1915988b6253449 100644 (file)
@@ -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()
index 6287355188c1c891ff3fc9b1572edc4ae42bd186..08f2e0fe6148cb3a6e795d44d2f0391079eb7288 100644 (file)
@@ -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 9fb6fc1129ac0749c1671a0b9ec24ca1a7676967..9525e795985d6f19eb081e30d05fe411eee35646 100644 (file)
--- 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
index f4e8bbaaf3363acd243e0849ec2be9511315a928..3e8b0f706d623393102031dddd1c9b607e59422b 100644 (file)
@@ -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).