python - 跨请求持续存在的无效事务

标签 python flask sqlalchemy uwsgi flask-sqlalchemy

概括

我们在生产中的一个线程出错,现在正在生产 InvalidRequestError: This session is in 'prepared' state; no further SQL can be emitted within this transaction.错误,在它的余生中,在每个带有查询的请求上!这几天一直这样,现在!这怎么可能,我们如何防止它继续发展?

背景

我们在 uWSGI(4 个进程,2 个线程)上使用 Flask 应用程序,Flask-SQLAlchemy 为我们提供到 SQL Server 的数据库连接。

当我们在生产中的一个线程在这个 Flask-SQLAlchemy 方法中拆除它的请求时,问题似乎开始了:

@teardown
def shutdown_session(response_or_exc):
    if app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN']:
        if response_or_exc is None:
            self.session.commit()
    self.session.remove()
    return response_or_exc

...不知何故设法拨打 self.session.commit()当交易无效时。这导致了 sqlalchemy.exc.InvalidRequestError: Can't reconnect until invalid transaction is rolled back无视我们的日志记录配置,将输出输出到标准输出,这是有道理的,因为它发生在应用程序上下文拆除期间,永远不应该引发异常。如果没有 response_or_exec,我不确定交易是如何无效的开始设置,但这实际上是 AFAIK 较小的问题。

更大的问题是,那是“'准备好'状态”错误开始的时候,此后一直没有停止。每次这个线程服务一个访问数据库的请求时,它都会持续 500 秒。每个其他线程似乎都很好:据我所知,即使是同一进程中的线程也运行正常。

胡乱猜测

SQLAlchemy 邮件列表有一个关于“'prepared' state”错误的条目,表示如果 session 开始提交但尚未完成,并且其他东西尝试使用它,则会发生这种情况。我的猜测是该线程中的 session 从未到达 self.session.remove()一步,现在它永远不会。

我仍然觉得这并不能解释这个 session 是如何跨请求持续存在的。我们没有修改 Flask-SQLAlchemy 对请求范围 session 的使用,所以 session 应该返回到 SQLAlchemy 的池并在请求结束时回滚,即使是那些出错的(尽管不可否认,可能不是第一个,因为在应用程序上下文拆除期间引发了)。为什么回滚没有发生?如果我们每次都在标准输出(在 uwsgi 的日志中)看到“无效交易”错误,我可以理解,但我们不是:我只看到过一次,第一次。但是每次发生 500 次时,我都会看到“'准备好'状态”错误(在我们的应用程序日志中)。

配置详情

我们已关闭 expire_on_commitsession_options ,我们已经打开 SQLALCHEMY_COMMIT_ON_TEARDOWN .我们只是从数据库中读取,还没有写入。我们还对所有查询使用 Dogpile-Cache(使用 memcached 锁,因为我们有多个进程,实际上有 2 个负载平衡服务器)。对于我们的主要查询,缓存每分钟过期一次。

2014-04-28 更新:解决步骤

重新启动服务器似乎解决了问题,这并不完全令人惊讶。也就是说,我希望再次看到它,直到我们弄清楚如何阻止它。 benselme(下面)建议编写我们自己的拆卸回调,并围绕提交进行异常处理,但我觉得更大的问题是线程在其余生中都被搞砸了。一两次请求后这种情况并没有消失的事实真的让我很紧张!

最佳答案

编辑 2016-06-05:

一个解决这个问题的 PR 已经在 2016 年 5 月 26 日合并了。

Flask PR 1822

编辑 2015-04-13:

谜团已揭开!

TL;DR:通过使用 2014-12-11 编辑中的拆卸包装配方,绝对确保您的拆卸功能成功!

也使用 Flask 开始了一项新工作,在我实现拆解包装配方之前,这个问题再次出现。所以我重新审视了这个问题,终于弄清楚发生了什么。

正如我所想的那样,每次有新请求下线时,Flask 都会将一个新的请求上下文推送到请求上下文堆栈中。这用于支持请求本地全局变量,如 session 。

Flask 也有一个与请求上下文分开的“应用程序”上下文的概念。它旨在支持测试和 CLI 访问等不发生 HTTP 的事情。我知道这一点,我也知道这就是 Flask-SQLA 放置其数据库 session 的地方。

在正常操作期间,请求和应用程序上下文都在请求开始时推送,并在请求结束时弹出。

