跨请求持久化的事务无效

2024-04-26 05:50:44 发布

您现在位置:Python中文网/ 问答频道 /正文

摘要

我们生产中的一个线程遇到了错误,现在正在生成InvalidRequestError: This session is in 'prepared' state; no further SQL can be emitted within this transaction.错误,在它服务的每一个查询请求的剩余生命中!它已经这样做了,现在!这怎么可能,我们怎么能阻止它向前发展呢?

背景

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

当我们生产中的一个线程在这个烧瓶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不顾我们的日志配置,将输出发送到stdout,这是有意义的,因为它发生在应用程序上下文被破坏的过程中,不应该引发异常。我不确定如果不设置response_or_exec,事务是如何变得无效的,但这实际上是一个较小的问题。

更大的问题是,当“prepared”状态错误开始时,从那以后就没有停止过。每次这个线程处理一个到达数据库的请求时,它都是500秒。其他线程似乎都很好:据我所知,即使是在同一进程中的线程也没有问题。

胡乱猜测

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

不过,我仍然觉得这并不能解释这个会话是如何在请求之间持久化的。我们还没有修改Flask SQLAlchemy对请求范围会话的使用,因此该会话应该返回到SQLAlchemy的池中,并在请求结束时回滚,甚至是那些出错的会话(尽管不可否认,可能不是第一个,因为这是在应用程序上下文崩溃期间引发的)。为什么没有发生回滚?如果我们每次都在stdout(在uwsgi的日志中)看到“无效事务”错误,我可以理解,但我们没有看到:我只看到一次,第一次。但每次出现500秒时,我都会看到“prepared”状态错误(在我们的应用程序日志中)。

配置详细信息

我们在session_options中关闭了expire_on_commit,并且打开了SQLALCHEMY_COMMIT_ON_TEARDOWN。我们只是在读数据库,还没有写。我们还对所有查询使用Dogpile缓存(使用memcached锁,因为我们有多个进程,实际上有两个负载平衡的服务器)。对于我们的主要查询,缓存每分钟都会过期。

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

重新启动服务器似乎解决了这个问题,这并不完全令人惊讶。也就是说,我希望能再次看到它,直到我们找到阻止它的方法。benselme(下面)建议编写我们自己的teardown回调函数,并对提交进行异常处理,但我觉得更大的问题是线程在其余生中一直处于混乱状态。事实上,在一两个请求之后,这个并没有消失,这真的让我很紧张!


Tags: orself应用程序flasksqlalchemy进程isresponse
2条回答

编辑2016-06-05:

一家解决这一问题的公关公司已于2016年5月26日合并。

Flask PR 1822

编辑2015-04-13:

谜团解开了!

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

开始了一项新的工作,也使用烧瓶,这个问题再次出现,在我把撕裂包装食谱到位之前。所以我重新审视了这个问题,最终弄清楚了发生了什么。

如我所想,每当一个新的请求下线时,Flask就会将一个新的请求上下文推送到请求上下文堆栈上。这用于支持请求本地全局变量,如会话。

Flask还有一个“application”上下文的概念,它独立于请求上下文。它的目的是支持诸如测试和CLI访问之类的东西,而不是HTTP。我知道这一点,我也知道这就是Flask SQLA放置其DB会话的地方。

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

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

因此,如果应用程序上下文没有在请求结束时由于一个解压函数的提升而弹出,那么它不仅会一直存在,甚至不会有一个新的应用程序上下文推到它上面。

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

也就是说,这确实意味着您必须绝对确保您的可拆卸函数成功,使用下面的可拆卸函数包装器之类的东西。这是一个好主意,即使没有这个功能,以避免泄漏内存和数据库连接,但鉴于这些发现是特别重要的。因为这个原因,我要向Flask的医生提交一份公关报告。(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会等待那个工人完成所有请求,就像uWSGI的“优雅重新加载”功能一样,但似乎不是这样。我们开始遇到这样的问题:一个线程在Memcached中获取一个dogpile锁,然后在uWSGI由于另一个线程中的异常而重新加载工作线程时终止,这意味着锁永远不会被释放。

删除SQLALCHEMY_COMMIT_ON_TEARDOWN解决了我们的部分问题,尽管在session.remove()过程中,在app拆卸过程中仍会偶尔出现错误。这似乎是由SQLAlchemy issue 3043引起的,它在0.9.5版本中已经修复,所以希望升级到0.9.5版本将允许我们依赖于始终工作的应用程序上下文分解。

原件:

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

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

我们还将关闭SQLALCHEMY_COMMIT_ON_TEARDOWN,尽管我们可能会显式提交而不是为app teardown编写自己的回调,因为我们很少向数据库写入数据。

令人惊讶的是,围绕这个self.session.commit没有异常处理。提交可能会失败,例如,如果与数据库的连接丢失。因此,提交失败,session不会被删除,下次该特定线程处理请求时,它仍试图使用现在无效的会话。

不幸的是,Flask SQLAlchemy不提供任何干净的可能性来拥有自己的拆卸功能。一种方法是将SQLALCHEMY_COMMIT_ON_TEARDOWN设置为False,然后编写自己的teardown函数。

应该是这样的:

@app.teardown_appcontext
def shutdown_session(response_or_exc):
    try: 
        if response_or_exc is None:
            sqla.session.commit()
    finally:
        sqla.session.remove()
    return response_or_exc

现在,你仍然会有你失败的承诺,你必须单独调查。。。但至少你的线程应该恢复。

相关问题 更多 >