ets-labs / python-dependency-injector

Dependency injection framework for Python
https://python-dependency-injector.ets-labs.org/
BSD 3-Clause "New" or "Revised" License
3.89k stars 304 forks source link

Confusing AsyncResource closing Mechanism with FastAPI #719

Open rumbarum opened 1 year ago

rumbarum commented 1 year ago

Hello ets-labs, I felt really great on this morning for applying this awesome tool!

I almost had been finishing organizing dependency injection with fastapi during today, but there was a issue when closing AsyncResouces with on_shutdown of FastAPI.

I am on Mac with python 3.11.4, current version of installed D.I. is 4.41.0.

This works unexpectedly, remaining unclosed session.

from dependency_injector import providers, containers, resources, wiring
from aiohttp import ClientSession
import asyncio

from fastapi import FastAPI

class AsyncHttpClient(resources.AsyncResource):
    async def init(self) -> ClientSession:
        return ClientSession()

    async def shutdown(self, client_session: ClientSession | None):
        if client_session is not None:
            await client_session.close()

class Container(containers.DeclarativeContainer):
    wiring_config = containers.WiringConfiguration(
        modules=[__name__]
    )
    async_resource = providers.Resource(AsyncHttpClient)

async def main():
    container = Container()
    await container.init_resources()
    app = FastAPI(
        on_shutdown=[container.shutdown_resources],
    )

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

 >>>
Unclosed client session
client_session: <aiohttp.client.ClientSession object at xxxxxxx>

Below is working clean, remaining no unclosed session.

from dependency_injector import providers, containers, resources, wiring
from aiohttp import ClientSession
import asyncio

from fastapi import FastAPI

class AsyncHttpClient(resources.AsyncResource):
    async def init(self) -> ClientSession:
        return ClientSession()

    async def shutdown(self, client_session: ClientSession | None):
        if client_session is not None:
            await client_session.close()

class Container(containers.DeclarativeContainer):
    wiring_config = containers.WiringConfiguration(
        modules=[__name__]
    )
    async_resource = providers.Resource(AsyncHttpClient)

@wiring.inject
async def inner(client=wiring.Provide[Container.async_resource]):
    resp = await client.get("https://ipconfig.io/json")
    print(await resp.json())

async def main():
    container = Container()
    await container.init_resources()
    app = FastAPI(
        on_shutdown=[container.shutdown_resources],
    )

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

The only difference is existence of injected function. If injected function is, it succeed to closing and vice versa.(Even, I am not using it) I had tried making closing first for hours, so I could not find solution. Luckily, I tested another way and found the way.

Would you tell me any mechanism about this ?

rumbarum commented 1 year ago

This is not D.I. problem. This is about the way fast api's event loop, which is distinct from aiohttp clientsession.