Repository: tornadoweb/tornado

Issue:
Excessive CPU load when returning empty collection
The following causes my CPU to stay pinned at 100% usage

``` python
from tornado.ioloop import IOLoop
from tornado import gen
from threading import Thread

loop = IOLoop.current()
t = Thread(target=loop.start, daemon=True)
t.start()

@gen.coroutine
def f():
    yield gen.sleep(0)
    return {}

loop.add_callback(f)

from time import sleep  # just to stick around in case you run this in a script
sleep(100)
```

Replacing the empty dict `{}` with an empty list `[]` has the same effect.  Replacing with `None` behaves as expected.

Running Python 3.5, Tornado 4.4.1, on Ubuntu 16.04



Discussion:
This can be reproduced as simply as:

```
from tornado.ioloop import IOLoop
from tornado import gen

@gen.coroutine
def f():
    return {}

loop = IOLoop.current()
loop.add_callback(f)
loop.start()
```

In `_run_callback`, the loop runs `f` which returns `{}`. `_run_callback` then calls `convert_yielded` on `{}` and creates an empty `multi_future` and schedules it on the IOLoop:

```
    def _run_callback(self, callback):
        """Runs a callback with error handling.

        For use in subclasses.
        """
        try:
            ret = callback()
            if ret is not None:
                from tornado import gen
                # Functions that return Futures typically swallow all
                # exceptions and store them in the Future.  If a Future
                # makes it out to the IOLoop, ensure its exception (if any)
                # gets logged too.
                try:
                    ret = gen.convert_yielded(ret)
                except gen.BadYieldError:
                    # It's not unusual for add_callback to be used with
                    # methods returning a non-None and non-yieldable
                    # result, which should just be ignored.
                    pass
                else:
                    self.add_future(ret, lambda f: f.result())
        except Exception:
            self.handle_callback_exception(callback)
```

This causes another call to `_run_callback` on the next tick, which retrieves `{}` from the new `multi_future`, and the cycle repeats.


Resolving patch:
diff --git a/tornado/ioloop.py b/tornado/ioloop.py
--- a/tornado/ioloop.py
+++ b/tornado/ioloop.py
@@ -616,10 +616,14 @@ def _run_callback(self, callback):
                     # result, which should just be ignored.
                     pass
                 else:
-                    self.add_future(ret, lambda f: f.result())
+                    self.add_future(ret, self._discard_future_result)
         except Exception:
             self.handle_callback_exception(callback)
 
+    def _discard_future_result(self, future):
+        """Avoid unhandled-exception warnings from spawned coroutines."""
+        future.result()
+
     def handle_callback_exception(self, callback):
         """This method is called whenever a callback run by the `IOLoop`
         throws an exception.


Test patch:
diff --git a/tornado/test/ioloop_test.py b/tornado/test/ioloop_test.py
--- a/tornado/test/ioloop_test.py
+++ b/tornado/test/ioloop_test.py
@@ -9,6 +9,7 @@
 import sys
 import threading
 import time
+import types
 
 from tornado import gen
 from tornado.ioloop import IOLoop, TimeoutError, PollIOLoop, PeriodicCallback
@@ -61,6 +62,25 @@ def sleep(self, t):
 
 
 class TestIOLoop(AsyncTestCase):
+    def test_add_callback_return_sequence(self):
+        # A callback returning {} or [] shouldn't spin the CPU, see Issue #1803.
+        self.calls = 0
+
+        loop = self.io_loop
+        test = self
+        old_add_callback = loop.add_callback
+
+        def add_callback(self, callback, *args, **kwargs):
+            test.calls += 1
+            old_add_callback(callback, *args, **kwargs)
+
+        loop.add_callback = types.MethodType(add_callback, loop)
+        loop.add_callback(lambda: {})
+        loop.add_callback(lambda: [])
+        loop.add_timeout(datetime.timedelta(milliseconds=50), loop.stop)
+        loop.start()
+        self.assertLess(self.calls, 10)
+
     @skipOnTravis
     def test_add_callback_wakeup(self):
         # Make sure that add_callback from inside a running IOLoop