Back to home

Flask 如何实现 auto reloader

简介

对于长时间执行的任务,我们不可能将其放在一个请求中完成,通常会用其他的方式异步执行。 队列则就是这样一个专门提供非阻塞任务执行的系统组件。在开发过程中我需要对其异步执行的任务进行测试,由于 Celery 没有提供Inprocess的API,所以不能直接在测试、开发的时候调用需要一番Tricky手段。由于Celery需要阻塞线程执行,无法直接作为后台任务,我们还需要在应用开始时,将其使用另外一个线程开启。

Flask Auto Reloader

Flask_ 的开发者模式(准确的来说是 WerkZeug_ 作为基础类库提供的)提供了许多工具,比如基于页面的Debuger提供网页版的Shell。但是开发模式会默认将auto_reloader开启,好处是不用在修改逻辑后手工重启,坏处是他不是直接启动WebServer,而是从子线程中执行WebServer后,主线程执行一个本地文件Watcher,在文件发生变动时重启。

下图为调试模式下执行逻辑

3ea2c2f97dd249fdf1e6d913d27e7aed.jpe

CheckAndReloader过程中,一旦发现有.py[co]文件发生变更,则另外一个子进程,原进程退出。

为了复用逻辑,首次运行时不会直接执行 WebServer 部分,而是设置好环境变量WERKZEUG_RUN_MAIN后,进入 sMain 流程。下面为 WerkZeug serving.py 部分代码:

def run_with_reloader(main_func, extra_files=None, interval=1):
    """Run the given function in an independent python interpreter."""
    import signal
    signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))
    if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
        thread.start_new_thread(main_func, ())
        try:
            reloader_loop(extra_files, interval)
        except KeyboardInterrupt:
            return
    try:
        sys.exit(restart_with_reloader())
    except KeyboardInterrupt:
        pass

问题

麻烦在于,每一个在 Module 构建时创建对象,都会因为子进程的原因创建两次。

解决

不过既然知道了问题,我们屏蔽掉第一次执行母进程时的相关创建逻辑就行。
接下来问题就转换为如何判断当前进程是否为子进程的问题。 方法有很多,比如根据系统环境变量来WERKZEUG_RUN_MAIN来判断。或者比较父进程组ID,因为子进程所属于父进程组,所以可以根据如下方法判断。

# avoid Werbzeg's `run(use_reloader=True)` will create subprocess
if os.getppid() != os.getpgrp():
    __start_celelry_worker()

Conclusion

虽然解决了 Module 作用域下变量创建在 WerkZeug 调试模式中创建两次的问题,但是就问题本身来说解决的很不优美。
Flask 插件通常会绑定到 Flask.app 创建之后,通过一个工厂方法调用。项目中的例子不是标准的 Flask 插件,所以会发生这类问题。如果调试模式增加一个 Hook 用于*正常逻辑*的执行也能解决这个问题。究其原因毕竟时基于 Flask 应用模块生命周期问题,Flask 已经提供了一套严禁的API,按照它提供的思路执行就是。所谓逆我者亡,也只能怪自己只猜中这开头,猜不中这结局。

很多同学的 SqlalchemyFlask 结合的问题也通常卡在维护 Session 生命周期上。
选择一款框架绝对不是拿来主义,更多的是对原框架理念的一种赞同。

最后我还是考虑 Mock 掉这些API吧,哈哈。

References