]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
Support jitter in PeriodicCallback (#2330)
authorMin RK <benjaminrk@gmail.com>
Fri, 13 Apr 2018 15:36:59 +0000 (17:36 +0200)
committerBen Darnell <ben@bendarnell.com>
Fri, 13 Apr 2018 15:36:59 +0000 (11:36 -0400)
reduces likelihood of alignment with many similar timers

tornado/ioloop.py
tornado/test/ioloop_test.py

index 4870013942d12956ad44b88b4683132a89ec5067..123f2ba5de8d53752eae17e09ad4201944d3e458 100644 (file)
@@ -47,6 +47,7 @@ import threading
 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
@@ -1161,6 +1162,14 @@ class PeriodicCallback(object):
     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.
 
@@ -1168,12 +1177,16 @@ class PeriodicCallback(object):
 
     .. 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
 
@@ -1218,6 +1231,9 @@ class PeriodicCallback(object):
 
     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,
index 9f7c184782d38a181247b574af2286374e0ee027..b1b22b1af69390fd002943040e82838f19c40cf9 100644 (file)
@@ -10,6 +10,13 @@ import sys
 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
@@ -845,6 +852,20 @@ class TestPeriodicCallbackMath(unittest.TestCase):
         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 = [