antonrh / anydi

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

Exception is not delegated to resource managers #102

Closed attilavetesi-epam closed 1 month ago

attilavetesi-epam commented 1 month ago

Exception is not delegated to resource managers

If an exception happens while a resource is being used, the exception is not delegated to the context manager of the resource, thus preventing the manager to take the appropriate action.
This is especially important for cases where a DB session has to be committed / rolled back based on the outcome of the operations.

Example runnable snippet:

import time
from threading import Thread
from typing import AsyncIterator

import anydi
import requests
import uvicorn
from anydi import Container
from anydi.ext.fastapi import Inject
from anydi.ext.starlette.middleware import RequestScopedMiddleware
from fastapi import FastAPI
from starlette.middleware import Middleware

class SomePersistentResource:
    def use_resource(self):
        print("Resource being used!")

    def commit(self):
        print("Resource committed!")

    def rollback(self):
        print("Resource rolled back!")

container = Container()

@container.provider(scope="request")
async def create_resource() -> AsyncIterator[SomePersistentResource]:
    print("Initializing resource...")
    res = SomePersistentResource()
    try:
        yield res
    except Exception:
        # PROBLEM: this branch is not called when exception happens while using the resource
        res.rollback()
    else:
        # PROBLEM: this branch is called always
        res.commit()

app = FastAPI(middleware=[
    Middleware(RequestScopedMiddleware, container=container),
])

@app.get("/some-endpoint")
async def some_endpoint(res: SomePersistentResource = Inject()) -> str:
    res.use_resource()
    raise RuntimeError()
    return "Success"

anydi.ext.fastapi.install(app, container)

def run_server():
    uvicorn.run(app, host="0.0.0.0", port=8000)

if __name__ == "__main__":
    thread = Thread(target=run_server)
    thread.daemon = True
    thread.start()

    # Give the server a moment to start
    print("Waiting for server to start...")
    time.sleep(3)
    print("Sending test request...")

    response = requests.get("http://127.0.0.1:8000/some-endpoint")
    print(response.text)

Output:

Waiting for server to start...
INFO:     Started server process [23344]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
Sending test request...
Initializing resource...
Resource being used!
Resource committed!
INFO:     127.0.0.1:24335 - "GET /some-endpoint HTTP/1.1" 500 Internal Server Error
ERROR:    Exception in ASGI application
  + Exception Group Traceback (most recent call last):
<error truncated>

(Resource committed instead of rolled back.)

antonrh commented 1 month ago

Hi, @attilavetesi-epam,

Could you please review the following PR: https://github.com/antonrh/anydi/pull/103/files and validate the behavior in the prepatch version https://pypi.org/project/anydi/0.26.8a0/?

antonrh commented 1 month ago

I tested the fix version on my existing projects and found no issues. Closing for now, but I'm ready to fix anything if you find something