sanic-org / sanic

Accelerate your web app development | Build fast. Run fast.

Home Page:https://sanic.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Infinite task using `loop.create_task` in `main_process_start` is hanging.

lxdlam opened this issue · comments

commented

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

I've using Sanic with discord.py to start both a discord bot and a Sanic server. I'm initializing the discord bot in a function like:

@app.main_process_start
async def start(app: Sanic, loop: AbstractEventLoop) -> None:
    loop.create_task(bot.start(CLIENT_TOKEN))

The bot.start call is a async function which is expected to enter a infinite loop. When I start my server through sanic main:app, the discord server will never go live, and after sending Ctrl-C, it seems the coroutine is stucked at making connection and prints a error message Fatal error on transport TCPTransport.

If I change the decorator into @app.before_server_start, it works with no issue, but I want to make only one global instance of the client, instead of making one each worker. Also, I think the behaviour is weird since I have digged into the code and seen nothing special.

Code snippet

No response

Expected Behavior

No response

How do you run Sanic?

Sanic CLI

Operating System

Fedora 38 - Linux 6.3

Sanic Version

v23.3.0

Additional context

No response

Can you please share the link to your discord bot's code so we can understand what it's trying to do on startup?

commented

Can you please share the link to your discord bot's code so we can understand what it's trying to do on startup?

Sure. It's the minimal snippet:

  • bot.py
from discord.ext import commands # discord.py, https://discordpy.readthedocs.io/en/stable/

class Bot(commands.Bot):
    def __init__(self):
        # ...

    async def on_ready(self):
        logger.info(f"Discord bot connected. Logged in as {self.user} ({self.user.id})")


bot = Bot()
  • routes.py
import sanic  # for typing stuff
from sanic import HTTPResponse

app = sanic.Sanic("sanic_server")


@app.route("/")
async def index(_request: sanic.Request) -> HTTPResponse:
    return HTTPResponse("<p>Hello World!</p>")
  • main.py
from os import environ

from sanic import Sanic
from asyncio import AbstractEventLoop
import routes
from bot import bot

CLIENT_TOKEN = environ.get("DISCORD_TOKEN")

app = Sanic.get_app("sanic_server")


@app.main_process_start
async def start(app: Sanic, loop: AbstractEventLoop) -> None:
    loop.create_task(bot.start(CLIENT_TOKEN))

I believe the main process doesn't run in async mode. This test case shows that the task is only executed for one round (until the await), thus this prints "Running..." only once. If used in server process, it prints the message each second.

import asyncio
from sanic import Sanic

app = Sanic("sanic_server")

async def worker():
    while True:
        print("Running...")
        await asyncio.sleep(1)

@app.main_process_start
async def start(app, loop) -> None:
    loop.create_task(worker())

@app.route("/")
async def test(request):
    ...

As a workaround, spawn a process from main_process_ready and join it in main_process_stop. If you need await, use asyncio.run within your process for your very own event loop. The main process is not quite designed to run extra tasks by itself.

commented

I see the reason and the workaround is acceptable. Thanks.

See also: #2775

I would not suggest running it in the main process. You should probably run it in its own process.

https://sanic.dev/en/guide/deployment/manager.html#running-custom-processes