Fastapi: [BUG] BackgroundTasks not working properly

Created on 22 Apr 2020  路  16Comments  路  Source: tiangolo/fastapi

Describe the bug

The request remains open/active until the background task finishes running, making the purpose of BackgroundTasks kind of useless.
On the snippet below the request takes about 10 seconds to complete.
I followed the documentation guide but it didnt seem to work.

To Reproduce

async def background_task(text: str, seconds: int):
    import time

    time.sleep(seconds)
    print(text)

@app.get("/run", status_code=status.HTTP_200_OK)
async def test(background_tasks: BackgroundTasks, text: str = "Test", secs: int = 10):
    background_tasks.add_task(background_task, text, secs)
    return {"text": text, "secs": secs}

Screenshot

Screenshot from 2020-04-22 17-16-17

Expected behavior

The request to finish right away and the task to run in background.

Environment

  • OS: Linux Manjaro 19
  • FastAPI Version 0.54.1
  • Python version 3.8.2
answered question

Most helpful comment

@tiangolo could this be reopened - while this is an upstream bug, it is not really resolved

All 16 comments

I'm not 100% sure about this but here's a few guesses:

  1. Have you tried to run it with await asyncio.sleep(seconds) instead of the time.sleep function? time.sleep is synchronous as far as I know and since background tasks that are defined as "async" just use the event loop as their "background engine", using a synchronous method in that background task will still globally block the process.

  2. Alternatively, you can try removing the "async" from def background_task. In that case the task should run in a thread pool instead which would then also not block.

Sorry but how do you run the app? Uvicorn? Could you post a self-contained app that can run?

Sorry but how do you run the app? Uvicorn? Could you post a self-contained app that can run?

Im running with uvicorn using this code as entry

if __name__ == "__main__":
    import uvicorn

    uvicorn.run("app:app", host="0.0.0.0", port=5000, debug=True, reload=True)

I'm not 100% sure about this but here's a few guesses:

1. Have you tried to run it with `await asyncio.sleep(seconds)` instead of the `time.sleep` function?  `time.sleep` is synchronous as far as I know and since background tasks that are defined as "async" just use the event loop as their "background engine", using a synchronous method in that background task will still globally block the process.

2. Alternatively, you can try removing the "async" from `def background_task`.  In that case the task should run in a thread pool instead which would then also not block.

The code above I wrote just to simplify my problem. Removing async in this example seemed to work. Im going to try this with my real code. Thanks

