thinでsleepして止まった話
先日、スマートフォンアプリの実装の開発を助けるために、以下のようなAPIをつくった。
# app.rb require "sinatra" # ロックファイルを作る get "/lock" do FileUtils.touch("/tmp/lock") "ok" end # ロックファイルが存在する限りループ(1秒間隔でチェック、最大30秒) get "/wait" do cnt = 0 while File.exist?("/tmp/lock") # 1秒待つ sleep 1 cnt += 1 # 30秒を超えたら if cnt >= 30 halt 400 end end "ok" end # ロックファイル削除 get "/unlock" do FileUtils.rm("/tmp/lock") "ok" end
# config.ru require "./app" run Sinatra::Application
で、これをthinで起動した。
$ bundle exec thin start
クライアントとしては、/lockにアクセスしたあと、/waitでアクセスを維持しつつ、/unlockでロックファイルを削除して、/waitの返却値を期待する、みたいな感じ。(あくまで実験的な実装)
で、実際にやってみたら、/lock→/waitで待っている間に/unlockにアクセスしてもなんの返答もない。
しばらく待って、/waitの30秒が経つのと同時に/unlockのレスポンスが返ってきた。
そりゃそうか。
プロセスは1つしかないので、出口が詰まるとみんな詰まる。
async_sinatra
ググったら、GitHub - raggi/async_sinatra: A plugin for Sinatra to provide a DSL extension for using Thin for asynchronous responsesというgemをみつけた。
Thin, Rainbows, Zbateryを使っている場合に有効らしい。
使い方はいつものgetやpostと書くルーティングにaをつけて、agetやapostにするだけである。
# app.rb require "sinatra" require "sinatra/async" register Sinatra::Async aget "/wait" do # 時間がかかる処理 end
一応内部の実装とthinを見てみて、最低限の処理を抽出すると以下のような感じだった。
# app.rb require "sinatra" get "/wait" do EM.next_tick do cnt = 0 # 1秒間隔でチェック EM.add_periodic_timer(1) do cnt += 1 if cnt >= 30 request.env['async.callback'].call([200, {'Content-Type' => 'text/plain'}, ['ok']]) end end end throw :async end
最後に:asyncをぶん投げるのが肝のようだ。
あとは環境変数env['async.callback']にいつもの3つを渡せばよい。