Skip to content

Commit

Permalink
Support jitter in PeriodicCallback (tornadoweb#2330)
Browse files Browse the repository at this point in the history
reduces likelihood of alignment with many similar timers
  • Loading branch information
minrk authored and bdarnell committed Apr 13, 2018
1 parent 415f453 commit 3fe0c5e
Show file tree
Hide file tree
Showing 2 changed files with 38 additions and 1 deletion.
18 changes: 17 additions & 1 deletion tornado/ioloop.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
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
Expand Down Expand Up @@ -1161,19 +1162,31 @@ 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.
`start` must be called after the `PeriodicCallback` is created.
.. 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

Expand Down Expand Up @@ -1218,6 +1231,9 @@ def _schedule_next(self):

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,
Expand Down
21 changes: 21 additions & 0 deletions tornado/test/ioloop_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@
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
Expand Down Expand Up @@ -845,6 +852,20 @@ def test_clock_backwards(self):
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 = [
Expand Down

0 comments on commit 3fe0c5e

Please sign in to comment.