odoo源码阅读之自动重载的实现

Author Avatar
呃哦 12月 13, 2018

在阅读源码的时候发现原来odoo是有重载代码的实现的, 加上上一篇的模块远程部署更新功能,组合可以实现在终端上重载服务器了.

odoo服务器启动过程

在odoo的server启动过程中, 首先是执行cli的main方法解析命令参数,默认则是启动服务器.

# odoo/cli/command.py:33
def main():
    # ....此处省略若干代码
    # 可以看到默认如果第一个参数不是-或者--开头的参数则是server命令启动服务器
    command = "server"
    # ....此处省略若干代码
    if command in commands:
        o = commands[command]()
        o.run(args)
    else:
        sys.exit('Unknow command %r' % (command,))

接下来看服务器的run方法, odoo/cli/server.py 的run方法直接是执行main函数,因此看main函数.

# odoo/cli/server.py
def main(args):
    # 检查运行环境和解析配置
    check_root_user()
    odoo.tools.config.parse_config(args)
    check_postgres_user()
    report_configuration()

    config = odoo.tools.config

    csv.field_size_limit(500 * 1024 * 1024)

    # 如果有指定数据库,数据库不存在则创建
    preload = []
    if config['db_name']:
        preload = config['db_name'].split(',')
        for db_name in preload:
            try:
                odoo.service.db._create_empty_database(db_name)
            except ProgrammingError as err:
                if err.pgcode == errorcodes.INSUFFICIENT_PRIVILEGE:
                    _logger.info("Could not determine if database %s exists, "
                                 "skipping auto-creation: %s", db_name, err)
                else:
                    raise err
            except odoo.service.db.DatabaseExists:
                pass

    # 导入导出翻译
    if config["translate_out"]:
        export_translation()
        sys.exit(0)

    if config["translate_in"]:
        import_translation()
        sys.exit(0)

    # 指定多进程
    if config['workers']:
        odoo.multi_process = True

    stop = config["stop_after_init"]

    setup_pid_file()
    # 敲重点,服务器开始运行
    rc = odoo.service.server.start(preload=preload, stop=stop)
    sys.exit(rc)

odoo服务器启动:

# odoo/service/server.py
def start(preload=None, stop=False):
    """ Start the odoo http server and cron processor.
    """
    global server

    load_server_wide_modules()
    odoo.service.wsgi_server._patch_xmlrpc_marshaller()

    # 选择服务器类型, gevent服务器 or 多进程 or 多线程
    if odoo.evented:
        server = GeventServer(odoo.service.wsgi_server.application)
    elif config['workers']:
        if config['test_enable'] or config['test_file']:
            _logger.warning("Unit testing in workers mode could fail; use --workers 0.")

        server = PreforkServer(odoo.service.wsgi_server.application)

        # Workaround for Python issue24291, fixed in 3.6 (see Python issue26721)
        if sys.version_info[:2] == (3,5):
            # turn on buffering also for wfile, to avoid partial writes (Default buffer = 8k)
            werkzeug.serving.WSGIRequestHandler.wbufsize = -1
    else:
        server = ThreadedServer(odoo.service.wsgi_server.application)

    # 敲重点, 监视文件变化从而自动重载
    watcher = None
    if 'reload' in config['dev_mode']:
        if watchdog:
            watcher = FSWatcher()
            watcher.start()
        else:
            _logger.warning("'watchdog' module not installed. "
                            "Code autoreload feature is disabled")
    if 'werkzeug' in config['dev_mode']:
        server.app = DebuggedApplication(server.app, evalex=True)

    # 敲重点, 启动服务器
    rc = server.run(preload, stop)

    # 如果标志 phoenix 为 true 则在服务器退出后重新载入
    # like the legend of the phoenix, all ends with beginnings
    if getattr(odoo, 'phoenix', False):
        if watcher:
            watcher.stop()
        _reexec()

    return rc if rc else 0

watchdog实现文件内容更改则reload服务器

在上面的服务器启动过程中, 可以看到, 如果我们配置了dev_mode包含了reload选项, 并且watchdog变量不为空的话, 则会启动watcher观察文件系统.那么, watchdog在哪导入的呢,在源码中有:

# odoo/service/server.py
try:
    import watchdog
    from watchdog.observers import Observer
    from watchdog.events import FileCreatedEvent, FileModifiedEvent, FileMovedEvent
except ImportError:
    watchdog = None

可以看到, 尝试导入 watchdog , 导入失败则为None. 为此我专门去odoo的 requirements.txt 看了下, 并没有 watchdog 这个依赖, 当然, pip install watchdog 就好了, 但是官方这么做是为啥呢? 一脸黑线🌚

