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
`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``,
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
`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.
.. 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,
.. 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
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
# 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)
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
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
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, "")