読者です 読者をやめる 読者になる 読者になる

リクエスト単位で独立したコンテキストをtornadoで使用する

今の案件ではtornadoを使ってるですが、リクエスト単位でまとまったログを出したいなと思って、色々調べてみました。
(と言っても数時間かけてtornadoのソースとにらめっこして何となくできたコードを元に@さんに色々教えてもらった。python知らないのにtornado.stack_contextはむちゃだったw)


ポイントとしてはこんな感じ。

  • tornado.stack_context.StackContext()とwithを使ってtornadoにコンテキストの管理をやってもらう
  • StackContext()に渡すコンテキストファクトリはリクエスト単位で固定のコンテキストを返す関数を渡す
  • 自前のコンテキストの__enter__ではグローバル変数に自身を設定する
    • グローバル変数に設定することでリクエスト処理中のどこからでもコンテキストにアクセス可能
      • グローバル変数使えるのはユーザが実装したリクエスト処理部分はシングルスレッドで実行されるから
    • もしtornado自体をマルチスレッドで動かすなら、グローバル変数じゃなくてスレッドローカルな変数にすること
      • まああまり無いと思うけど・・・
# -*- coding: utf-8 -*-
import tornado.ioloop
import tornado.web
import tornado.httpclient
import tornado.stack_context

global context
context = None

class MainHandler(tornado.web.RequestHandler):
    def __init__(self, application, request, **kwargs):
        super(MainHandler, self).__init__(application, request, **kwargs)
        self._context = Context()

    def get(self):
        #非同期処理の場合、コールバックが呼び出されるときにtornadoの機能によってコンテキストが復元される。
        #それを利用して、リクエストを受けた時のコンテキストをコールバック実行時のコンテキストと設定できる。
        #tornadoにコンテキストの管理を行わせるにはwith文に指定するStackContextにコンテキストのファクトリ関数を渡す。
        #通常、ファクトリ関数は毎回コンテキストを生成するようなもの(例えばクラス自身)を渡すのだが、
        #リクエスト時のコンテキストをコールバック時にも使いたいため、常に固定のコンテキストを返す関数をファクトリ関数として渡す。
        #http://www.tornadoweb.org/documentation/stack_context.html#tornado.stack_context.StackContext
        with tornado.stack_context.StackContext(lambda: self._context):
            context.add_log(self.request.uri)
            if self.request.uri == "/async":
                self.async()
            else:
                self.sync()

    @tornado.web.asynchronous
    def async(self):
        http_client = tornado.httpclient.AsyncHTTPClient()
        http_client.fetch("http://example.com/sleep", self.callback) #sleepはレスポンスに数秒かかるダミーのAPI

    def callback(self, response):
        context.add_log("this is async process")
        print context.get_log() #/async, this is async process,
        self.write("async ok\n")
        self.finish()

    def sync(self):
        context.add_log("this is sync process")
        print context.get_log() #/sync, this is sync process,
        self.write("sync ok\n")

#リクエスト実行時のコンテキストとして使用するクラス。
#tornado.stack_context.StackContext()とwith文で使用されることを想定している。
#__enter__でグローバル変数のcontextに自身を代入しているのは
#各処理のどこからでもコンテキストにアクセスできるようにするため。
#グローバル変数を使用するのはtornadoのリクスと処理がシングルスレッドで実行されることに依存している。
#もしtornado自身を複数スレッドで動かす場合はグローバル変数の代わりにスレッドローカルな変数を使用する必要がある。
#このコンテキストには簡易なロギング機能を持たせてあるので、
#各リクエスト単位でログを集めることができる。(リクエスト単位でContextが独立しているため)
#つまり非同期処理をおこなってもログが混ざり合うことはない。
class Context:
    def __init__(self):
        self._log = ""
    def __enter__(self):
        global context
        self._old_context = context
        context = self
    def __exit__(self, type, value, traceback):
        global context
        context = self._old_context
    def add_log(self, log):
        self._log += log + ", "
    def get_log(self):
        return self._log

application = tornado.web.Application([
    (r"/async", MainHandler),
    (r"/sync", MainHandler),
])

if __name__ == "__main__":
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()


何か間違えてたら教えて下さい。
ではでは。