RecursionError: maximum recursion depth exceeded

环境

python 版本3.6.4

gevent 1.5.0

gunicorn 20.1.0

错误

RecursionError: maximum recursion depth exceeded while calling a Python object

错误原因

根据错误栈,出问题的代码在python官方ssl包ssl.py第465行,具体代码

 class SSLContext(_SSLContext):

@property
def options(self):
return Options(super().options)

@options.setter
def options(self, value):
# 这就是抛错的代码
super(SSLContext, SSLContext).options.__set__(self, value)

在对

SSLContext
实例设置option属性的时候,会调用到
super(SSLContext, SSLContext).options.__set__(self, value)

问题的原因在于先导入了

ssl
包,然后才进行了gevent patch,这样上面这一行代码中的
SSLContext
实际上已经被patch成了
gevent._ssl3.SSLContext

gevent._ssl3.SSLContext
相关的代码如下

 class SSLContext(orig_SSLContext):
@orig_SSLContext.options.setter
def options(self, value):
super(orig_SSLContext, orig_SSLContext).options.__set__(self, value)

gevent._ssl3.SSLContext
中继承的
orig_SSLContext
就是python官方的
ssl.SSLContext

所以整体的逻辑就变成了

1.

super(SSLContext, SSLContext).options.__set__(self, value)

2.由于已经经过了patch,所以

SSLContext
实际上是
gevent._ssl3.SSLContext
,那么
super(SSLContext, SSLContext).options.__set__(self, value)
实际上是
super(gevent._ssl3.SSLContext, gevent._ssl3.SSLContext).options.__set__(self, value)

3.由于gevent继承了

ssl.SSLContext
所以会调用到
SSLContext的options.setter
方法,这样就回到了1,在这里开始了无限递归

所以patch时机不对,导致调用

SSLContext
实际是调用了
gevent._ssl.SSLContext

如果先patch再导入,则自始至终都是

gevent._ssl3.SSLContext
,调用的代码变成
super(orig_SSLContext, orig_SSLContext).options.__set__(self, value)

orig_SSLContext
ssl.SSLContext

patch时机正确,则直接从

gevent._ssl.SSLContext
调用

根本原因

抛出异常的原因清楚了,我们再来找找为什么会抛出这个异常

先看gunicorn的启动顺序,为了清晰,我省略了无关的代码,只列出了和启动相关的代码

gunicorn启动的入口是

WSGIApplication().run()

WSGIApplication
继承了
Application
Application
继承
BaseApplication
BaseApplication
__init__
方法中调用了
self.do_load_config()
进行配置加载

首先,进行初始化,在__init__中调用了这个方法

 def do_load_config(self):
"""
Loads the configuration
"""
try:
# 对cfg进行初始化,读取配置
self.load_default_config()
# 加载配置文件
self.load_config()
except Exception as e:
print("\nError: %s" % str(e), file=sys.stderr)
sys.stderr.flush()
sys.exit(1)

self.do_load_config()
调用
self.load_default_config()
self.load_config()

对cfg进行初始化

接着,调用

run
方法,
WSGIApplication
没有实现
run
方法,则调用
Application
run
方法

 def run(self):
if self.cfg.print_config or self.cfg.check_config:
try:
# 在这里加载app
self.load()
except Exception:
sys.exit(1)
sys.exit(0)

# 这里会调用Arbiter的run方法
super().run()

可以看到调用了

self.load()

接着看

load
方法

 def load(self):
if self.cfg.paste is not None:
return self.load_pasteapp()
else:
# 我们目前走这里
return self.load_wsgiapp()

所以

load
这里加载了我们的app

接着,

Application
run
方法最后会调用
Arbiter
run
方法

 def run(self):
"Main master loop."
self.start()
util._setproctitle("master [%s]" % self.proc_name)

try:
# 这里处理worker
self.manage_workers()
# 省略部分代码
except Exception:
sys.exit(-1)

启动worker最终会调用

spawn_worker

 def spawn_worker(self):
self.worker_age += 1
# 在配置中设置的worker class
worker = self.worker_class(self.worker_age, self.pid, self.LISTENERS,
self.app, self.timeout / 2.0,
self.cfg, self.log)
# 省略部分代码
try:
# 这里初始化,对gevent而言,初始化的时候,才会进行patch
worker.init_process()
sys.exit(0)
except SystemExit:
raise

worker的

init_process
方法如下

 def init_process(self):
# 在这里调用patch
self.patch()
hub.reinit()
super().init_process()

self.patch()
的实现

 def patch(self):
# 在这里进行patch
monkey.patch_all()

综上,gunicorn启动的时候,加载顺序为:

配置文件加载 -> app加载 -> worker初始化

此外我们还发现,在gunicorn处理config的时候,在

gunicorn.config
中导入了
ssl
包,所以在worker初始化之前
ssl
包已经被导入了,后面的patch又把
ssl
包patch成了
gevent._ssl3
,最终导致了上面的问题

复现

问题找到,我们先构造一个可以复现的例子

app.py

 from flask import Flask
import requests

app = Flask(__name__)

from requests.packages.urllib3.util.ssl_ import create_urllib3_context
ctx = create_urllib3_context()


@app.route("/test")
def test():
requests.get("https://www.baidu.com")
return "test"


if __name__ == "__main__":
app.run(debug=True)

启动命令

 gunicorn -w 2 --worker-class gevent --preload -b 0.0.0.0:5000 app:app 

现在当我们启动后,调用http://127.0.0.1:5000/test 就会触发

RecursionError

解决

既然问题在于ssl包导入之后才进行patch,那么我们前置patch即可,考虑到配置文件加载在加载app之前,如果我们在配置文件加载时patch,则是目前能够找到的最早的patch时机。

配置文件gunicorn_config.py

 import gevent.monkey
gevent.monkey.patch_all()

workers = 8

启动命令

gunicorn --config config.py --worker-class gevent --preload -b 0.0.0.0:5000 app:app

问题解决

标签: python

添加新评论