diff --git a/tornado/gen.py b/tornado/gen.py index 915e5a5300..6eac483097 100644 --- a/tornado/gen.py +++ b/tornado/gen.py @@ -556,11 +556,17 @@ class Multi(YieldPoint): Futures, in which case a parallel dictionary is returned mapping the same keys to their results. + It is not normally necessary to call this class directly, as it + will be created automatically as needed. However, calling it directly + allows you to use the ``quiet_exceptions`` argument to control + the logging of multiple exceptions. + .. versionchanged:: 4.2 If multiple ``YieldPoints`` fail, any exceptions after the first - (which is raised) will be logged. + (which is raised) will be logged. Added the ``quiet_exceptions`` + argument to suppress this logging for selected exception types. """ - def __init__(self, children): + def __init__(self, children, quiet_exceptions=()): self.keys = None if isinstance(children, dict): self.keys = list(children.keys()) @@ -572,6 +578,7 @@ def __init__(self, children): self.children.append(i) assert all(isinstance(i, YieldPoint) for i in self.children) self.unfinished_children = set(self.children) + self.quiet_exceptions = quiet_exceptions def start(self, runner): for i in self.children: @@ -589,12 +596,13 @@ def get_result(self): for f in self.children: try: result_list.append(f.get_result()) - except Exception: + except Exception as e: if exc_info is None: exc_info = sys.exc_info() else: - app_log.error("Multiple exceptions in yield list", - exc_info=True) + if not isinstance(e, self.quiet_exceptions): + app_log.error("Multiple exceptions in yield list", + exc_info=True) if exc_info is not None: raise_exc_info(exc_info) if self.keys is not None: @@ -603,7 +611,7 @@ def get_result(self): return list(result_list) -def multi_future(children): +def multi_future(children, quiet_exceptions=()): """Wait for multiple asynchronous futures in parallel. Takes a list of ``Futures`` (but *not* other ``YieldPoints``) and returns @@ -616,16 +624,21 @@ def multi_future(children): Futures, in which case a parallel dictionary is returned mapping the same keys to their results. - It is not necessary to call `multi_future` explcitly, since the engine will - do so automatically when the generator yields a list of `Futures`. - This function is faster than the `Multi` `YieldPoint` because it does not - require the creation of a stack context. + It is not normally necessary to call `multi_future` explcitly, + since the engine will do so automatically when the generator + yields a list of `Futures`. However, calling it directly + allows you to use the ``quiet_exceptions`` argument to control + the logging of multiple exceptions. + + This function is faster than the `Multi` `YieldPoint` because it + does not require the creation of a stack context. .. versionadded:: 4.0 .. versionchanged:: 4.2 If multiple ``Futures`` fail, any exceptions after the first (which is - raised) will be logged. + raised) will be logged. Added the ``quiet_exceptions`` + argument to suppress this logging for selected exception types. """ if isinstance(children, dict): keys = list(children.keys()) @@ -646,10 +659,11 @@ def callback(f): for f in children: try: result_list.append(f.result()) - except Exception: + except Exception as e: if future.done(): - app_log.error("Multiple exceptions in yield list", - exc_info=True) + if not isinstance(e, quiet_exceptions): + app_log.error("Multiple exceptions in yield list", + exc_info=True) else: future.set_exc_info(sys.exc_info()) if not future.done(): diff --git a/tornado/test/gen_test.py b/tornado/test/gen_test.py index b5b459281b..53c89ace59 100644 --- a/tornado/test/gen_test.py +++ b/tornado/test/gen_test.py @@ -403,12 +403,17 @@ def test_multi_exceptions(self): self.async_exception(RuntimeError("error 2"))]) self.assertEqual(str(cm.exception), "error 1") - return # With only one exception, no error is logged. with self.assertRaises(RuntimeError): yield gen.Multi([self.async_exception(RuntimeError("error 1")), self.async_future(2)]) + # Exception logging may be explicitly quieted. + with self.assertRaises(RuntimeError): + yield gen.Multi([self.async_exception(RuntimeError("error 1")), + self.async_exception(RuntimeError("error 2"))], + quiet_exceptions=RuntimeError) + @gen_test def test_multi_future_exceptions(self): with ExpectLog(app_log, "Multiple exceptions in yield list"): @@ -422,6 +427,13 @@ def test_multi_future_exceptions(self): yield [self.async_exception(RuntimeError("error 1")), self.async_future(2)] + # Exception logging may be explicitly quieted. + with self.assertRaises(RuntimeError): + yield gen.multi_future( + [self.async_exception(RuntimeError("error 1")), + self.async_exception(RuntimeError("error 2"))], + quiet_exceptions=RuntimeError) + def test_arguments(self): @gen.engine def f():