Repository: tornadoweb/tornado

Issue:
Check _events and _handlers before start()
```
Traceback (most recent call last):
    File "/usr/local/lib/python2.7/site-packages/tornado/ioloop.py", line 887, in start
     fd_obj, handler_func = self._handlers[fd]
     KeyError: 16
```

https://github.com/tornadoweb/tornado/blob/master/tornado/ioloop.py#L883

I ran into this issue when testing using multiple processes and threads.  We sandbox our test processes using multiprocessing and were getting this cryptic error.

I found a blog post @toumorokoshi wrote going over his own experiences with this issue: http://y.tsutsumi.io/keyerror-in-self_handlers-a-journey-deep-into-tornados-internals.html

At the very least it would be nice to have some documentation suggesting that you run IOLoop.clear_instance() and IOLoop.clear_current() after a fork()

Discussion:

Patch:
diff --git a/tornado/ioloop.py b/tornado/ioloop.py
index ad35787f..a1288e77 100644
--- a/tornado/ioloop.py
+++ b/tornado/ioloop.py
@@ -121,6 +121,16 @@ class IOLoop(Configurable):
     current instance. If ``make_current=False``, the new `IOLoop` will
     not try to become current.
 
+    In general, an `IOLoop` cannot survive a fork or be shared across
+    processes in any way. When multiple processes are being used, each
+    process should create its own `IOLoop`, which also implies that
+    any objects which depend on the `IOLoop` (such as
+    `.AsyncHTTPClient`) must also be created in the child processes.
+    As a guideline, anything that starts processes (including the
+    `tornado.process` and `multiprocessing` modules) should do so as
+    early as possible, ideally the first thing the application does
+    after loading its configuration in ``main()``.
+
     .. versionchanged:: 4.2
        Added the ``make_current`` keyword argument to the `IOLoop`
        constructor.
@@ -701,6 +711,7 @@ class PollIOLoop(IOLoop):
         self._stopped = False
         self._closing = False
         self._thread_ident = None
+        self._pid = os.getpid()
         self._blocking_signal_threshold = None
         self._timeout_counter = itertools.count()
 
@@ -753,6 +764,8 @@ class PollIOLoop(IOLoop):
     def start(self):
         if self._running:
             raise RuntimeError("IOLoop is already running")
+        if os.getpid() != self._pid:
+            raise RuntimeError("Cannot share PollIOLoops across processes")
         self._setup_logging()
         if self._stopped:
             self._stopped = False
diff --git a/tornado/web.py b/tornado/web.py
index 132f42dd..d79889fa 100644
--- a/tornado/web.py
+++ b/tornado/web.py
@@ -319,7 +319,10 @@ class RequestHandler(object):
         if reason is not None:
             self._reason = escape.native_str(reason)
         else:
-            self._reason = httputil.responses.get(status_code, "Unknown")
+            try:
+                self._reason = httputil.responses[status_code]
+            except KeyError:
+                raise ValueError("unknown status code %d" % status_code)
 
     def get_status(self):
         """Returns the status code for our response."""
@@ -1558,7 +1561,11 @@ class RequestHandler(object):
             # send a response.
             return
         if isinstance(e, HTTPError):
-            self.send_error(e.status_code, exc_info=sys.exc_info())
+            if e.status_code not in httputil.responses and not e.reason:
+                gen_log.error("Bad HTTP status code: %d", e.status_code)
+                self.send_error(500, exc_info=sys.exc_info())
+            else:
+                self.send_error(e.status_code, exc_info=sys.exc_info())
         else:
             self.send_error(500, exc_info=sys.exc_info())
 


Test patch:
diff --git a/tornado/test/web_test.py b/tornado/test/web_test.py
index de26c42e..d79ea52c 100644
--- a/tornado/test/web_test.py
+++ b/tornado/test/web_test.py
@@ -917,10 +917,6 @@ class ErrorResponseTest(WebTestCase):
             self.assertEqual(response.code, 503)
             self.assertTrue(b"503: Service Unavailable" in response.body)
 
-            response = self.fetch("/default?status=435")
-            self.assertEqual(response.code, 435)
-            self.assertTrue(b"435: Unknown" in response.body)
-
     def test_write_error(self):
         with ExpectLog(app_log, "Uncaught exception"):
             response = self.fetch("/write_error")
@@ -1493,9 +1489,9 @@ class StatusReasonTest(SimpleHandlerTestCase):
         response = self.fetch("/?code=682&reason=Bar")
         self.assertEqual(response.code, 682)
         self.assertEqual(response.reason, "Bar")
-        response = self.fetch("/?code=682")
-        self.assertEqual(response.code, 682)
-        self.assertEqual(response.reason, "Unknown")
+        with ExpectLog(app_log, 'Uncaught exception'):
+            response = self.fetch("/?code=682")
+        self.assertEqual(response.code, 500)
 
 
 @wsgi_safe