From: Ben Darnell Date: Wed, 20 Aug 2014 03:01:28 +0000 (-0400) Subject: Fix a regression in which a timeout could fire after being cancelled. X-Git-Tag: v4.1.0b1~107 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=63033590b251f7ec5c8d05fdb6ee5de09d9f003f;p=thirdparty%2Ftornado.git Fix a regression in which a timeout could fire after being cancelled. Closes #1148. --- diff --git a/tornado/ioloop.py b/tornado/ioloop.py index e15252d34..a8f662acb 100644 --- a/tornado/ioloop.py +++ b/tornado/ioloop.py @@ -754,17 +754,18 @@ class PollIOLoop(IOLoop): # Do not run anything until we have determined which ones # are ready, so timeouts that call add_timeout cannot # schedule anything in this iteration. + due_timeouts = [] if self._timeouts: now = self.time() while self._timeouts: if self._timeouts[0].callback is None: - # the timeout was cancelled + # The timeout was cancelled. Note that the + # cancellation check is repeated below for timeouts + # that are cancelled by another timeout or callback. heapq.heappop(self._timeouts) self._cancellations -= 1 elif self._timeouts[0].deadline <= now: - timeout = heapq.heappop(self._timeouts) - callbacks.append(timeout.callback) - del timeout + due_timeouts.append(heapq.heappop(self._timeouts)) else: break if (self._cancellations > 512 @@ -778,9 +779,12 @@ class PollIOLoop(IOLoop): for callback in callbacks: self._run_callback(callback) + for timeout in due_timeouts: + if timeout.callback is not None: + self._run_callback(timeout.callback) # Closures may be holding on to a lot of memory, so allow # them to be freed before we go into our poll wait. - callbacks = callback = None + callbacks = callback = due_timeouts = timeout = None if self._callbacks: # If any callbacks or timeouts called add_callback, diff --git a/tornado/test/ioloop_test.py b/tornado/test/ioloop_test.py index 8bf6ee268..110158d12 100644 --- a/tornado/test/ioloop_test.py +++ b/tornado/test/ioloop_test.py @@ -173,6 +173,25 @@ class TestIOLoop(AsyncTestCase): self.io_loop.add_callback(lambda: self.io_loop.add_callback(self.stop)) self.wait() + def test_remove_timeout_from_timeout(self): + calls = [False, False] + + # Schedule several callbacks and wait for them all to come due at once. + # t2 should be cancelled by t1, even though it is already scheduled to + # be run before the ioloop even looks at it. + now = self.io_loop.time() + def t1(): + calls[0] = True + self.io_loop.remove_timeout(t2_handle) + self.io_loop.add_timeout(now + 0.01, t1) + def t2(): + calls[1] = True + t2_handle = self.io_loop.add_timeout(now + 0.02, t2) + self.io_loop.add_timeout(now + 0.03, self.stop) + time.sleep(0.03) + self.wait() + self.assertEqual(calls, [True, False]) + def test_timeout_with_arguments(self): # This tests that all the timeout methods pass through *args correctly. results = []