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:

Patch:
diff --git a/tornado/ioloop.py b/tornado/ioloop.py
index cadb4116..d6183176 100644
--- a/tornado/ioloop.py
+++ b/tornado/ioloop.py
@@ -616,10 +616,14 @@ class IOLoop(Configurable):
                     # 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
index 8570e73f..1bb8ce08 100644
--- a/tornado/test/ioloop_test.py
+++ b/tornado/test/ioloop_test.py
@@ -9,6 +9,7 @@ import socket
 import sys
 import threading
 import time
+import types
 
 from tornado import gen
 from tornado.ioloop import IOLoop, TimeoutError, PollIOLoop, PeriodicCallback
@@ -61,6 +62,25 @@ class FakeTimeIOLoop(PollIOLoop):
 
 
 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