Fix TypeError: call() takes 2 positional arguments but 3 were given

gunicorn启动一个tornado服务时报了个错:

1
2
3
4
5
6
7
8
9
[ERROR] [MainThread] (http1connection:67) Uncaught exception
Traceback (most recent call last):
  File "/Users/ferstar/.pyenv/versions/scriber/lib/python3.6/site-packages/tornado/http1connection.py", line 273, in _read_message
    delegate.finish()
  File "/Users/ferstar/.pyenv/versions/scriber/lib/python3.6/site-packages/tornado/httpserver.py", line 280, in finish
    self.request_callback(self.request)
  File "/Users/ferstar/.pyenv/versions/scriber/lib/python3.6/site-packages/tornado/wsgi.py", line 114, in __call__
    WSGIContainer.environ(request), start_response
TypeError: __call__() takes 2 positional arguments but 3 were given

调试发现是这行代码的问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
        # Assume the app is a WSGI callable if its not an
        # instance of tornado.web.Application or is an
        # instance of tornado.wsgi.WSGIApplication
        app = self.wsgi
        if tornado.version_info[0] < 6:
            if not isinstance(app, tornado.web.Application) or \
            isinstance(app, tornado.wsgi.WSGIApplication):
                app = WSGIContainer(app)
        elif not isinstance(app, WSGIContainer):
            app = WSGIContainer(app)

我用的 tornado 版本是6.1, 可以看到, web.ApplicationWSGIContainer包了一层, 实际上tornado6.0以后的版本中有意在剥离WSGI的支持, 所以比较苟的一个解决方法是退回到6.0之前的版本, 比如5.1.1, 即可正常; 然而作为一个有追求的攻城狮, 怎么能够因为一个小小的兼容问题就退版本号呢, 我选择硬肛, 既然gunicorn自己的tornado worker不能用, 那就照抄另写一个:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.
# filename: gtornado.py

import copy
import gettext
import logging.config
import os
import sys

from gunicorn.workers.base import Worker
import tornado
import tornado.httpserver
import tornado.web
from tornado.ioloop import IOLoop, PeriodicCallback

class TornadoWorker(Worker):
    def handle_exit(self, sig, frame):
        if self.alive:
            super().handle_exit(sig, frame)

    def handle_request(self):
        self.nr += 1
        if self.alive and self.nr >= self.max_requests:
            self.log.info("Autorestarting worker after current request.")
            self.alive = False

    def watchdog(self):
        if self.alive:
            self.notify()

        if self.ppid != os.getppid():
            self.log.info("Parent changed, shutting down: %s", self)
            self.alive = False

    def heartbeat(self):
        if not self.alive:
            if self.server_alive:
                if hasattr(self, 'server'):
                    try:
                        self.server.stop()
                    except Exception:  # pylint: disable=broad-except
                        pass
                self.server_alive = False
            else:
                for callback in self.callbacks:
                    callback.stop()
                self.ioloop.stop()

    def init_process(self):
        # IOLoop cannot survive a fork or be shared across processes
        # in any way. When multiple processes are being used, each process
        # should create its own IOLoop. We should clear current IOLoop
        # if exists before os.fork.
        IOLoop.clear_current()
        super().init_process()

    def run(self):
        self.ioloop = IOLoop.current()
        self.alive = True
        self.server_alive = False

        self.callbacks = []
        self.callbacks.append(PeriodicCallback(self.watchdog, 1000))
        self.callbacks.append(PeriodicCallback(self.heartbeat, 1000))
        for callback in self.callbacks:
            callback.start()

        # Assume the app is a WSGI callable if its not an
        # instance of tornado.web.Application or is an
        # instance of tornado.wsgi.WSGIApplication
        app = self.wsgi

        # Monkey-patching HTTPConnection.finish to count the
        # number of requests being handled by Tornado. This
        # will help gunicorn shutdown the worker if max_requests
        # is exceeded.
        httpserver = sys.modules["tornado.httpserver"]
        if hasattr(httpserver, 'HTTPConnection'):
            old_connection_finish = httpserver.HTTPConnection.finish

            def finish(other):
                self.handle_request()
                old_connection_finish(other)

            httpserver.HTTPConnection.finish = finish
            sys.modules["tornado.httpserver"] = httpserver

            server_class = tornado.httpserver.HTTPServer
        else:

            class _HTTPServer(tornado.httpserver.HTTPServer):
                def on_close(instance, server_conn):  # pylint: disable=no-self-argument
                    self.handle_request()
                    super(_HTTPServer, instance).on_close(server_conn)

            server_class = _HTTPServer

        app_params = {
            "max_buffer_size": 200 * 1024 * 1024,  # 200MB
            "decompress_request": True,
        }
        if self.cfg.is_ssl:
            _ssl_opt = copy.deepcopy(self.cfg.ssl_options)
            # tornado refuses initialization if ssl_options contains following
            # options
            del _ssl_opt["do_handshake_on_connect"]
            del _ssl_opt["suppress_ragged_eofs"]
            app_params["ssl_options"] = _ssl_opt
        server = server_class(app, **app_params)

        self.server = server
        self.server_alive = True

        for socket in self.sockets:
            socket.setblocking(0)
            server.add_socket(socket)

        server.no_keep_alive = self.cfg.keepalive <= 0
        server.start(num_processes=1)

        self.ioloop.start()

主要就是直接丢掉WSGIContainer, 使用tornado.web.Application, 然后运行:

1
gunicorn -k gtornado.TornadoWorker main:app -b 0.0.0.0:8080 --graceful-timeout 120 --timeout 600

发送个HUP信号看看反应, 嗯, 顺利重载

1
2
3
4
5
6
7
8
[INFO] Starting gunicorn 20.1.0
[INFO] Listening at: http://0.0.0.0:8080 (51702)
[INFO] Using worker: gtornado.TornadoWorker
[INFO] Booting worker with pid: 51756
[INFO] Handling signal: hup  # 收到信号
[INFO] Hang up: Master
[INFO] Booting worker with pid: 52640  # 顺利重载
[INFO] Worker exiting (pid: 51756)

附上测试代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# filename: main.py
import asyncio
from tornado.web import Application, RequestHandler


class MainHandler(RequestHandler):
    def get(self):
        self.write("Hello, world")


class LongPollHandler(RequestHandler):
    async def get(self):
        lines = ['line 1\n', 'line 2\n']

        for line in lines:
            self.write(line)
            await self.flush()
            await asyncio.sleep(0.5)
        await self.finish()


app = Application([
    (r"/", MainHandler),
    (r"/longpoll", LongPollHandler)
])
# NOTE: I am not responsible for any expired content.
create@2021-03-23T01:54:07+08:00
update@2021-11-09T11:07:43+08:00
comment@https://github.com/ferstar/blog/issues/39
加载评论