Repository: tornadoweb/tornado

Issue:
process, tcpserver: Soft-deprecate multi-process mode
Multi-process mode has a lot of subtleties; it is responsible for a large fraction of our questions on stack overflow. And now that we have SO_REUSEPORT, I see little reason to use it instead of multiple independent processes (I've always viewed multiple independent processes as desirable for "serious" production use, but the need for an external load balancer is a significant operational overhead that justifies the shared-listener multiprocess mode. But if you can share a port without fork and its associates subtlety, that's even better)

We should at least update the docs to steer people towards reusing ports instead of forking multiple-processes. We might want to go as far as deprecating the process argument to `server.start()`, although I'm in no rush to actually remove it (I don't think I'd deprecate the `process.fork_processes()` function - empirically the people who get in trouble with multi-process mode are all using `start(N)`; explicit forking is much more explicit and either the people who try it are able to get it working without difficulty or no one's trying it). 


Discussion:
I'd love to see an example of how `SO_REUSEPORT` can be used to replace `start(N)`.
`SO_REUSEPORT` on its own doesn't do it. But what you can do is replace your calls to `bind(options.port); start(N)` with `listen(options.port, reuse_port=True)` and then run N copies of the same process. 
Can you make an example using the ```reuse_port```?   Hot to run the following code with ```reuse_port``` ?

```
import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
    ])

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()
```
Just add `reuse_port=True` to the `app.listen` call, and run multiple copies of the script. 
oh I see. I wanted that parent process (main application) starts with `reuse_port` and then child process (tensorboard) starts many visualizations on same port, with different context, i.e `localhost:port/tensorboard/path1`,  `localhost:port/tensorboard/path2`, `localhost:port/tensorboard/path3`
But I think that this doesnt work like this

`reuse_port` is for when all the instances behind the port serve exactly the same application. To run different applications in different processes that are distinguished by path, you need a layer 7 proxy like haproxy or nginx to route the requests accordingly. 
Is it ok if we install a SIGTERM handler in the parent process to explicitly kill the child process on exit? We are seeing this problem #1912 in our space and looking for possible solutions. Want to ensure this wouldn't cause leaks or other possible conflicts
Installing a SIGTERM handler in the parent process won't hurt anything, but it's an imperfect solution to orphaned processes as seen in #1912. SIGTERM is not the only reason a process can exit; you could also add handlers for SIGINT and others but you can't have a handler for SIGKILL. A complete solution would need to be in the child processes, either polling `getppid` or (linux-specific) using `pidfd`. But if you're mainly concerned about SIGTERM to the parent process, then a SIGTERM handler could work well enough for you. 

Resolving patch:
diff --git a/tornado/httpserver.py b/tornado/httpserver.py
--- a/tornado/httpserver.py
+++ b/tornado/httpserver.py
@@ -84,46 +84,53 @@ class HTTPServer(TCPServer, Configurable, httputil.HTTPServerConnectionDelegate)
     `HTTPServer` initialization follows one of three patterns (the
     initialization methods are defined on `tornado.tcpserver.TCPServer`):
 
-    .. note::
+    1. `~tornado.tcpserver.TCPServer.listen`: single-process::
 
-       The multi-process examples here produce deprecation warnings in
-       Python 3.10; updated guidance is still in development.
+            async def main():
+                server = HTTPServer()
+                server.listen(8888)
+                await asyncio.Event.wait()
 
-    1. `~tornado.tcpserver.TCPServer.listen`: simple single-process::
-
-            server = HTTPServer(app)
-            server.listen(8888)
-            IOLoop.current().start()
+            asyncio.run(main())
 
        In many cases, `tornado.web.Application.listen` can be used to avoid
        the need to explicitly create the `HTTPServer`.
 
-    2. `~tornado.tcpserver.TCPServer.bind`/`~tornado.tcpserver.TCPServer.start`:
-       simple multi-process::
+       While this example does not create multiple processes on its own, when
+       the ``reuse_port=True`` argument is passed to ``listen()`` you can run
+       the program multiple times to create a multi-process service.
 
