]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
tcpserver: Deprecate bind/start multi-process 3149/head
authorBen Darnell <ben@bendarnell.com>
Fri, 3 Jun 2022 19:47:39 +0000 (15:47 -0400)
committerBen Darnell <ben@bendarnell.com>
Sat, 4 Jun 2022 02:12:13 +0000 (22:12 -0400)
This is partially a casualty of the Python 3.10 deprecation
changes, although it's also something I've wanted to do for other
reasons, since it's been a very common source of user confusion.

Fixes #2801

docs/guide/running.rst
tornado/httpserver.py
tornado/tcpserver.py
tornado/test/tcpserver_test.py

index 47db460ad9911de4f32ea76e82e207ba39e0bf5a..566865524453e275a7f5b465dfd0a182a14536ad 100644 (file)
@@ -35,33 +35,28 @@ Due to the Python GIL (Global Interpreter Lock), it is necessary to run
 multiple Python processes to take full advantage of multi-CPU machines.
 Typically it is best to run one process per CPU.
 
-.. note::
+The simplest way to do this is to add ``reuse_port=True`` to your ``listen()``
+calls and then simply run multiple copies of your application.
 
-   This section is somewhat out of date; the built-in multi-process mode
-   produces deprecation warnings on Python 3.10 (in addition to its other
-   limitations). Updated guidance is still in development; tentative
-   recommendations include running independent processes as described
-   in the paragraph beginning "For more sophisticated deployments", or
-   using ``SO_REUSEPORT`` instead of forking. 
-
-Tornado includes a built-in multi-process mode to start several
-processes at once (note that multi-process mode does not work on
-Windows). This requires a slight alteration to the standard main
-function:
+Tornado also has the ability to start mulitple processes from a single parent
+process (note that this does not work on Windows). This requires some
+alterations to application startup.
 
 .. testcode::
 
     def main():
-        app = make_app()
-        server = tornado.httpserver.HTTPServer(app)
-        server.bind(8888)
-        server.start(0)  # forks one process per cpu
-        IOLoop.current().start()
+        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())
 
 .. testoutput::
    :hide:
 
-This is the easiest way to start multiple processes and have them all
+This is another way to start multiple processes and have them all
 share the same port, although it has some limitations.  First, each
 child process will have its own ``IOLoop``, so it is important that
 nothing touches the global ``IOLoop`` instance (even indirectly) before the
index b3e034b9ef3927d685af564216356b8ec6e11cd9..77dc541e9de7c58afe426acf71705e7958620d14 100644 (file)
@@ -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``,
index 9065b62e89932fe768626982ee630dc659027228..183aac21777d4d3fff7b2430061ecacdca086d17 100644 (file)
@@ -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 @@ class TCPServer(object):
 
     `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 @@ class TCPServer(object):
 
         .. 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 @@ class TCPServer(object):
         .. 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
index 8b59db577efa41d93df22b1927a6e4ad0260e079..c636c8586fb48c7ecd4b851cb5e0eaec937a8856 100644 (file)
@@ -11,6 +11,8 @@ from tornado.tcpserver import TCPServer
 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 @@ class TestMultiprocess(unittest.TestCase):
             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 @@ class TestMultiprocess(unittest.TestCase):
 
             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 @@ class TestMultiprocess(unittest.TestCase):
             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, "")