Skip to content

Handle transactional logic? #27

Description

@UnoYakshi

How it works now

Right now, this module require to create adapter EVERY time the transaction occurs (ref). However, what we need is to be able to provide already generated session to the Adapter's methods. There is no point to re-create adapter itself, the DB URI likely won't ever change.

Problem

In terms of atomic DB-operations, Casbin adapter is a bit "difficult" to work with.

For example, when a User created, we want to assign some policies.

async def create_user(session, ...):
    user = ...  # Handled via session, but not yet commited...
    await session.refresh(user)

    # Since AsyncAdapter uses `self._session_scope()` all the time, we can't control the flow whatsoever...
    await enforcer.add_group(...)  # Assign `user.id` to a group...
    await enforcer.add_policies(...)  # Assign permissions to `user.id`...

    # Something happens, rollback. No user is created in DB...
    # But group is assigned to non-existing ID and this ID has permissions...

And here our atomicity breaks completely, because we haven't yet commited created user and a rollback might happen. If rollback occurs, we have to clean up manually.

Solution (?)

Provide 2 parameters parameters to most of the methods to handle transactions to DB:

  • session: AsyncSession | None = None ― either use provided session or use self._session_scope() to generate a new session;
  • commit: bool = True ― whether we need to commit or not.

Full list of the methods:

  • Adapter.add_policy(<previous signature>, commit: bool = True, session: AsyncSession | None = None)
  • Adapter.add_policies(<previous signature>, commit: bool = True, session: AsyncSession | None = None)
  • Adapter.remove_policy(<previous signature>, commit: bool = True, session: AsyncSession | None = None)
  • Adapter.remove_policies(<previous signature>, commit: bool = True, session: AsyncSession | None = None)
  • Adapter.remove_filtered_policy(<previous signature>, commit: bool = True, session: AsyncSession | None = None)
  • Adapter.update_policy(<previous signature>, commit: bool = True, session: AsyncSession | None = None)
  • Adapter.update_policies(<previous signature>, commit: bool = True, session: AsyncSession | None = None)
  • Adapter.update_filtered_policies(<previous signature>, commit: bool = True, session: AsyncSession | None = None)

And the session-related method should now become aware of externally used session provided for/within methods (not constructor).

@asynccontextmanager
async def _session_scope(self, commit: bool = True, session: AsyncSesssion | None = None) -> AsyncSession:
    """Provide an asynchronous transactional scope around a series of operations."""

    # Instead of using a session passed in the constructor, let's pass it
    if session:
        yield session
    else:
        # Use internal session with automatic commit/rollback
        async with self.session_local() as session:
            try:
                yield session
                if commit:
                    await session.commit()
            except Exception as e:
                await session.rollback()
                raise e

Metadata

Metadata

Labels

enhancementNew feature or request

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions