Skip to content

Commit c350dc9

Browse files
authored
Merge pull request tornadoweb#2545 from bdarnell/future-exception-cancel
concurrent: Add future_set_exception_unless_cancelled
2 parents 9868443 + 566441e commit c350dc9

File tree

5 files changed

+71
-20
lines changed

5 files changed

+71
-20
lines changed

tornado/concurrent.py

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
import sys
3434
import types
3535

36+
from tornado.log import app_log
37+
3638
import typing
3739
from typing import Any, Callable, Optional, Tuple, Union
3840

@@ -185,6 +187,28 @@ def future_set_result_unless_cancelled(
185187
future.set_result(value)
186188

187189

190+
def future_set_exception_unless_cancelled(
191+
future: Union["futures.Future[_T]", "Future[_T]"], exc: BaseException
192+
) -> None:
193+
"""Set the given ``exc`` as the `Future`'s exception.
194+
195+
If the Future is already canceled, logs the exception instead. If
196+
this logging is not desired, the caller should explicitly check
197+
the state of the Future and call Future.set_exception instead of
198+
this wrapper.
199+
200+
Avoids asyncio.InvalidStateError when calling set_exception() on
201+
a cancelled `asyncio.Future`.
202+
203+
.. versionadded:: 6.0
204+
205+
"""
206+
if not future.cancelled():
207+
future.set_exception(exc)
208+
else:
209+
app_log.error("Exception after Future was cancelled", exc_info=exc)
210+
211+
188212
def future_set_exc_info(
189213
future: Union["futures.Future[_T]", "Future[_T]"],
190214
exc_info: Tuple[
@@ -193,19 +217,20 @@ def future_set_exc_info(
193217
) -> None:
194218
"""Set the given ``exc_info`` as the `Future`'s exception.
195219
196-
Understands both `asyncio.Future` and Tornado's extensions to
197-
enable better tracebacks on Python 2.
220+
Understands both `asyncio.Future` and the extensions in older
221+
versions of Tornado to enable better tracebacks on Python 2.
198222
199223
.. versionadded:: 5.0
224+
225+
.. versionchanged:: 6.0
226+
227+
If the future is already cancelled, this function is a no-op.
228+
(previously asyncio.InvalidStateError would be raised)
229+
200230
"""
201-
if hasattr(future, "set_exc_info"):
202-
# Tornado's Future
203-
future.set_exc_info(exc_info) # type: ignore
204-
else:
205-
# asyncio.Future
206-
if exc_info[1] is None:
207-
raise Exception("future_set_exc_info called with no exception")
208-
future.set_exception(exc_info[1])
231+
if exc_info[1] is None:
232+
raise Exception("future_set_exc_info called with no exception")
233+
future_set_exception_unless_cancelled(future, exc_info[1])
209234

210235

211236
@typing.overload

tornado/httpclient.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@
4545
import time
4646
import weakref
4747

48-
from tornado.concurrent import Future, future_set_result_unless_cancelled
48+
from tornado.concurrent import (
49+
Future,
50+
future_set_result_unless_cancelled,
51+
future_set_exception_unless_cancelled,
52+
)
4953
from tornado.escape import utf8, native_str
5054
from tornado import gen, httputil
5155
from tornado.ioloop import IOLoop
@@ -295,7 +299,7 @@ def fetch(
295299
def handle_response(response: "HTTPResponse") -> None:
296300
if response.error:
297301
if raise_error or not response._error_is_response_code:
298-
future.set_exception(response.error)
302+
future_set_exception_unless_cancelled(future, response.error)
299303
return
300304
future_set_result_unless_cancelled(future, response)
301305

tornado/iostream.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -627,15 +627,17 @@ def _signal_closed(self) -> None:
627627
futures.append(self._connect_future)
628628
self._connect_future = None
629629
for future in futures:
630-
future.set_exception(StreamClosedError(real_error=self.error))
630+
if not future.done():
631+
future.set_exception(StreamClosedError(real_error=self.error))
631632
future.exception()
632633
if self._ssl_connect_future is not None:
633634
# _ssl_connect_future expects to see the real exception (typically
634635
# an ssl.SSLError), not just StreamClosedError.
635-
if self.error is not None:
636-
self._ssl_connect_future.set_exception(self.error)
637-
else:
638-
self._ssl_connect_future.set_exception(StreamClosedError())
636+
if not self._ssl_connect_future.done():
637+
if self.error is not None:
638+
self._ssl_connect_future.set_exception(self.error)
639+
else:
640+
self._ssl_connect_future.set_exception(StreamClosedError())
639641
self._ssl_connect_future.exception()
640642
self._ssl_connect_future = None
641643
if self._close_callback is not None:

tornado/process.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@
2727

2828
from binascii import hexlify
2929

30-
from tornado.concurrent import Future, future_set_result_unless_cancelled
30+
from tornado.concurrent import (
31+
Future,
32+
future_set_result_unless_cancelled,
33+
future_set_exception_unless_cancelled,
34+
)
3135
from tornado import ioloop
3236
from tornado.iostream import PipeIOStream
3337
from tornado.log import gen_log
@@ -296,7 +300,9 @@ def wait_for_exit(self, raise_error: bool = True) -> "Future[int]":
296300
def callback(ret: int) -> None:
297301
if ret != 0 and raise_error:
298302
# Unfortunately we don't have the original args any more.
299-
future.set_exception(CalledProcessError(ret, "unknown"))
303+
future_set_exception_unless_cancelled(
304+
future, CalledProcessError(ret, "unknown")
305+
)
300306
else:
301307
future_set_result_unless_cancelled(future, ret)
302308

tornado/test/httpclient_test.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from tornado.httpserver import HTTPServer
2626
from tornado.ioloop import IOLoop
2727
from tornado.iostream import IOStream
28-
from tornado.log import gen_log
28+
from tornado.log import gen_log, app_log
2929
from tornado import netutil
3030
from tornado.testing import AsyncHTTPTestCase, bind_unused_port, gen_test, ExpectLog
3131
from tornado.test.util import skipOnTravis
@@ -572,6 +572,20 @@ def test_response_times(self):
572572
for k, v in response.time_info.items():
573573
self.assertTrue(0 <= v < 1.0, "time_info[%s] out of bounds: %s" % (k, v))
574574

575+
@gen_test
576+
def test_error_after_cancel(self):
577+
fut = self.http_client.fetch(self.get_url("/404"))
578+
self.assertTrue(fut.cancel())
579+
with ExpectLog(app_log, "Exception after Future was cancelled") as el:
580+
# We can't wait on the cancelled Future any more, so just
581+
# let the IOLoop run until the exception gets logged (or
582+
# not, in which case we exit the loop and ExpectLog will
583+
# raise).
584+
for i in range(100):
585+
yield gen.sleep(0.01)
586+
if el.logged_stack:
587+
break
588+
575589

576590
class RequestProxyTest(unittest.TestCase):
577591
def test_request_set(self):

0 commit comments

Comments
 (0)