@soares7vinicius just fyi this: https://github.com/encode/starlette/issues/436 explains your problem es well. The tldr:

  • async function background task => runs in asyncio loop (everything that doesn't use "await" will block)
  • normal function background task => runs in a threadpool (and will thus not block in this case)

@daviskirk

Seems I have similar issue, tried to narrow down to middleware.
You can run this snippet and requests are getting blocked when middleware is added.

On purpose, I added middleware which does nothing and it is visible that there is no blocking code block.

If you try to comment app.add_middleware(DoNothingMiddleware) then behaviour is as expected.

Might be some issue there, can this be a bug or is it a limitation ?

import asyncio

from fastapi import BackgroundTasks, FastAPI
from starlette import status
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.requests import Request
from starlette.responses import Response


class DoNothingMiddleware(BaseHTTPMiddleware):
    async def dispatch(
        self, request: Request, call_next: RequestResponseEndpoint
    ) -> Response:
        response = await call_next(request)
        return response


async def background_task(text: str, seconds: int):
    print(f"Task {text} started")
    await asyncio.sleep(seconds)
    print(f"Task {text} ended")


app = FastAPI(debug=True)
# if you add this middleware then requests are getting blocked
app.add_middleware(DoNothingMiddleware)


@app.get("/run", status_code=status.HTTP_200_OK)
async def test(background_tasks: BackgroundTasks, text: str = "Test", secs: int = 10):
    background_tasks.add_task(background_task, text, secs)
    return {"text": text, "secs": secs}


if __name__ == "__main__":
    import uvicorn

    uvicorn.run("main:app", host="0.0.0.0", port=8000, debug=True, reload=True)

@demonno This is a bug in Starlette - see encode/starlette#919

Thanks for the help here everyone! :clap: :bow:

@soares7vinicius yeah, as @daviskirk says, you shouldn't use time.sleep() in a function with async def. When in doubt, just use def everywhere.

Check more info about all that in the docs: https://fastapi.tiangolo.com/async/

I got same problem.
This code sends requests to the specified links. and it is executed so that the client expects a result. code does not execute in the background.

background_tasks.add_task(await Request.package_send(urls=urls*100, callback=callback)

from pydantic import BaseModel

from services.core.models import TaskStatement

class Task(BaseModel):
    id: uuid.UUID

def with_client_session(coro):
    async def wrapper(
        method: str, url: str, data: dict = None,
        headers: dict = None, raw: bool = None
    ):
        session = ClientSession(loop=loop)
        result = None

        if method == 'GET':
            result = await coro(
                method=session.get, url=url, data=data,
                headers=headers, raw=raw)

        elif method == 'POST':
            result = await coro(
                method=session.post, url=url, data=data,
                headers=headers, raw=raw)

        elif method == 'PUT':
            result = await coro(
                method=session.put, url=url, data=data,
                headers=headers, raw=raw)

        elif method == 'DELETE':
            result = await coro(
                method=session.delete, url=url, data=data,
                headers=headers, raw=raw)

        elif method == 'PATCH':
            result = await coro(
                method=session.patch, url=url, data=data,
                headers=headers, raw=raw)

        await session.close()
        return result

    return wrapper

class Request:

    @staticmethod
    @with_client_session
    async def send(method, url: str, data: list = None, headers: dict = None, raw: bool = None):
        response: ClientResponse = await method(
            url=url, data=json.dumps(data), headers=headers, ssl=ssl_context)

        if raw:
            return response, response.status, await response.text()

        try:
            return await response.json(), response.status
        except:
            return await response.text(), response.status

   @staticmethod
    async def package_send(urls, callback):
        parent_task = await TaskStatement.create(
            uuid=uuid.UUID(uuid.uuid4().hex),
        )
        async for url in generator(urls):
            future: asyncio.Future = asyncio.ensure_future(Request.send(
                method=url.method, url=url.url, data=url.data, headers=url.headers, raw=True
            ))

            child = await TaskStatement.create(
                uuid=uuid.UUID(uuid.uuid4().hex),
                parent=parent_task
            )

            await parent_task.children.add(child)
            await parent_task.save()

            future.self = child
            future.parent = parent_task

            future.add_done_callback(callback)
            future: asyncio.Task

        return Task(id=parent_task.uuid)


I'm not 100% sure about this but here's a few guesses:

1. Have you tried to run it with `await asyncio.sleep(seconds)` instead of the `time.sleep` function?  `time.sleep` is synchronous as far as I know and since background tasks that are defined as "async" just use the event loop as their "background engine", using a synchronous method in that background task will still globally block the process.

2. Alternatively, you can try removing the "async" from `def background_task`.  In that case the task should run in a thread pool instead which would then also not block.

The code above I wrote just to simplify my problem. Removing async in this example seemed to work. Im going to try this with my real code. Thanks

if you removed async, then it turns out that your code is synchronous. unless tasks will block each other if they are performed without event loop?

@codefather-labs I'm not sure I understand your code, it seems you are adding @staticmethod to a custom Request, that will probably override the normal Request.

Check the docs for background tasks: https://fastapi.tiangolo.com/tutorial/background-tasks/
And for async: https://fastapi.tiangolo.com/async/

@codefather-labs I'm not sure I understand your code, it seems you are adding @staticmethod to a custom Request, that will probably override the normal Request.

Check the docs for background tasks: https://fastapi.tiangolo.com/tutorial/background-tasks/
And for async: https://fastapi.tiangolo.com/async/

i can't fetch coroutine in background task wright? on function can?

@codefather-labs please create a new issue with a minimal, self-contained, example, that will let me (us) help you better by being able to check what you are trying to achieve and might be going on.

Assuming the original issue was solved, it will be automatically closed now. But feel free to add more comments or create new issues.

@tiangolo could this be reopened - while this is an upstream bug, it is not really resolved

i have same issue and if i add

await asyncio.sleep(1)
inside any method that is triggered by task, it is working fine. I don't understand how it can work but it is working.
even i put time.sleep(30) after asyncio.sleep, i get result immediately but process finish 30sec later which is correct.

Was this page helpful?
0 / 5 - 0 ratings