antonrh / anydi

Python Dependency Injection
https://anydi.readthedocs.io/
MIT License
34 stars 0 forks source link

[version 26.2] AnyDi preinitializes providers in request context even if they are not actually needed #96

Closed attilavetesi-epam closed 3 months ago

attilavetesi-epam commented 3 months ago

In the latest 26.2 version (probably already in 26.0), even though the resource handling is improved and request-scoped resources are properly closed now, on the other hand the framework pre-initializes some providers (resources) that are not needed to handle the specific request. If these resources are heavier to create, then the request serving time is affected.

I think this is done here: anydi._context.ResourceScopedContext.astart

    async def astart(self) -> None:
        """Start the scoped context asynchronously."""
        for interface in self.container._providers_cache.get(self.scope, []):  # noqa
            await self.container.aresolve(interface)

Where container._providers_cache contains seemingly unrelated providers to the current request's required dependencies.

antonrh commented 3 months ago

@attilavetesi-epam , Thanks once again. Fixed in 0.26.3.

Starting a request context should not start unnecessary resources unless it is a request-scoped event, e.g.:


import asyncio
from typing import AsyncIterator

import anydi

container = anydi.Container()

@container.provider(scope="request")
async def dep1() -> AsyncIterator[str]:
    print("dep1: start")
    yield "dep1"
    print("dep1: close")

@container.provider(scope="request")
async def dep2() -> AsyncIterator[int]:
    print("dep2: start")
    yield 100
    print("dep2: close")

@container.provider(scope="request")
async def global_request_event() -> AsyncIterator[None]:
    print("before request")
    yield
    print("after request")

@container.inject
async def handle(dep1: str = anydi.auto) -> None:
    print(f"handling '{dep1}'")

async def main() -> None:
    async with container.arequest_context():
        await handle()

asyncio.run(main())

output:

before request
dep1: start
handling 'dep1'
dep1: close
after request
attilavetesi-epam commented 3 months ago

Thanks for the quick response.

With 26.3 I get an error during bootup at:

def is_event_type(cls: Any) -> bool:
    """Checks if an object is an event type."""
    return issubclass(cls, Event)

Error: TypeError: issubclass() arg 1 must be a class

Stack:

  File "<...>pypoetry\Cache\virtualenvs\<project>-py3.11\Lib\site-packages\anydi\_container.py", line 488, in arequest_context
    async with context:
  File "<...>pypoetry\Cache\virtualenvs\<project>-py3.11\Lib\site-packages\anydi\_context.py", line 273, in __aenter__
    await self.astart()
  File "<...>pypoetry\Cache\virtualenvs\<project>-py3.11\Lib\site-packages\anydi\_context.py", line 335, in astart
    if not is_event_type(interface):
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "<...>pypoetry\Cache\virtualenvs\<project>-py3.11\Lib\site-packages\anydi\_types.py", line 41, in is_event_type
    return issubclass(cls, Event)
           ^^^^^^^^^^^^^^^^^^^^^^

cls is an _AnnotatedAlias or typing.Annotated[SomeOfMyServices, 'some_of_my_qualifiers'] when the error happens.

antonrh commented 3 months ago

Fixed in 0.26.4

attilavetesi-epam commented 3 months ago

Works thanks!