-            server = HTTPServer(app)
-            server.bind(8888)
-            server.start(0)  # Forks multiple sub-processes
-            IOLoop.current().start()
+    2. `~tornado.tcpserver.TCPServer.add_sockets`: multi-process::
 
-       When using this interface, an `.IOLoop` must *not* be passed
-       to the `HTTPServer` constructor.  `~.TCPServer.start` will always start
-       the server on the default singleton `.IOLoop`.
+            sockets = bind_sockets(8888)
+            tornado.process.fork_processes(0)
+            async def post_fork_main():
+                server = HTTPServer()
+                server.add_sockets(sockets)
+                await asyncio.Event().wait()
+            asyncio.run(post_fork_main())
 
-    3. `~tornado.tcpserver.TCPServer.add_sockets`: advanced multi-process::
+       The ``add_sockets`` interface is more complicated, but it can be used with
+       `tornado.process.fork_processes` to run a multi-process service with all
+       worker processes forked from a single parent.  ``add_sockets`` can also be
+       used in single-process servers if you want to create your listening
+       sockets in some way other than `~tornado.netutil.bind_sockets`.
 
-            sockets = tornado.netutil.bind_sockets(8888)
-            tornado.process.fork_processes(0)
-            server = HTTPServer(app)
-            server.add_sockets(sockets)
+       Note that when using this pattern, nothing that touches the event loop
+       can be run before ``fork_processes``.
+
+    3. `~tornado.tcpserver.TCPServer.bind`/`~tornado.tcpserver.TCPServer.start`:
+       simple **deprecated** multi-process::
+
+            server = HTTPServer()
+            server.bind(8888)
+            server.start(0)  # Forks multiple sub-processes
             IOLoop.current().start()
 
-       The `~.TCPServer.add_sockets` interface is more complicated,
-       but it can be used with `tornado.process.fork_processes` to
-       give you more flexibility in when the fork happens.
-       `~.TCPServer.add_sockets` can also be used in single-process
-       servers if you want to create your listening sockets in some
-       way other than `tornado.netutil.bind_sockets`.
+       This pattern is deprecated because it requires interfaces in the
+       `asyncio` module that have been deprecated since Python 3.10. Support for
+       creating multiple processes in the ``start`` method will be removed in a
+       future version of Tornado.
 
     .. versionchanged:: 4.0
        Added ``decompress_request``, ``chunk_size``, ``max_header_size``,
diff --git a/tornado/tcpserver.py b/tornado/tcpserver.py
--- a/tornado/tcpserver.py
+++ b/tornado/tcpserver.py
@@ -48,14 +48,13 @@ class TCPServer(object):
 
       from tornado.tcpserver import TCPServer
       from tornado.iostream import StreamClosedError
-      from tornado import gen
 
       class EchoServer(TCPServer):
           async def handle_stream(self, stream, address):
               while True:
                   try:
-                      data = await stream.read_until(b"\n")
-                      await stream.write(data)
+                      data = await stream.read_until(b"\n") await
+                      stream.write(data)
                   except StreamClosedError:
                       break
 
@@ -71,37 +70,49 @@ async def handle_stream(self, stream, address):
 
     `TCPServer` initialization follows one of three patterns:
 
-    1. `listen`: simple single-process::
+    1. `listen`: single-process::
 
-            server = TCPServer()
-            server.listen(8888)
-            IOLoop.current().start()
+            async def main():
+                server = TCPServer()
+                server.listen(8888)
+                await asyncio.Event.wait()
 
-    2. `bind`/`start`: simple multi-process::
-
-            server = TCPServer()
-            server.bind(8888)
-            server.start(0)  # Forks multiple sub-processes
-            IOLoop.current().start()
+            asyncio.run(main())
 
