PerlerのRuby日記

Rubyとか

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

ググったら、raggi/async_sinatra ? GitHubという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つを渡せばよい。