然而,事实证明,在推送请求上下文时,请求上下文会检查是否存在现有的应用程序上下文,如果存在,则不会推送新的!

因此,如果由于拆卸功能提升而在请求结束时未弹出应用程序上下文,它不仅会永远存在,甚至不会在其上推送新的应用程序上下文。

这也解释了一些我在我们的集成测试中没有理解的魔法。您可以插入一些测试数据,然后运行一些请求,尽管您没有提交,但这些请求将能够访问该数据。这是唯一可能的,因为请求有一个新的请求上下文,但正在重用测试应用程序上下文,因此它正在重用现有的数据库连接。所以这真的是一个功能,而不是一个错误。

也就是说,这确实意味着您必须绝对确保您的拆卸功能成功,使用类似下面的拆卸功能包装器的东西。即使没有该功能以避免泄漏内存和数据库连接,这也是一个好主意,但鉴于这些发现,这一点尤其重要。出于这个原因,我将向 Flask 的文档提交 PR。 ( Here it is )

编辑 2014-12-11:

我们最终放置的一件事是以下代码(在我们的应用程序工厂中),它包装了每个拆卸函数以确保它记录异常并且不会进一步引发。这可确保应用上下文始终成功弹出。显然,这必须在您确定所有拆卸功能都已注册之后进行。

# Flask specifies that teardown functions should not raise.
# However, they might not have their own error handling,
# so we wrap them here to log any errors and prevent errors from
# propagating.
def wrap_teardown_func(teardown_func):
    @wraps(teardown_func)
    def log_teardown_error(*args, **kwargs):
        try:
            teardown_func(*args, **kwargs)
        except Exception as exc:
            app.logger.exception(exc)
    return log_teardown_error

if app.teardown_request_funcs:
    for bp, func_list in app.teardown_request_funcs.items():
        for i, func in enumerate(func_list):
            app.teardown_request_funcs[bp][i] = wrap_teardown_func(func)
if app.teardown_appcontext_funcs:
    for i, func in enumerate(app.teardown_appcontext_funcs):
        app.teardown_appcontext_funcs[i] = wrap_teardown_func(func)

编辑 2014-09-19:

好的,结果是 --reload-on-exception如果 1.) 您正在使用多个线程和 2.) 在请求中终止线程可能会导致麻烦,这不是一个好主意。我以为 uWSGI 会等待该 worker 完成所有请求,就像 uWSGI 的“优雅重新加载”功能那样,但似乎并非如此。我们开始遇到一些问题,一个线程会在 Memcached 中获取狗堆锁,然后在 uWSGI 由于不同线程中的异常重新加载工作线程时被终止,这意味着锁永远不会被释放。

删除 SQLALCHEMY_COMMIT_ON_TEARDOWN解决了我们的部分问题,尽管我们在 session.remove() 期间在应用程序拆卸期间仍然偶尔会出现错误.看来这些是由 SQLAlchemy issue 3043 引起的,它在 0.9.5 版本中得到修复,所以希望升级到 0.9.5 将允许我们依赖应用程序上下文拆卸始终有效。

原文:

这首先是如何发生的仍然是一个悬而未决的问题,但我确实找到了一种方法来防止它:uWSGI 的 --reload-on-exception选项。

我们的 Flask 应用程序的错误处理应该几乎可以捕获任何东西,因此它可以提供自定义错误响应,这意味着只有最意外的异常才应该一直到 uWSGI。因此,每当发生这种情况时,重新加载整个应用程序是有意义的。

我们还将关闭 SQLALCHEMY_COMMIT_ON_TEARDOWN ,尽管我们可能会明确提交而不是编写我们自己的应用程序拆卸回调,因为我们很少写入数据库。

关于python - 跨请求持续存在的无效事务,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23301968/

相关文章:

python - python3.2有python-notify模块吗?

Python Flask Jinja2 模板 null 错误

python - VS代码: How to debug Flask app that uses Connexion?

python - 在 sqlalchemy 中按年、月、日分组

python - sqlalchemy 打印结果而不是对象

mysql - MySQL "SET foreign_key_checks = 0"的 SQLAlchemy 配置

python - 如何使用元组访问深度嵌套的字典?

python - 'thread._local' 对象没有属性

python - 保存带有时间戳的视频帧

python - 如何在python中的POST请求中发送urlencoded参数