-       When using this interface, an `.IOLoop` must *not* be passed
-       to the `TCPServer` constructor.  `start` will always start
-       the server on the default singleton `.IOLoop`.
+       While this example does not create multiple processes on its own, when
+       the ``reuse_port=True`` argument is passed to ``listen()`` you can run
+       the program multiple times to create a multi-process service.
 
-    3. `add_sockets`: advanced multi-process::
+    2. `add_sockets`: multi-process::
 
             sockets = bind_sockets(8888)
             tornado.process.fork_processes(0)
+            async def post_fork_main():
+                server = TCPServer()
+                server.add_sockets(sockets)
+                await asyncio.Event().wait()
+            asyncio.run(post_fork_main())
+
+       The `add_sockets` interface is more complicated, but it can be used with
+       `tornado.process.fork_processes` to run a multi-process service with all
+       worker processes forked from a single parent.  `add_sockets` can also be
+       used in single-process servers if you want to create your listening
+       sockets in some way other than `~tornado.netutil.bind_sockets`.
+
+       Note that when using this pattern, nothing that touches the event loop
+       can be run before ``fork_processes``.
+
+    3. `bind`/`start`: simple **deprecated** multi-process::
+
             server = TCPServer()
-            server.add_sockets(sockets)
+            server.bind(8888)
+            server.start(0)  # Forks multiple sub-processes
             IOLoop.current().start()
 
-       The `add_sockets` interface is more complicated, but it can be
-       used with `tornado.process.fork_processes` to give you more
-       flexibility in when the fork happens.  `add_sockets` can
-       also be used in single-process servers if you want to create
-       your listening sockets in some way other than
-       `~tornado.netutil.bind_sockets`.
+       This pattern is deprecated because it requires interfaces in the
+       `asyncio` module that have been deprecated since Python 3.10. Support for
+       creating multiple processes in the ``start`` method will be removed in a
+       future version of Tornado.
 
     .. versionadded:: 3.1
        The ``max_buffer_size`` argument.
