Repository navigation
Worker processes log nothing under the forkserver start method (Python 3.14 default on Linux) #685
Description
Summary
On Python 3.14, multiprocessing on Linux defaults to the forkserver start method instead of
fork. Under forkserver, taskiq worker subprocesses keep working, but none of their log
records are emitted: no Listening started., no Importing tasks from module …, no shutdown
messages. Warnings and errors still reach stderr through Python's last-resort handler, so the
problem is easy to miss. It breaks any readiness check that waits for Listening started..
Cause
start_listen() in taskiq/cli/worker/run.py reconfigures logging in the child only for spawn
(line 89 on master at 8a1b04d):
if args.configure_logging and get_start_method() == "spawn":
logging.basicConfig(level=args.log_level, format=args.log_format)A fork child inherits the parent's logging.basicConfig. A forkserver child starts from a
fresh interpreter, like a spawn child, so it inherits nothing and ends up with no handler at
all.
Reproduction
taskiq 0.12.6, Python 3.14.7, Linux x86_64. No external services are needed.
repro_broker.py:
import asyncio
from collections.abc import AsyncGenerator
from taskiq import AsyncBroker, BrokerMessage
class IdleBroker(AsyncBroker):
"""Minimal broker: receives nothing, just keeps the worker listening."""
async def kick(self, message: BrokerMessage) -> None:
pass
async def listen(self) -> AsyncGenerator[bytes, None]:
# print(), not logging: proves the worker process is alive and listening.
print("IdleBroker.listen() reached", flush=True)
while True:
await asyncio.sleep(3600)
yield b""
broker = IdleBroker()run_cli.py runs the CLI after optionally forcing a start method:
import multiprocessing, sys
if __name__ == "__main__":
method = sys.argv.pop(1)
if method != "default":
multiprocessing.set_start_method(method)
from taskiq.__main__ import main
sys.argv = ["taskiq", "worker", "repro_broker:broker", "--workers", "1", "--log-level", "INFO"]
main()for m in default fork forkserver spawn; do
echo "== $m"; timeout -s INT 5 python run_cli.py $m 2>&1 | grep -E "worker-0\]|IdleBroker"
doneResult
| Start method | listen() reached |
Log records from worker-0 |
|---|---|---|
fork |
yes | yes (Listening started., shutdown messages) |
spawn |
yes | yes |
forkserver |
yes | none |
default on Python 3.14 / Linux (forkserver) |
yes | none |
The plain taskiq worker repro_broker:broker --workers 1 command behaves like the "default" row on
Python 3.14.
Suggested fix
Configure logging in the child for every start method except fork:
if args.configure_logging and get_start_method() != "fork":The child already receives args through kwargs={"args": self.args}, so --log-level,
--log-format and --no-configure-logging keep working.
Workaround
Until this is fixed, we call logging.basicConfig at import time of the module that holds the
broker. Every worker child imports that module to resolve module:broker. The call is guarded by
multiprocessing.parent_process() is not None and
multiprocessing.get_start_method() == "forkserver".
Activity