问题介绍

为方便理解,简单说明一下项目,项目中使用的依赖模块有:flask,flask-sqlalchemy,flask-celery等等。

在同步方式调用task函数的时候出现了DetachedInstanceError的异常。出错的代码如下(已简化):

def func():user = User.query.first()task_func()print(user.id)@celery.task
def task_func():pass

在访问user的id属性时报错,报错如下:

sqlalchemy.orm.exc.DetachedInstanceError: Instance <User at 0x7fb780d40da0> is not bound to a Session; attribute refresh operation cannot proceed (Background on this error at: http://sqlalche.me/e/bhk3)

查找问题的原因

根据错误提示查找原因,发现原因是:session被关闭导致对象和session失去关联,当对象属性需要加载时则会加载失败。

在代码中并没有调用session.close(),那为什么session会被关闭呢?猜测可能是因为线程切换导致的,因为在flask_sqlalchemy中使用的是scoped_session,那么不同线程的session对象是不同的,所以线程切换有可能导致session关闭。

下面测试同步调用task函数时线程是否进行了切换:

from threading import current_threaddef func():print(current_thread)task_func()print(current_thread)@celery.task
def task_func():print(current_thread)

执行结果:

<function current_thread at 0x7f42af472268>
<function current_thread at 0x7f42af472268>
<function current_thread at 0x7f42af472268>

执行结果证明并没有进行线程切换。

那么回到之前的问题,是什么导致了session关闭。再看一下出错代码,正常的数据库查询和函数调用是不太会有问题的,所以只有task装饰器比较可疑。尝试将装饰器移除再执行发现并没有报错,那么原因应该就在这个task装饰器中。

查阅了一些资料以及分析task装饰器的源码后,发现在task装饰器中会创建新的应用上下文对象。代码如下:

class ContextTask(task_base):"""Celery instance wrapped within the Flask app context."""def __call__(self, *_args, **_kwargs):with app.app_context():return task_base.__call__(self, *_args, **_kwargs)

在出错代码中去除装饰器后模拟创建应用上下文的行为:

def func():user = User.query.first()task_func()print(user.id)def task_func():from flask import current_appwith current_app.app_context():pass

执行后发现会出现同样的异常,则代表异常是这段代码导致的。

继续分析session是在什么地方关闭的,这个Flask对象的应用上下文结束时会执行一些清理操作,代码如下:

class AppContextt(object):def pop(self, exc=_sentinel):"""Pops the app context."""try:self._refcnt -= 1if self._refcnt <= 0:if exc is _sentinel:exc = sys.exc_info()[1]self.app.do_teardown_appcontext(exc)finally:rv = _app_ctx_stack.pop()assert rv is self, 'Popped wrong app context.  (%r instead of %r)' \% (rv, self)appcontext_popped.send(self.app)def __exit__(self, exc_type, exc_value, tb):self.pop(exc_value)if BROKEN_PYPY_CTXMGR_EXIT and exc_type is not None:reraise(exc_type, exc_value, tb)

这里的do_teardown_appcontext()会调用被teardown_appcontext装饰的函数,代码如下:

class Flask(_PackageBoundObject):@setupmethoddef teardown_appcontext(self, f):self.teardown_appcontext_funcs.append(f)return fdef do_teardown_appcontext(self, exc=_sentinel):if exc is _sentinel:exc = sys.exc_info()[1]for func in reversed(self.teardown_appcontext_funcs):func(exc)appcontext_tearing_down.send(self, exc=exc)

而在使用flask_sqlalchemy创建Sqlalchemy对象时会注册一个teardown函数

class SQLAlchemy(object):def init_app(self, app):...@app.teardown_appcontextdef 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

由于项目中配置了SQLALCHEMY_COMMIT_ON_TEARDOWN=True,所以在应用上下文结束时self.session.commit()和self.session.remove()这两行代码都会被执行。

session的remove方法会调用close方法。close方法会调用expunge_all(),并释放所有事务/连接资源。而expunge_all方法将会所有对象从session中移除。

验证session关闭后对象是否从session中移除:

def func():user = User.query.first()print(db.session.identity_map.values())db.session.close()print(db.session.identity_map.values())print(user.id)

执行结果证明session关闭后对象确实不在session中了,但是访问对象属性并没有报错,说明仅仅session关闭并不会导致异常。

继续查阅资料发现,commit方法会将所有对象过期,当再次调用对象时会重新去数据库中查询。我们可以通过查看obj._sa_instance_state.expired属性可以查看对象是否过期,打开SQLALCHEMY_ECHO配置可以在执行sql时打印日志,验证代码如下:

def func():user = User.query.first()db.session.commit()print(user._sa_instance_state.expired)print(user.id)

执行结果为对象的expired值为False,访问对象属性会重新执行查询sql。

最后一起调用session.commit()和session.close()进行测试:

def func():user = User.query.first()db.session.commit()db.session.close()print(user._sa_instance_state.expired)print(db.session.identity_map.values())print(user.id)

执行结果为对象过期并且session中的对象列表为空,访问对象属性时报错。到这里出现异常的原因已经很明显了。

原因总结

同步调用task函数时会创建新的应用上下文,即app.app_context()。在函数调用结束时,应用上下文也会结束,应用上下文结束时会调用sqlalchemy的teardown函数。其中一个由flask_sqlalchemy注册的teardown函数中会调用session.commit()和session.remove(),commit会让对象过期,remove会移除session中的所有对象。这时去访问对象属性则需要会重新从db查询,但是对象已经没有关联的session了,故无法查询导致报错。

解决方法

  1. sqlalchemy初始化时增加参数expire_on_commit=False,这样在commit之后就不会将对象置为过期。
  2. 在调用task函数前使用session.expunge_all(),将对象和session的关系断开,这样对象就不会过期了。
  3. 在调用完task函数时使用db.session.add(obj),将对象再次加入session,这样访问对象属性时就会重新加载了。

推荐使用第一种方法,因为这种方法改起来比较方便,后续代码也不用做特殊处理。

参考文档

http://wiki.mchz.com.cn/pages/viewpage.action?pageId=25069046

https://stackoverflow.com/questions/30347090/pushing-celery-task-from-flask-view-detach-sqlalchemy-instances-detachedinstanc/30348496#30348496

https://blog.csdn.net/yangxiaodong88/article/details/82458769

http://blog.0x01.site/2016/10/25/从SQLAlchemy的ObjectDeletedError到SQLAlchemy的对象刷新机制/

https://docs.sqlalchemy.org/en/13/orm/session_api.html?highlight=commit#sqlalchemy.orm.session.Session.commit

在flask中同步调用celery.task函数报错分析相关推荐

  1. ios .mm文件调用c语言函数报错,深入浅出 iOS 编译

    前言 两年前曾经写过一篇关于编译的文章<iOS编译过程的原理和应用>,这篇文章介绍了iOS编译相关基础知识和简单应用,但也很有多问题都没有解释清楚: Clang和LLVM究竟是什么 源文件 ...

  2. rf调用的python函数报错_Robot Framework(15)- 扩展关键字

    如果你还想从头学起Robot Framework,可以看看这个系列的文章哦! 前言 什么是扩展关键字?就是你自己写的 Python 文件,里面包含了函数或者类 然后 RF 导入这个 Python 模块 ...

  3. jersey tomcat MySQL_tomcat web容器中,调用jersey client端报错的处理

    在web工程中,写main方法,运行ok. 发布到tomcat中后,报错. javax.ws.rs.core.UriBuilder.uri(Ljava/lang/String;)Ljavax/ws/r ...

  4. python3中调用map函数报错map object at 0x000001EF004D97B8

    python3中调用map函数报错<map object at 0x000001EF004D97B8> 在python中这样的一段代码报错: a=map(int,input().split ...

  5. 关于vs2010调用python中Py_Initialize函数报错的原因

    程序刚开始运行到Py_Initialize()函数,报错:ImportError: No module named site 如遇到这个函数报错那么基本上就是你没有将相关路径添加到环境变量中. 两个方 ...

  6. 2021-06-10 工作笔记 ng-change不能触发的解决方法+wui-date日期选择器使用onchange调用函数报错

    当通过JavaScript来修改ng-model的值,ng-change不能触发 <input type="number" ng-model="testNumber ...

  7. php的old函数,laravel单元测试之phpUnit中old()函数报错解决

    php 的 laravel单元测试之phpUnit中old()函数报错解决 前言 最近在做laravel单元测试.遇到了一个问题: 当添加的view里面使用old()函数时就会报错,正常url访问没问 ...

  8. Oracle中wm_concat函数报错解决方法

    Oracle中wm_concat函数报错解决方法 参考文章: (1)Oracle中wm_concat函数报错解决方法 (2)https://www.cnblogs.com/52net/archive/ ...

  9. 解决VS2017中使用scanf函数报错的问题

    解决VS2017中使用scanf函数报错的问题 参考文章: (1)解决VS2017中使用scanf函数报错的问题 (2)https://www.cnblogs.com/tanghaiyong/p/11 ...

最新文章

  1. Qt ffmpeg环境搭建
  2. CSS 从入门到放弃系列:CSS的引入方式
  3. 迁移学习 nlp_NLP的发展-第3部分-使用ULMFit进行迁移学习
  4. Git安装步骤+Mac终端配置
  5. halcon rectangle1_domain缩减图像域为矩形
  6. DB级buffer与IO 查看
  7. BASE64Encoder
  8. 微信公众平台开发(四)——微信支付
  9. 数据结构设计_数据结构算法设计题学起来很困难怎么破
  10. 图的遍历(广度优先遍历)- 数据结构和算法61
  11. python情感分析词典_基于情感词典的文本情感分析
  12. svn linux客户端使用教程,linux svn 客户端安装配置
  13. 用户调研的操作步骤与过程模板
  14. DW——验证注册页面 设计JavaScript
  15. 使用Java编写一个简单的Web的监控系统cpu利用率,cpu温度,总内存大小
  16. 【华为机试真题 Python】勾股数元祖
  17. 罗永浩两年还债4个亿,我却被《真还传》圈粉
  18. 小写数字转大写金额php,php 金额小写数字转大写汉字
  19. 华为服务器系统重装,华为服务器 重装系统
  20. Android仿微信拍摄、录制视频,以及视频播放(基于JCameraView和GSYVideoPlayer)

热门文章

  1. 写在2016的尾巴上
  2. 如何写出高质量的文章:从战略到战术
  3. 计算机与网络之间是红叉,雨林木风win7系统网络连接出现红叉无法上网的解决办法...
  4. asp实现注册登录界面_asp.net 实现用户登录和注册——基于webform模式
  5. JAVA JNI调用科大讯飞离线语音合成(Linux篇)
  6. 守护线程daemon
  7. 计算机基础知识电大形考任务5,国家开放大学电大专科《计算机平面设计(1)》网络课形考任务4及任务5答案.doc...
  8. 项目经理如何成功地分配任务?| 每天成就更大成功
  9. 使用环信sdk做一个简单的聊天APP
  10. python 语音识别培训使用Python和Keras创建简单语音识别引擎