Skip to content

Commit

Permalink
tcpserver: Deprecate bind/start multi-process
Browse files Browse the repository at this point in the history
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 tornadoweb#2801
  • Loading branch information
bdarnell committed Jun 3, 2022
1 parent 8ebbfea commit 878a36b
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 49 deletions.
31 changes: 13 additions & 18 deletions docs/guide/running.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
79 changes: 53 additions & 26 deletions tornado/tcpserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,15 @@ class TCPServer(object):
To use `TCPServer`, define a subclass which overrides the `handle_stream`
method. For example, a simple echo server could be defined like this::
from tornado.tcpserver import TCPServer
from tornado.iostream import StreamClosedError
from tornado import gen
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
Expand All @@ -71,37 +70,53 @@ 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():
server = TCPServer()
server.add_sockets(sockets)
await asyncio.Event().wait()
asyncio.run(post_fork())
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`.
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`.
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.
Expand Down Expand Up @@ -232,6 +247,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,
Expand Down Expand Up @@ -275,6 +296,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
Expand Down
10 changes: 5 additions & 5 deletions tornado/test/tcpserver_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ class TestMultiprocess(unittest.TestCase):
def run_subproc(self, code: str) -> str:
try:
result = subprocess.run(
sys.executable,
[sys.executable, "-Wd"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
input=code,
Expand All @@ -137,7 +137,7 @@ def run_subproc(self, code: str) -> str:
) from e
return result.stdout

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(
Expand All @@ -154,7 +154,7 @@ def test_single(self):
out = self.run_subproc(code)
self.assertEqual("".join(sorted(out)), "012")

def test_simple(self):
def test_bind_start(self):
code = textwrap.dedent(
"""
from tornado.ioloop import IOLoop
Expand All @@ -171,7 +171,7 @@ def test_simple(self):
out = self.run_subproc(code)
self.assertEqual("".join(sorted(out)), "012")

def test_advanced(self):
def test_add_sockets(self):
code = textwrap.dedent(
"""
from tornado.ioloop import IOLoop
Expand All @@ -191,7 +191,7 @@ def test_advanced(self):
out = self.run_subproc(code)
self.assertEqual("".join(sorted(out)), "012")

def test_reuse_port(self):
def test_listen_multi_reuse_port(self):
code = textwrap.dedent(
"""
import socket
Expand Down

0 comments on commit 878a36b

Please sign in to comment.