SQLAlchemy、可序列化事务隔离和以惯用Python方式重试

SQLAlchemy、可序列化事务隔离和以惯用Python方式重试,python,postgresql,sqlalchemy,pyramid,zope,Python,Postgresql,Sqlalchemy,Pyramid,Zope,PostgreSQL和SQL定义了一个。如果将事务隔离到此级别,冲突的并发事务将中止并需要重试 我熟悉Plone/Zope world中事务重试的概念,在那里,如果存在事务冲突,可以重放整个HTTP请求。SQLAlchemy(以及潜在的SQLAlchemy)如何实现类似的功能?我试图阅读zope.sqlalchemy和的文档,但这对我来说并不明显 我特别想要这样的东西: # Try to do the stuff, if it fails because of transaction con

PostgreSQL和SQL定义了一个。如果将事务隔离到此级别,冲突的并发事务将中止并需要重试

我熟悉Plone/Zope world中事务重试的概念,在那里,如果存在事务冲突,可以重放整个HTTP请求。SQLAlchemy(以及潜在的SQLAlchemy)如何实现类似的功能?我试图阅读zope.sqlalchemy和的文档,但这对我来说并不明显

我特别想要这样的东西:

  # Try to do the stuff, if it fails because of transaction conflict do again until retry count is exceeded
  with transaction.manager(retries=3):
        do_stuff()

  # If we couldn't get the transaction through even after 3 attempts, fail with a horrible exception

所以,经过两周的摸索,没有现成的解决方案,我想出了自己的解决方案

这里有一个
ConflictResolver
类,它提供了
managed\u事务
函数装饰器。您可以使用decorator将函数标记为可重试。也就是说,如果在运行函数时出现数据库冲突错误,函数将再次运行,现在更希望导致冲突错误的db事务已经完成

源代码如下:

此处介绍了单元测试:

仅限Python 3.4+

"""Serialized SQL transaction conflict resolution as a function decorator."""

import warnings
import logging
from collections import Counter

from sqlalchemy.orm.exc import ConcurrentModificationError
from sqlalchemy.exc import OperationalError


UNSUPPORTED_DATABASE = "Seems like we might know how to support serializable transactions for this database. We don't know or it is untested. Thus, the reliability of the service may suffer. See transaction documentation for the details."

#: Tuples of (Exception class, test function). Behavior copied from _retryable_errors definitions copied from zope.sqlalchemy
DATABASE_COFLICT_ERRORS = []

try:
    import psycopg2.extensions
except ImportError:
    pass
else:
    DATABASE_COFLICT_ERRORS.append((psycopg2.extensions.TransactionRollbackError, None))

# ORA-08177: can't serialize access for this transaction
try:
    import cx_Oracle
except ImportError:
    pass
else:
    DATABASE_COFLICT_ERRORS.append((cx_Oracle.DatabaseError, lambda e: e.args[0].code == 8177))

if not DATABASE_COFLICT_ERRORS:
    # TODO: Do this when cryptoassets app engine is configured
    warnings.warn(UNSUPPORTED_DATABASE, UserWarning, stacklevel=2)

#: XXX: We need to confirm is this the right way for MySQL, SQLIte?
DATABASE_COFLICT_ERRORS.append((ConcurrentModificationError, None))


logger = logging.getLogger(__name__)


class CannotResolveDatabaseConflict(Exception):
    """The managed_transaction decorator has given up trying to resolve the conflict.

    We have exceeded the threshold for database conflicts. Probably long-running transactions or overload are blocking our rows in the database, so that this transaction would never succeed in error free manner. Thus, we need to tell our service user that unfortunately this time you cannot do your thing.
    """


class ConflictResolver:

    def __init__(self, session_factory, retries):
        """

        :param session_factory: `callback()` which will give us a new SQLAlchemy session object for each transaction and retry

        :param retries: The number of attempst we try to re-run the transaction in the case of transaction conflict.
        """
        self.retries = retries

        self.session_factory = session_factory

        # Simple beancounting diagnostics how well we are doing
        self.stats = Counter(success=0, retries=0, errors=0, unresolved=0)

    @classmethod
    def is_retryable_exception(self, e):
        """Does the exception look like a database conflict error?

        Check for database driver specific cases.

        :param e: Python Exception instance
        """

        if not isinstance(e, OperationalError):
            # Not an SQLAlchemy exception
            return False

        # The exception SQLAlchemy wrapped
        orig = e.orig

        for err, func in DATABASE_COFLICT_ERRORS:
            # EXception type matches, now compare its values
            if isinstance(orig, err):
                if func:
                    return func(e)
                else:
                    return True

        return False

    def managed_transaction(self, func):
        """SQL Seralized transaction isolation-level conflict resolution.

        When SQL transaction isolation level is its highest level (Serializable), the SQL database itself cannot alone resolve conflicting concurrenct transactions. Thus, the SQL driver raises an exception to signal this condition.

        ``managed_transaction`` decorator will retry to run everyhing inside the function

        Usage::

            # Create new session for SQLAlchemy engine
            def create_session():
                Session = sessionmaker()
                Session.configure(bind=engine)
                return Session()

            conflict_resolver = ConflictResolver(create_session, retries=3)

            # Create a decorated function which can try to re-run itself in the case of conflict
            @conflict_resolver.managed_transaction
            def myfunc(session):

                # Both threads modify the same wallet simultaneously
                w = session.query(BitcoinWallet).get(1)
                w.balance += 1

            # Execute the conflict sensitive code inside a managed transaction
            myfunc()

        The rules:

        - You must not swallow all exceptions within ``managed_transactions``. Example how to handle exceptions::

            # Create a decorated function which can try to re-run itself in the case of conflict
            @conflict_resolver.managed_transaction
            def myfunc(session):

                try:
                    my_code()
                except Exception as e:
                    if ConflictResolver.is_retryable_exception(e):
                        # This must be passed to the function decorator, so it can attempt retry
                        raise
                    # Otherwise the exception is all yours

        - Use read-only database sessions if you know you do not need to modify the database and you need weaker transaction guarantees e.g. for displaying the total balance.

        - Never do external actions, like sending emails, inside ``managed_transaction``. If the database transaction is replayed, the code is run twice and you end up sending the same email twice.

        - Managed transaction section should be as small and fast as possible

        - Avoid long-running transactions by splitting up big transaction to smaller worker batches

        This implementation heavily draws inspiration from the following sources

        - http://stackoverflow.com/q/27351433/315168

        - https://gist.github.com/khayrov/6291557
        """

        def decorated_func():

            # Read attemps from app configuration
            attempts = self.retries

            while attempts >= 0:

                session = self.session_factory()
                try:
                    result = func(session)
                    session.commit()
                    self.stats["success"] += 1
                    return result

                except Exception as e:
                    if self.is_retryable_exception(e):
                        session.close()
                        self.stats["retries"] += 1
                        attempts -= 1
                        if attempts < 0:
                            self.stats["unresolved"] += 1
                            raise CannotResolveDatabaseConflict("Could not replay the transaction {} even after {} attempts".format(func, self.retries)) from e
                        continue
                    else:
                        session.rollback()
                        self.stats["errors"] += 1
                        # All other exceptions should fall through
                        raise

        return decorated_func
