Tornadoのautoreloadの仕組みを調べた

ノンブロッキング Web サーバ Tornado を利用したサーバをデバッグモードで起動している場合、インポートしているファイルが変更されるとサーバは自動的に再起動する仕組みがある(autoreload)。

$ [I 120825 10:26:20 autoreload:175] /path/to/file.py modified; restarting server

Tornado ではどのようにして実現されているのか調べてみた。

処理の流れ

処理の流れは以下のようにシンプル。

  1. I/O イベントループインスタンスを作成
  2. ファイルの最終更新日をチェックするコールバック関数を定期実行スケジューラーに登録
  3. インポートしているファイル群をワッチリストに追加
  4. I/O イベントループを開始

インポートしているファイルの最終更新日が変更されると、スケジューラーの定期実行時に前回との更新日時の比較が行われ、ファイル変更イベントをキャッチしてサーバの再起動がかかる。

ミニマムな実装

tornado/autoreload.py で実装されている。autoreload.py からコア以外はバッサリと切り捨てて一部加工し、ミニマムな実装を目指す。(といっても autoreload.py 自体、コメント込みで300行程度しか無い)

Step 1: Build an I/O event loop

まずは、何はともあれ I/O イベントループ(tornado.ioloop)を用意

# vim: set fileencoding=utf8
from tornado import ioloop

import functools
import os
import sys

def wait():
    io_loop = ioloop.IOLoop()
    io_loop.start()

if __name__ == '__main__':
    wait()

Step 2: Register periodic callback

500ミリ秒ごとに定期実行されるスケジューラ(tornado.ioloop.PeriodicCallback)を用意し、コールバック関数(_reload_on_update)を登録。ioloop 起動後は check_time で指定したミリ秒単位でコールバック関数が呼ばれる。下の実装は500ミリ秒≒0.5秒。

# vim: set fileencoding=utf8
from tornado import ioloop

import functools
import os
import sys

def start(io_loop, check_time=500):
    modify_times = {} # watch list
    callback = functools.partial(_reload_on_update, modify_times)
    scheduler = ioloop.PeriodicCallback(callback, check_time, io_loop=io_loop)
    scheduler.start()

def wait():
    io_loop = ioloop.IOLoop()
    start(io_loop)
    io_loop.start()

def _reload_on_update(modify_times):
    pass

if __name__ == '__main__':
    wait()

Step 3: Implement callback function

コールバック関数 _reload_on_update では、インポートしているファイル一覧を取得し、ファイルが変更された場合は検出したい。
インポートしているファイル一覧は sys.modules を利用。

This is a dictionary that maps module names to modules which have already been loaded. This can be manipulated to force reloading of modules and other tricks. Note that removing a module from this dictionary is not the same as calling reload() on the corresponding module object.

http://docs.python.org/library/sys.html#sys.modules

モジュール名から Python ファイルの実際のパスを頑張って取得する。

# vim: set fileencoding=utf8
from tornado import ioloop

import functools
import os
import sys

def start(io_loop, check_time=500):
    modify_times = {} # watch list
    callback = functools.partial(_reload_on_update, modify_times)
    scheduler = ioloop.PeriodicCallback(callback, check_time, io_loop=io_loop)
    scheduler.start()

def wait():
    io_loop = ioloop.IOLoop()
    start(io_loop)
    io_loop.start()

def _reload_on_update(modify_times):
    for module in sys.modules.values():
        path = getattr(module, "__file__", None)
        if not path:
            continue
        if path.endswith(".pyc") or path.endswith(".pyo"):
            path = path[:-1]
        _check_file(modify_times, path)

def _check_file(modify_times, path):
    pass

if __name__ == '__main__':
    wait()

Step 4 : Get last modified time

os.stat でファイルの更新日時を取得。ファイルパスと更新日時の key/value を modify_times で管理。
過去に更新日時を取得している場合は、前回と異なっているかチェック。
異なっている場合は変更ありと判断し、on_modified イベント発動。

# vim: set fileencoding=utf8
from tornado import ioloop

import functools
import os
import sys

def start(io_loop, check_time=500):
    modify_times = {} # watch list
    callback = functools.partial(_reload_on_update, modify_times)
    scheduler = ioloop.PeriodicCallback(callback, check_time, io_loop=io_loop)
    scheduler.start()

def wait():
    io_loop = ioloop.IOLoop()
    start(io_loop)
    io_loop.start()

def _reload_on_update(modify_times):
    for module in sys.modules.values():
        path = getattr(module, "__file__", None)
        if not path:
            continue
        if path.endswith(".pyc") or path.endswith(".pyo"):
            path = path[:-1]
        _check_file(modify_times, path)

def _check_file(modify_times, path):
    try:
        modified = os.stat(path).st_mtime
    except Exception:
        return
    if path not in modify_times:
        # 起動後1回目の呼び出し
        modify_times[path] = modified
        return
    if modify_times[path] != modified:
        modify_times[path] = modified
        on_modified(path)

def on_modified(path):
    # ここにファイル変更時の処理を実装
    print "%s modified"%path

if __name__ == '__main__':
    wait()

自家製autoreloadを使う

以下のように foo.py で自家製 autoreload を呼び出すようにし、ついでに bar.py もインポートする。

#foo.py
import bar
import autoreload
autoreload.wait()

bar.py は空でOK($ touch bar.py)

foo.py を起動後、インポートしているファイルが変更された時に、on_modified 関数が呼びだされれば OK。
イベント発動にはファイルの最終更新日を更新すればよいので、touch コマンドを利用する。

$ python foo.py &
[1] 19420
$ touch bar.py
WARNING:root:/path/to/bar.py modified

$ touch bar.py
WARNING:root:/path/to/bar.py modified

ということで、期待通りに動いてくれた。

Python/Tornado ベッタリの実装だけど、なかなか良くできている。

“Introduction to Tornado – Modern Web Applications with Python” By Michael Dory, Adam Parrish, Brendan Berg

Tagged with: ,
Posted in middleware, python

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Archives
  • RT @__apf__: How to write a research paper: a guide for software engineers & practitioners. docs.google.com/presentation/d… /cc @inwyrd 1 week ago
  • RT @HayatoChiba: 昔、自然と対話しながら数学に打ち込んだら何かを悟れるのではと思いたち、専門書1つだけ持ってパワースポットで名高い奈良の山奥に1週間籠ったことがある。しかし泊まった民宿にドカベンが全巻揃っていたため、水島新司と対話しただけで1週間過ぎた。 それ… 3 weeks ago
  • RT @googlecloud: Ever wonder what underwater fiber optic internet cables look like? Look no further than this deep dive w/ @NatAndLo: https… 3 weeks ago
  • @ijin UTC+01:00 な時間帯で生活しています、、、 6 months ago
  • RT @mattcutts: Google's world-class Site Reliability Engineering team wrote a new book: amazon.com/Site-Reliabili… It's about managing produc… 9 months ago
%d bloggers like this: