import time
import traceback
import math
+import random
from tornado.concurrent import Future, is_future, chain_future, future_set_exc_info, future_add_done_callback # noqa: E501
from tornado.log import app_log, gen_log
Note that the timeout is given in milliseconds, while most other
time-related functions in Tornado use seconds.
+ If ``jitter`` is specified, each callback time will be randomly selected
+ within a window of ``jitter * callback_time`` milliseconds.
+ Jitter can be used to reduce alignment of events with similar periods.
+ A jitter of 0.1 means allowing a 10% variation in callback time.
+ The window is centered on ``callback_time`` so the total number of calls
+ within a given interval should not be significantly affected by adding
+ jitter.
+
If the callback runs for longer than ``callback_time`` milliseconds,
subsequent invocations will be skipped to get back on schedule.
.. versionchanged:: 5.0
The ``io_loop`` argument (deprecated since version 4.1) has been removed.
+
+ .. versionchanged:: 5.1
+ The ``jitter`` argument is added.
"""
- def __init__(self, callback, callback_time):
+ def __init__(self, callback, callback_time, jitter=0):
self.callback = callback
if callback_time <= 0:
raise ValueError("Periodic callback must have a positive callback_time")
self.callback_time = callback_time
+ self.jitter = jitter
self._running = False
self._timeout = None
def _update_next(self, current_time):
callback_time_sec = self.callback_time / 1000.0
+ if self.jitter:
+ # apply jitter fraction
+ callback_time_sec *= 1 + (self.jitter * (random.random() - 0.5))
if self._next_timeout <= current_time:
# The period should be measured from the start of one call
# to the start of the next. If one call takes too long,
import threading
import time
import types
+try:
+ from unittest import mock # type: ignore
+except ImportError:
+ try:
+ import mock # type: ignore
+ except ImportError:
+ mock = None
from tornado.escape import native_str
from tornado import gen
self.assertEqual(self.simulate_calls(pc, [-100, 0, 0]),
[1010, 1020, 1030])
+ @unittest.skipIf(mock is None, 'mock package not present')
+ def test_jitter(self):
+ random_times = [0.5, 1, 0, 0.75]
+ expected = [1010, 1022.5, 1030, 1041.25]
+ call_durations = [0] * len(random_times)
+ pc = PeriodicCallback(None, 10000, jitter=0.5)
+
+ def mock_random():
+ return random_times.pop(0)
+ with mock.patch('random.random', mock_random):
+ self.assertEqual(self.simulate_calls(pc, call_durations),
+ expected)
+
+
class TestIOLoopConfiguration(unittest.TestCase):
def run_python(self, *statements):
statements = [