@@ -232,6 +243,12 @@ def bind(
 
         .. versionchanged:: 6.2
            Added the ``flags`` argument to match `.bind_sockets`.
+
+        .. deprecated:: 6.2
+           Use either ``listen()`` or ``add_sockets()`` instead of ``bind()``
+           and ``start()``. The ``bind()/start()`` pattern depends on
+           interfaces that have been deprecated in Python 3.10 and will be
+           removed in future versions of Python.
         """
         sockets = bind_sockets(
             port,
@@ -275,6 +292,12 @@ def start(
         .. versionchanged:: 6.0
 
            Added ``max_restarts`` argument.
+
+        .. deprecated:: 6.2
+           Use either ``listen()`` or ``add_sockets()`` instead of ``bind()``
+           and ``start()``. The ``bind()/start()`` pattern depends on
+           interfaces that have been deprecated in Python 3.10 and will be
+           removed in future versions of Python.
         """
         assert not self._started
         self._started = True


Test patch:
diff --git a/tornado/test/tcpserver_test.py b/tornado/test/tcpserver_test.py
--- a/tornado/test/tcpserver_test.py
+++ b/tornado/test/tcpserver_test.py
@@ -11,6 +11,8 @@
 from tornado.test.util import skipIfNonUnix
 from tornado.testing import AsyncTestCase, ExpectLog, bind_unused_port, gen_test
 
+from typing import Tuple
+
 
 class TCPServerTest(AsyncTestCase):
     @gen_test
@@ -121,46 +123,52 @@ class TestMultiprocess(unittest.TestCase):
     # processes, each of which prints its task id to stdout (a single
     # byte, so we don't have to worry about atomicity of the shared
     # stdout stream) and then exits.
-    def run_subproc(self, code: str) -> str:
+    def run_subproc(self, code: str) -> Tuple[str, str]:
         try:
             result = subprocess.run(
-                sys.executable,
-                stdout=subprocess.PIPE,
-                stderr=subprocess.STDOUT,
+                [sys.executable, "-Werror::DeprecationWarning"],
+                capture_output=True,
                 input=code,
                 encoding="utf8",
                 check=True,
             )
         except subprocess.CalledProcessError as e:
             raise RuntimeError(
-                f"Process returned {e.returncode} output={e.stdout}"
+                f"Process returned {e.returncode} stdout={e.stdout} stderr={e.stderr}"
             ) from e
-        return result.stdout
+        return result.stdout, result.stderr
 
-    def test_single(self):
+    def test_listen_single(self):
         # As a sanity check, run the single-process version through this test
         # harness too.
         code = textwrap.dedent(
             """
-            from tornado.ioloop import IOLoop
+            import asyncio
             from tornado.tcpserver import TCPServer
 
-            server = TCPServer()
-            server.listen(0, address='127.0.0.1')
-            IOLoop.current().run_sync(lambda: None)
+            async def main():
+                server = TCPServer()
+                server.listen(0, address='127.0.0.1')
+
+            asyncio.run(main())
             print('012', end='')
         """
         )
-        out = self.run_subproc(code)
+        out, err = self.run_subproc(code)
         self.assertEqual("".join(sorted(out)), "012")
+        self.assertEqual(err, "")
 
-    def test_simple(self):
+    def test_bind_start(self):
         code = textwrap.dedent(
             """
+            import warnings
+
             from tornado.ioloop import IOLoop
             from tornado.process import task_id
             from tornado.tcpserver import TCPServer
 
+            warnings.simplefilter("ignore", DeprecationWarning)
+
             server = TCPServer()
             server.bind(0, address='127.0.0.1')
             server.start(3)
@@ -168,13 +176,14 @@ def test_simple(self):
             print(task_id(), end='')
         """
         )
-        out = self.run_subproc(code)
+        out, err = self.run_subproc(code)
         self.assertEqual("".join(sorted(out)), "012")
+        self.assertEqual(err, "")
 
-    def test_advanced(self):
+    def test_add_sockets(self):
         code = textwrap.dedent(
             """
-            from tornado.ioloop import IOLoop
+            import asyncio
             from tornado.netutil import bind_sockets
             from tornado.process import fork_processes, task_id
             from tornado.ioloop import IOLoop
@@ -182,20 +191,22 @@ def test_advanced(self):
 
             sockets = bind_sockets(0, address='127.0.0.1')
             fork_processes(3)
-            server = TCPServer()
-            server.add_sockets(sockets)
-            IOLoop.current().run_sync(lambda: None)
+            async def post_fork_main():
+                server = TCPServer()
+                server.add_sockets(sockets)
+            asyncio.run(post_fork_main())
             print(task_id(), end='')
         """
         )
-        out = self.run_subproc(code)
+        out, err = self.run_subproc(code)
         self.assertEqual("".join(sorted(out)), "012")
+        self.assertEqual(err, "")
 
-    def test_reuse_port(self):
+    def test_listen_multi_reuse_port(self):
         code = textwrap.dedent(
             """
+            import asyncio
             import socket
-            from tornado.ioloop import IOLoop
             from tornado.netutil import bind_sockets
             from tornado.process import task_id, fork_processes
             from tornado.tcpserver import TCPServer
@@ -206,11 +217,14 @@ def test_reuse_port(self):
             port = sock.getsockname()[1]
 
             fork_processes(3)
-            server = TCPServer()
-            server.listen(port, address='127.0.0.1', reuse_port=True)
-            IOLoop.current().run_sync(lambda: None)
+
+            async def main():
+                server = TCPServer()
+                server.listen(port, address='127.0.0.1', reuse_port=True)
+            asyncio.run(main())
             print(task_id(), end='')
             """
         )
-        out = self.run_subproc(code)
+        out, err = self.run_subproc(code)
         self.assertEqual("".join(sorted(out)), "012")
+        self.assertEqual(err, "")