Apache banchmark hangs on first request with ProcessPoolExecutor py 3.12 tornado 6.4.2

25 views
Skip to first unread message

Michal Seidl

unread,
May 2, 2025, 4:21:40 PMMay 2
to Tornado Web Server
Hello,
I am testing different executors for RequestHandler GET request on Python 3.11 and tornado 6.4.2.
  • classic sync  function
  • async function
  • IOLoop.current().run_in_executor(None, function,...) default executor
  • IOLoop.current().run_in_executor(thread_execuotr, function,...)
  • IOLoop.current().run_in_executor(process_executor, function,...)
All of them works ok with Apache benchmark 'ab' tool except 'process_executor'. After starting server, the first request return response but probably in some way the http conection is not closed properly so 'ab' hangs and finished with `apr_pollset_poll: The timeout specified has expired (70007)' error.

Any next requests are ok.

Here is my simple server code:

import asyncio
import time
import logging

from concurrent.futures import ProcessPoolExecutor

import tornado
from tornado.log import enable_pretty_logging
from tornado.ioloop import IOLoop


log_access = logging.getLogger('tornado.access')
enable_pretty_logging()

SLEEP_TIME = 0.25
MAX_WORKERS = 2


def sleep_sync_process_simple(sec: int) -> int:
    time.sleep(sec)
    return sec


class ProcessHandlerSimple(tornado.web.RequestHandler):
    async def get(self):
        log_access.info(f'START PROCESS POOL WITH STATE')
        r = await IOLoop.current().run_in_executor(self.application.process_executor, sleep_sync_process_simple, SLEEP_TIME)
        log_access.info(f'END PROCESS POOL WITH STATE {r}')
        self.write(f"Result: {r}\n")


async def check_state(state):
    progress = 0
    while state['status'] != 'F':
        if progress != state['progress']:
            log_access.info(f'CHANGED PROCESS WITH STATE {state}')
            progress = state['progress']
        await asyncio.sleep(0.5)


def make_app():
    return tornado.web.Application(
        [
            (r"/process_simple", ProcessHandlerSimple),
        ],
    )


async def main():
    log_access.info('Starting Tornado server')

    process_executor = ProcessPoolExecutor(max_workers=MAX_WORKERS)

    app = make_app()
    app.process_executor = process_executor
    app.listen(8889)
    shutdown_event = asyncio.Event()
    await shutdown_event.wait()

if __name__ == "__main__":
    asyncio.run(main())

Ben Darnell

unread,
May 5, 2025, 3:54:22 PMMay 5
to python-...@googlegroups.com
I'm unable to reproduce this (Python 3.11.11 on macos, run with `uv
run --with tornado -p 3.11 test.py`). Everything appears to work as
expected to me.

One way I've seen people get in trouble with ProcessPoolExecutor is if
they combine it with Tornado's multi-process mode (without taking care
to initialize things in the proper order) or try to use async
functions in the child process. But you're not doing either in this
example, so everything looks correct to me. (Note that you left a
check_state function here that is not used).

It's possible that creating the ProcessPoolExecutor from inside the
async main function is problematic because the asyncio event loop (and
its thread pool executor?) are already created. This is particularly
likely to be a problem when multiprocessing is run in fork mode (which
is the default on linux but not macos). If you're on linux, try
running `multiprocessing.set_start_method('spawn')` or 'forkserver' at
the start of the `__main__` block to see if that fixes things ('spawn'
is the default on macos; 'forkserver' is going to become the default
on linux in python 3.14. Both of them are safer than 'fork')

-Ben
> --
> You received this message because you are subscribed to the Google Groups "Tornado Web Server" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to python-tornad...@googlegroups.com.
> To view this discussion visit https://groups.google.com/d/msgid/python-tornado/08d588a5-29ae-4dc4-ab5f-387e153d2692n%40googlegroups.com.

Michal Seidl

unread,
May 8, 2025, 6:57:57 AMMay 8
to python-...@googlegroups.com
Hello,
you are absolutely right. I am on linux. Calling
multiprocessing.set_start_method('forkserver') or 'spawn' before
starting asyncio loop with asyncio.run(main()) looks like to solve the
problem.

Thanks very much Michal
Reply all
Reply to author
Forward
0 new messages