接下来就是watchdog的事件处理了:

class FSWatcher(object):
    def __init__(self):
        self.observer = Observer()
        for path in odoo.modules.module.ad_paths:
            _logger.info('Watching addons folder %s', path)
            # 注册为自己为watchdog的事件处理
            self.observer.schedule(self, path, recursive=True)

    def dispatch(self, event):
        if isinstance(event, (FileCreatedEvent, FileModifiedEvent, FileMovedEvent)):
            if not event.is_directory:
                path = getattr(event, 'dest_path', event.src_path)
                # 如果是py代码则尝试导入, try 没有语法错误 则 重启服务器
                if path.endswith('.py') and not os.path.basename(path).startswith('.~'):
                    try:
                        source = open(path, 'rb').read() + b'\n'
                        compile(source, path, 'exec')
                    except FileNotFoundError:
                        _logger.error('autoreload: python code change detected, FileNotFound for %s', path)
                    except SyntaxError:
                        _logger.error('autoreload: python code change detected, SyntaxError in %s', path)
                    else:
                        if not getattr(odoo, 'phoenix', False):
                            _logger.info('autoreload: python code updated, autoreload activated')
                            restart()

    def start(self):
        self.observer.start()
        _logger.info('AutoReload watcher running')

    def stop(self):
        self.observer.stop()
        self.observer.join()

__init__ 有个有点意思的是 self.observer.schedule(self, path, recursive=True) 代码, 查看 schedule 函数签名可以看到对于第一个参数 event_handler 的类型是要求:

`watchdog.events.FileSystemEventHandler` or a subclass

watchdog.events.FileSystemEventHandler 的核心就是 dispatch 方法.

看来源码这用的是动态语言的鸭子模型了. (这段有点啰嗦了)

信号处理重载服务器

在odoo服务器启动过程源码中, 可以看到odoo的服务器类型有三种: gevent, 多进程, 多线程.

而默认的就是多线程版本。在多线程版本的start代码中,有一段信号注册代码:

    def start(self, stop=False):
        _logger.debug("Setting signal handlers")
        if os.name == 'posix':
            signal.signal(signal.SIGINT, self.signal_handler)
            signal.signal(signal.SIGTERM, self.signal_handler)
            signal.signal(signal.SIGCHLD, self.signal_handler)
            signal.signal(signal.SIGHUP, self.signal_handler)
            signal.signal(signal.SIGQUIT, dumpstacks)
            signal.signal(signal.SIGUSR1, log_ormcache_stats)
        elif os.name == 'nt':
            import win32api
            win32api.SetConsoleCtrlHandler(lambda sig: self.signal_handler(sig, None), 1)

        test_mode = config['test_enable'] or config['test_file']
        if test_mode or (config['http_enable'] and not stop):
            # some tests need the http deamon to be available...
            self.http_spawn()

该代码表示, 如果系统内核是posix(Uni*系统),则注册一系列信号处理。

如果是nt内核(Windows系统),则将事件默认交给signal_handler 函数处理。

接下来就看对应的信号处理函数:

    def signal_handler(self, sig, frame):
        if sig in [signal.SIGINT, signal.SIGTERM]:
            # shutdown on kill -INT or -TERM
            self.quit_signals_received += 1
            if self.quit_signals_received > 1:
                # logging.shutdown was already called at this point.
                sys.stderr.write("Forced shutdown.\n")
                os._exit(0)
            # interrupt run() to start shutdown
            raise KeyboardInterrupt()
        elif sig == signal.SIGHUP:
            # restart on kill -HUP
            odoo.phoenix = True
            self.quit_signals_received += 1
            # interrupt run() to start shutdown
            raise KeyboardInterrupt()

可以看到,如果是SIGHUP信号,则标记phoenix为True,同时抛出 KeyboardInterrupt 中断服务器,此后,回到我们的odoo服务器启动过程源码的最后一段:

    # like the legend of the phoenix, all ends with beginnings
    if getattr(odoo, 'phoenix', False):
        if watcher:
            watcher.stop()
        _reexec()

由此可见,当odoo服务器进程收到SIGHUP信号时,将会重载odoo代码。

对此,我们可以这么操作发送SIGHUP信号:

$ kill -1 pid
# 或者
$ kill -s SIGHUP pid

注意

reload只是重载了python代码,像xml这些视图配置的,还是需要去升级模块才能够更新的,但是可以用deploy命令配合。