“作为函数装饰器的序列化SQL事务冲突解决方法。”“”
进口警告
导入日志记录
从收款进口柜台
从sqlalchemy.orm.exc导入ConcurrentModificationError
从sqlalchemy.exc导入错误
UNSUPPORTED_DATABASE=“似乎我们可能知道如何支持此数据库的可序列化事务。我们不知道或它未经测试。因此,服务的可靠性可能会受到影响。有关详细信息,请参阅事务文档。”
#:元组(异常类、测试函数)。行为复制自_retryable_错误定义复制自zope.sqlalchemy
数据库错误=[]
尝试:
导入psycopg2.extensions
除恐怖外:
通过
其他:
数据库_COFLICT_ERRORS.append((psycopg2.extensions.TransactionRollbackError,无))
#ORA-08177:无法序列化此事务的访问权限
尝试:
导入cx_Oracle
除恐怖外:
通过
其他:
数据库_COFLICT_ERRORS.append((cx_Oracle.DatabaseError,lambda e:e.args[0]。代码==8177))
如果不是数据库错误:
#TODO:在配置cryptoassets应用程序引擎时执行此操作
warning.warn(不支持的_数据库,UserWarning,stacklevel=2)
#:XXX:我们需要确认这是MySQL、SQLIte的正确方式吗?
数据库_COFLICT_ERRORS.append((ConcurrentModificationError,无))
logger=logging.getLogger(_名称__)
类CannotResolveDatabaseConflict(异常):
“”“托管\u事务装饰程序已放弃尝试解决冲突。
我们已超过数据库冲突的阈值。可能是长时间运行的事务或过载阻塞了我们在数据库中的行,因此此事务将永远不会以无错误的方式成功。因此,我们需要告诉服务用户,很遗憾,这一次您无法完成您的任务。
"""
类冲突解决程序:
定义初始化(自我、会话工厂、重试):
"""
:param session_factory:`callback()`这将为每个事务提供一个新的SQLAlchemy会话对象,然后重试
:param retries:在事务冲突的情况下,我们尝试重新运行事务的尝试次数。
"""
self.retries=重试
self.session\u工厂=session\u工厂
#简单的Bean计算诊断我们做得有多好
self.stats=计数器(成功=0,重试=0,错误=0,未解决=0)
@类方法
def是可重试的异常(self,e):
“”“异常看起来像是数据库冲突错误吗?
检查数据库驱动程序特定的情况。
:param e:Python异常实例
"""
如果不存在(e,操作错误):
#不是SQLAlchemy的例外
返回错误
#炼金术的例外
原点=例如原点
对于err,数据库中的func\u COFLICT\u错误:
#异常类型匹配,现在比较其值
如果存在(原始、错误):
如果func:
返回函数(e)
其他:
返回真值
返回错误
def管理的_事务(自身、职能):
“”“SQL已实现事务隔离级别冲突解决。”。
当SQL事务隔离级别是其最高级别(可序列化)时,SQL数据库本身无法单独解决冲突的并发事务。因此,SQL驱动程序会引发异常以通知此情况。
``托管_事务``装饰程序将重试运行函数中的所有内容
用法::
#为SQLAlchemy引擎创建新会话
def create_会话():
Session=sessionmaker()
配置(绑定=引擎)
返回会话()
冲突解决程序=冲突解决程序(创建会话,重试次数=3)
#创建一个修饰函数,在发生冲突时可以尝试重新运行自己
@冲突\u解析程序。托管\u事务
def myfunc(会话):
#两个线程同时修改同一钱包
w=session.query(比特币钱包).get(1)
w、 余额+=1
#在托管事务中执行冲突敏感代码
myfunc()
规则:
-您不能接受“托管的\u事务”中的所有异常。如何处理异常的示例:
#创建一个修饰函数,在发生冲突时可以尝试重新运行自己
@冲突\u解析程序。托管\u事务
def myfunc(会话):
尝试:
我的代码()
例外情况除外,如e:
如果ConflictResolver.is_可重试_异常(e):
#这必须传递给函数decorator,以便它可以尝试重试