A Graceful End - Python Flask/Gunicorn backend exit hook

basic idea

You can write the logout listener and logout service in the flask exit hook

Flask simple scene

  1. Flask does not have an app.stop() method

exit normally

  1. Python has built-in atexit library

“The atexit module defines functions to register and unregister cleanup functions. Functions thus registered are automatically executed upon normal interpreter termination. atexit runs these functions in the reverse order in which they were registered; if you register A, B, and C, at interpreter termination time, they will be run in the order C, B, A.”

command exit CTRL+C/kill

When using ctrl+C to close the server, another library can be calledsignal

Digression: You can handle some events by registering signal.signal, but there is no way to intercept signal.STOP, signal.SIGKILL (this kind of killing is implemented at the kernel level).

# wsgi.py
import signal

def exit_hook(signalNumber, frame):
    print("receiving", signalNumber)
    
if __name__ == "__main__":
    signal.signal(signal.SIGINT, exit_hook)
    app.run()
    
# 可以在结束时输出
# receiving 2

# 但是如果注册signal.SIGKILL/signal.SIGSTOP则代码会报错

The original text of the answer is as follows:

There is no way to handle SIGKILL

The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.

If you're looking to handle system shutdown gracefully, you should be handling SIGTERM or a startup/shutdown script such as an upstart job or a init.d script.


How to close Flask without Ctrl+C

from flask import request
def shutdown_server():
    func = request.environ.get('werkzeug.server.shutdown')
    if func is None:
        raise RuntimeError('Not running with the Werkzeug Server')
    func()
    
@app.get('/shutdown')def shutdown():
    shutdown_server()
    return 'Server shutting down...'
    
    
---------
from multiprocessing import Process

server = Process(target=app.run)
server.start()
# ...
server.terminate()
server.join()

But in fact the first method is outdated....

Reference: How To Create Exit Handlers for Your Python App


Gunicorn

Flask can use this directly, but what about multi-threaded and multi-process scenarios such as Gunicorn?

In the Gunicorn scenario, none of the following applies

Reference: how-to-configure-hooks-in-python-gunicorn

Summary: In the gunicorn scene, you can use the configs.py file to write all the configs in it, including various hooks (Hooks are also part of config), but the official document does not give any specific explanations about hooks, so you need to go to Check the source code.

Example:

# gunicorn_hooks_config.py
def on_starting(server):
    """
    Do something on server start
    """
    print("Server has started")


def on_reload(server):
    """
     Do something on reload
    """
    print("Server has reloaded")


def post_worker_init(worker):
    """
    Do something on worker initialization
    """
    print("Worker has been initialized. Worker Process id –>", worker.pid)

Run the file:

# wsgi.py
from app import app

if __name__ == '__main__':
    app.run()

run

gunicorn -c gunicorn_hooks_config.py wsgi:app

But the official documentation about this hook is too simple, nothing. No way, if you want to know how to write a function, you must start from the source code:

The entry Gunicorn code is as follows:

# gunicorn

#!/Users/xiao/opt/anaconda3/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from gunicorn.app.wsgiapp import run
if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
    sys.exit(run())
# wsgi.app.wsgiapp.py
def run():
    """\
    The ``gunicorn`` command line runner for launching Gunicorn with
    generic WSGI applications.
    """
    from gunicorn.app.wsgiapp import WSGIApplication
    WSGIApplication("%(prog)s [OPTIONS] [APP_MODULE]").run()
    
if __name__ == '__main__':
    run()

About loading configuration files

# app.base.py

...

class Application(BaseApplication):
    ...
    
    def get_config_from_filename(self, filename):

        if not os.path.exists(filename):
            raise RuntimeError("%r doesn't exist" % filename)

        ext = os.path.splitext(filename)[1]

        try:
            module_name = '__config__'
            if ext in [".py", ".pyc"]:
                spec = importlib.util.spec_from_file_location(module_name, filename)
            else:
                msg = "configuration file should have a valid Python extension.\n"
                util.warn(msg)
                loader_ = importlib.machinery.SourceFileLoader(module_name, filename)
                spec = importlib.util.spec_from_file_location(module_name, filename, loader=loader_)
            mod = importlib.util.module_from_spec(spec)
            sys.modules[module_name] = mod
            spec.loader.exec_module(mod)
        except Exception:
            print("Failed to read config file: %s" % filename, file=sys.stderr)
            traceback.print_exc()
            sys.stderr.flush()
            sys.exit(1)

        return vars(mod)

In Arbiter.py, there will be an Arbiter class responsible for generating and killing workers. It can be said that Arbiter is the main manager of Workers inside Gunicorn. But still, the worker class is directly called self.cfg.worker_class

Gunicorn uses paste.deploy.loadapp in this framework, so this is also very important.

specific process:

wsgiapplication runs -> detects configs.py -> importlib.util.spec_from_file_location returns as __config__ module -> the module becomes app.cfg -> cfg passed to Arbiter -> Worker class registers signal_handler -> calls self in the method .cfg.worker_abort(self) -> the method with the same name

# gunicorn/workers/base.py
class Worker(object):
    def init_signals(self):
        # reset signaling
        for s in self.SIGNALS:
            signal.signal(s, signal.SIG_DFL)
        # init new signaling
        signal.signal(signal.SIGQUIT, self.handle_quit)
        signal.signal(signal.SIGTERM, self.handle_exit)
        signal.signal(signal.SIGINT, self.handle_quit)
        signal.signal(signal.SIGWINCH, self.handle_winch)
        signal.signal(signal.SIGUSR1, self.handle_usr1)
        signal.signal(signal.SIGABRT, self.handle_abort)

        # Don't let SIGTERM and SIGUSR1 disturb active requests
        # by interrupting system calls
        signal.siginterrupt(signal.SIGTERM, False)
        signal.siginterrupt(signal.SIGUSR1, False)

        if hasattr(signal, 'set_wakeup_fd'):
            signal.set_wakeup_fd(self.PIPE[1])
    def handle_abort(self, sig, frame):
        self.alive = False
        self.cfg.worker_abort(self)
        sys.exit(1)

That is to say, the def worker_abort(worker) method written in configs.py must be a method that receives a Worker class parameter.

What about the server? With the idea of ​​calling the original name, it is easy to find it. Just search for on_exit and find that the server is the Arbiter class. The calling scene is as follows in line 348:

class Arbiter(Object):
    def halt(self, reason=None, exit_status=0):
        """ halt arbiter """
        self.stop()
        self.log.info("Shutting down: %s", self.master_name)
        if reason is not None:
            self.log.info("Reason: %s", reason)
        if self.pidfile is not None:
            self.pidfile.unlink()
        self.cfg.on_exit(self)
        sys.exit(exit_status)

Summarize:

The correct way to write these documents should be

# 注意此处lib的拼写错误是代码中的,不能自己改成init
def worker_int(worker: gunicorn.workers.Worker) -> None:
    ...

# 可以有返回值,但是返回值不会被调用
    
def on_exit(server: gunicorn.Arbiter) -> None:
    pass

It seems that in Gunicorn, the Arbiter name is hidden and renamed to SERVER (do not understand, confusing behavior)

 

 

Guess you like

Origin blog.csdn.net/xiaozoom/article/details/128149667