【问题标题】:Invalid transaction persisting across requests跨请求持续存在的无效事务
【发布时间】:2014-06-11 16:34:05
【问题描述】:

总结

我们在生产中的一个线程遇到了一个错误,现在在每个带有它所服务的查询的请求上都产生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”错误的条目,指出如果会话开始提交但尚未完成,并且其他东西试图使用它,就会发生这种情况。我的猜测是这个线程中的会话从未到达self.session.remove() 步骤,现在它永远不会。

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

配置详情

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

2014 年 4 月 28 日更新:解决步骤

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

【问题讨论】:

  • 线程被搞砸了因为Session不是removed。就个人而言,我建议不要使用 Flask-SQLAlchemy:例如,这个 bug 将很难解决,如果你查看 github repo,你会发现它不再真正被维护了。此外,与普通的 SQLAlchemy 相比,它并没有为您提供更多功能。
  • @benselme:Flask-SQLAlchemy 确实 提供的一件重要的事情是请求范围的会话。当新请求到来时,无论旧会话是否被删除,都应该生成一个新会话。这就是这很奇怪的部分原因:如果有什么我们应该让数据库连接保持打开太久,而不是线程处于永久错误状态。我同意 Flask-SQLAlchemy 的安静程度很奇怪,但它由 Flask 作者编写的,所以我认为它实际上相当稳定。
  • @benselme,我对 Flask-SQLAlchemy 也有同样的感觉。并编写了我自己的SQLAlchemy to Flask 集成代码。最终,我遇到了与此问题中描述的相同的奇怪行为。

标签: python flask sqlalchemy uwsgi flask-sqlalchemy


【解决方案1】:

编辑 2016-06-05:

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

Flask PR 1822

2015-04-13 编辑:

谜团解开!

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

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

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

Flask 还有一个“应用程序”上下文的概念,它与请求上下文是分开的。它旨在支持诸如测试和 CLI 访问之类的东西,而 HTTP 没有发生。我知道这一点,我也知道 Flask-SQLA 将它的 DB 会话放在那里。

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

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

因此,如果应用上下文没有在请求结束时由于拆卸函数引发而弹出,它不仅会永远存在,甚至不会有新的应用上下文推到它上面。

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

也就是说,这确实意味着您必须绝对确保您的拆解函数成功,使用类似下面的 teardown-function 包装器。即使没有该功能以避免泄漏内存和数据库连接,这也是一个好主意,但鉴于这些发现,这一点尤其重要。出于这个原因,我将向 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 中获取狗堆锁,然后由于不同线程中的异常而在 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,尽管我们可能会明确提交而不是编写自己的应用程序拆解回调,因为我们很少写入数据库。

【讨论】:

  • 这确实是正确的答案和有效的解决方案。在我们的产品上进行了测试。
  • 我在一个低流量站点上遇到了类似的问题,结果数据库提供商在 60 秒时终止了 MySQL 连接以控制“逃跑进程”。默认情况下,SQL alchemy 不会回收连接以使用该行为。部分问题在于 Flask 以许多不同且微妙的方式管理上下文,并且没有正确检查应该进行清理/回收的部分。我的应用程序的解决方案是在我的配置中添加app.config['SQLALCHEMY_POOL_RECYCLE'] = 45 以在提供程序关闭连接之前关闭连接。默认为None。希望这对其他人有帮助!
  • 谁能告诉我截至 2018 年 10 月对于同样问题的相关答案是什么?
  • @sherelock 这已在 2016 年修复,可能有回归吗?可能值得在#pocoo IRC 频道中提问。 github.com/pallets/flask/blob/master/…
  • 对于仍然遇到此问题的任何人,原因很可能是循环导入。检查这个问题以获得避免它的简单方法:stackoverflow.com/questions/42909816/…
【解决方案2】:

令人惊讶的是,self.session.commit 周围没有异常处理。并且提交可能会失败,例如,如果与数据库的连接丢失。因此提交失败,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

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

【讨论】:

  • 我也是这么想的。至少我们会得到更多的控制权。不过有一些建议:当SQLALCHEMY_COMMIT_ON_TEARDOWN 为False 时,Flask-SQLA 的回调总是会删除会话,所以我们不应该这样做。我还会写一个 except SQLAlchemyError: 块,它只记录回溯而不是引发,因为我认为 Flask 不会做出任何努力来处理这些回调期间发生的异常。
  • +1 我也支持@VanessaPhipps 的两个建议。确实,session.remove() 如果重复调用是无害的,但是如果不将它放在您自己的回调中,您的代码不会给您一种错误的感觉,即事实上,Session 正在被删除。这种错误的假设可能会在稍后的调试过程中让你失望。同样,我也不会从拆卸回调中返回任何内容,因为它们的返回值被忽略了。
猜你喜欢
  • 2019-07-03
  • 1970-01-01
  • 2011-10-29
  • 2019-12-20
  • 1970-01-01
  • 1970-01-01
  • 2016-02-14
  • 1970-01-01
  • 2022-09-27
相关资源
最近更新 更多