无效的事务在请求之间保持不变

时间:2014-04-25 20:00:44

标签: python flask sqlalchemy uwsgi flask-sqlalchemy

摘要

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

背景

我们在uWSGI上使用Flask应用程序(4个进程,2个线程),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输出到stdout,无视我们的日志记录配置,这是有道理的,因为它发生在应用程序上下文拆除期间,从来不应该引发异常。如果没有response_or_exec设置,我不确定如何该事务无效,但这实际上是AFAIK中较小的问题。

更大的问题是,当“准备好的'状态”错误开始时,并且此后一直没有停止。每次这个线程提供命中数据库的请求时,它就是500s。每个其他线程似乎都没问题:据我所知,即使是同一进程中的线程也没问题。

狂野猜测

SQLAlchemy邮件列表中有一个关于“'准备好的'状态”的条目错误,说明如果会话开始提交但尚未完成,并且还有其他东西试图使用它。我的猜测是这个帖子中的会话从未进入self.session.remove()步骤,现在它永远不会。

我仍然觉得这并没有解释这个会话如何持续跨请求。我们还没有修改Flask-SQLAlchemy对请求范围会话的使用,因此会话应该返回到SQLAlchemy的池并在请求结束时回滚,即使是错误的(尽管可能不是第一个,因为在应用程序上下文中引发的内容被拆除了)。为什么回滚没有发生?如果我们每次都看到stdout上的“无效事务”错误(在uwsgi的日志中),我就能理解它,但我们不是:我第一次只看到它一次。但每次500秒发生时,我都会看到“准备好的状态”错误(在我们的应用程序日志中)。

配置详细信息

我们已关闭expire_on_commit中的session_options,我们已启用SQLALCHEMY_COMMIT_ON_TEARDOWN。我们只是从数据库中读取,而不是写作。我们还使用Dogpile-Cache进行所有查询(使用memcached锁,因为我们有多个进程,实际上是2个负载均衡的服务器)。缓存会在我们的主要查询的每一分钟到期。

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

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

2 个答案:

答案 0 :(得分:32)

修改2016-06-05:

解决此问题的公关已于2016年5月26日合并。

Flask PR 1822

修改2015-04-13:

神秘解决了!

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

使用Flask也开始了一项新工作,在我将拆解包装配方付诸实施之前,这个问题又出现了。所以我重新审视了这个问题,最后想出了发生了什么。

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

Flask也有一个"应用"的概念。与请求上下文分开的上下文。这意味着支持HTTP不会发生的测试和CLI访问等事情。我知道这一点,而且我也知道Flask-SQLA放置数据库会话的地方。

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

但是,事实证明,在推送请求上下文时,请求上下文会检查是否存在现有的应用上下文,如果存在,则不会出现这样的情况。 t 推新一个!

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

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

那就是说,它确实意味着你必须绝对确定你的拆解功能是否成功,使用下面的拆卸功能包装器。即使没有该功能以避免泄漏内存和数据库连接,这也是一个好主意,但鉴于这些发现,这一点尤为重要。出于这个原因,我将向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会等待那个工人的所有要求完成,比如uWSGI"优雅的重装"功能确实如此,但似乎并非如此。我们开始遇到问题,其中一个线程会在Memcached中获取一个dogpile锁,然后当uWSGI由于另一个线程中的异常而重新加载worker时终止,这意味着锁从未被释放。

删除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,虽然我们可能会明确提交而不是为app拆卸编写我们自己的回调,因为我们很少写入数据库。< / p>

答案 1 :(得分:5)

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

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

它应该是这样的:

@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

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