diff --git a/tornado/ioloop.py b/tornado/ioloop.py index bd5ff6fb..3477684c 100644 --- a/tornado/ioloop.py +++ b/tornado/ioloop.py @@ -486,6 +486,19 @@ class IOLoop(Configurable): """ raise NotImplementedError() + def spawn_callback(self, callback, *args, **kwargs): + """Calls the given callback on the next IOLoop iteration. + + Unlike all other callback-related methods on IOLoop, + ``spawn_callback`` does not associate the callback with its caller's + ``stack_context``, so it is suitable for fire-and-forget callbacks + that should not interfere with the caller. + + .. versionadded:: 4.0 + """ + with stack_context.NullContext(): + self.add_callback(callback, *args, **kwargs) + def add_future(self, future, callback): """Schedules a callback on the ``IOLoop`` when the given `.Future` is finished. diff --git a/tornado/test/ioloop_test.py b/tornado/test/ioloop_test.py index 03b4fddf..e4f07338 100644 --- a/tornado/test/ioloop_test.py +++ b/tornado/test/ioloop_test.py @@ -273,6 +273,19 @@ class TestIOLoop(AsyncTestCase): with ExpectLog(app_log, "Exception in callback"): self.wait() + def test_spawn_callback(self): + # An added callback runs in the test's stack_context, so will be + # re-arised in wait(). + self.io_loop.add_callback(lambda: 1/0) + with self.assertRaises(ZeroDivisionError): + self.wait() + # A spawned callback is run directly on the IOLoop, so it will be + # logged without stopping the test. + self.io_loop.spawn_callback(lambda: 1/0) + self.io_loop.add_callback(self.stop) + with ExpectLog(app_log, "Exception in callback"): + self.wait() + # Deliberately not a subclass of AsyncTestCase so the IOLoop isn't # automatically set as current.