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.86k stars 303 forks source link

Custom ResourceFactory provider help #760

Open agusmdev opened 11 months ago

agusmdev commented 11 months ago

Hi! I'm trying to handle SQLAlchemy sessions as a resource provider instead of using FastAPI's built-in Depends.

Let me share some code:

The database class:


class Database:
    """Class to handle database connections and sessions"""

    def __init__(self, db_url: str, **kwargs):
        """Initialize the Database class with a database URL and optional keyword arguments"""
        self.db_url = db_url
        self.engine = create_engine(self.db_url, **kwargs, echo=settings.DEBUG_MODE, connect_args={"check_same_thread": False}, pool_size=0)
        self.local_session = sessionmaker(autocommit=False, autoflush=False, bind=self.engine)

    @contextmanager
    def session(self):
        """Context manager to handle sessions"""
        session: Session = self.local_session()
        try:
            yield session
        except Exception as e:
            session.rollback()
        finally:
            session.close()

The main container where I create these resources:


def get_db_session(session_factory):
    with session_factory() as db:
        yield db

class AppContainer(containers.DeclarativeContainer):
    """Container to serve all the containers related to the app"""

   # ..... other injections

    # Setup container for talent services
    db = providers.Factory(Database, db_url=settings.DB_URL)

    db_sess = providers.Resource(get_db_session, session_factory=db.provided.session)

    user_container = providers.Container(
        UserContainer,  # and this container has "UserService" that also uses the db session dependency
        db_sess=db_sess,
    )
    # .... 

So, I inject the session into the services directly (or in the future in one repository). I don't like injecting the session into the routers directly that's why I prefer not using the FastAPI Depends.

The code provided works as expected, it creates 1 session per request and then closes it, the thing is that this doesn't work in a concurrent environment, I made some local testing with locust, and once I start having many requests executing with multi-threads (I'm using sync endpoints), the sessions don't work as expected, it seems that the same session is being used in more than 1 request at the same time, so I guess this is something that we could solve if we had something like FactoryResource instead of just a Resource that works as a Singleton. I tried to implement this myself by creating a CustomProvider but I couldn't make it, so if anyone has any advice I'd appreciate it.

P.S: I checked all the related issues regarding FastAPI and SQLAlchemy sessions in this repo and tried some approaches but none of them worked correctly

theobouwman commented 10 months ago

@agusmdev you dont want a session/connection per request right? I thought that what you want is that connections are reused

theobouwman commented 10 months ago

@agusmdev wouldnt a possible solution be to create the session before a request, with middleware, and close it afterrwards

agusmdev commented 10 months ago

@agusmdev you dont want a session/connection per request right? I thought that what you want is that connections are reused

@theobouwman Mmm I think that I do want 1 session per request. I'm using FastAPI and I'd like to emulate the same injection that you can do with FastAPI built-in Depends but given that I want to inject the session in a service and not in a router I can't use the FastAPI built-in injector.

@agusmdev wouldnt a possible solution be to create the session before a request, with middleware, and close it afterrwards

Yes, it's possible, but the session per request is working correctly with the implementation I have, the issue appears when you have a bunch of requests at the same time, the sessions get reused by the requests because the implementation of Resource is like a Singleton and not like a Factory (or that's what I guess that's happening here)

theobouwman commented 10 months ago

@agusmdev okay I get it. I have the issue that with this implementation (https://github.com/ets-labs/python-dependency-injector/blob/master/examples/miniapps/fastapi-sqlalchemy/webapp/database.py) each query a new session is created, which is not what we want because this takes time as you can see in the screenshot.

Screenshot 2023-11-08 at 20 26 36
theobouwman commented 10 months ago

Yes, it's possible, but the session per request is working correctly with the implementation I have, the issue appears when you have a bunch of requests at the same time, the sessions get reused by the requests because the implementation of Resource is like a Singleton and not like a Factory (or that's what I guess that's happening here)

What complications could arise when using this?

theobouwman commented 10 months ago

Yes, it's possible, but the session per request is working correctly with the implementation I have, the issue appears when you have a bunch of requests at the same time, the sessions get reused by the requests because the implementation of Resource is like a Singleton and not like a Factory (or that's what I guess that's happening here)

How do you test this?