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_SSLContextssl.SSLContext

patch时机正确,则直接从 gevent._ssl.SSLContext调用

根本原因

抛出这一例外的原因很明显。让我们来找出抛出异常的原因。

[En]

The reason for throwing the exception is clear. Let’s find out why the exception was thrown.

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

gunicorn启动的入口是 WSGIApplication().run()

WSGIApplication继承了 ApplicationApplication继承 BaseApplicationBaseApplication__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方法,则调用 Applicationrun方法

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

接着, Applicationrun方法最后会调用 Arbiterrun方法

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,最终导致了上面的问题

当发现问题时,让我们首先构造一个可重现的示例。

[En]

When the problem is found, let’s first construct an example that can be reproduced.

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

问题解决

Original: https://www.cnblogs.com/buxizhizhoum/p/16264833.html
Author: Go_Forward
Title: RecursionError: maximum recursion depth exceeded

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/499575/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球