非同期サービスフレームワーク ginkgo (docs) というものが今年の PyCon US で発表された。サービスを作るには、デーモン化なり、PIDファイルの作成なり、シグナルの処理なり、プログラムでやらないといけないことが多い。 Twisted フレームワークだと、サービスまわりの機能がありますが、 gevent ベースのサービスを作るには結構大変なので、 ginkgo というものが作られた。

ginkgo は Service クラスを提供して、そのクラスを継承して、サービスのことを実装するだけ。

簡単なウェブサービス

まずは、WSGIベースの Hello World

from ginkgo import Service
from ginkgo.async.gevent import WSGIServer

class HelloWorldWebServer(Service):
    def __init__(self):
        self.add_service(WSGIServer(('0.0.0.0', 8000), self.handle))

    def handle(self, environ, start_response):
        start_response('200 OK', [('Content-Type', 'text/html')])
        return ["<strong>Hello World</strong>"]

これを server.py に保存することで、ginkgo コマンドで起動する

$ ginkgo server.HelloWorldWebServer
Starting process with server.HelloWorldWebServer...

そして、 curl でリクエストを投げると、Hello World が返ってくる

$ curl localhost:8000
<strong>Hello World</strong>

簡単な cron サービス

次は何秒ごとにハンドラーを実行する cron サービスを作ります。

class CronService(Service):
    def __init__(self, seconds=1, handle=None, start=None):
        self.seconds = seconds
        self.start_time = start
        if handle:
            self.handle = handle

    def handle(self, dt):
        print("Cron: %s" % dt)

    def do_start(self):
        self.spawn(self.serve)

    def serve(self):
        """
        Serve forever calling the self.handle function
        with the run time every self.seconds.

        next_time - The actual time the handler should be run.
        now - the current time

        """
        now = time.time()
        next_time = now
        if self.start_time and now < self.start_time:
            # If we aren't yet at the start time sleep until then.
            self.async.sleep(self.start_time - now)
            next_time = self.start_time

        while True:
            # self.handle could take some time to run so we calculate
            # the next time to run from the previous run time rather
            # than just sleeping for self.seconds.
            self.spawn(self.handle, next_time) # Could take some seconds?
            now = time.time()
            next_time = next_time + self.seconds

            self.async.sleep(next_time - now)

デフォルトで 1秒毎に実行時間を出力することになる。

$ ginkgo server.CronService
Starting process with server.CronService...
Cron: 1342061996.95
Cron: 1342061997.95
Cron: 1342061998.95
Cron: 1342061999.95
Cron: 1342062000.95

全部合流するサービスを作る

ginkgo は gevent を使っているので、非同期で処理できる。add_service()メソッドでサブサービスが設定できる。上の、CronServer の do_start()メソッドで、サービスを起動するときに、cron の greenlet を別に起動しますので、他の CronServer インスタンスと同時に使えます。さらに、ウェブサーバー加えると、hello world が返す cron サーバーが作れる。

ちょっとやってみよう。

class CronMonitorService(Service):
    def __init__(self):
        self.add_service(CronService(5, self.ping))
        self.add_service(CronService(7, self.pong))
        self.add_service(HelloWorldWebServer())

    def ping(self, *args, **kwargs):
        print("Ping")

    def pong(self, *args, **kwargs):
        print("Pong")

これで起動すると、

$ ginkgo server.CronMonitorService
Starting process with server.CronMonitorService...
Ping
Pong
127.0.0.1 - - [2012-07-12 12:05:46] "GET / HTTP/1.1" 200 129 0.000110
Ping
Pong
Ping

複数の cron が動いているウェブリクエストも処理するサーバーが立ち上げる。これは結構面白いことが簡単にできそう。