ダウンロードに時間がかかるときの挙動を確認したいことがあります。
一般的には、クライアントに速度制限アプリをいれたり、nginxのようなサーバでrate_limit
のような速度制限を入れるのが一般的です。
本記事ではそういったアプローチではなく、rackレベルで速度制限をかける方法を紹介します。
当然、サーバ側のコードに手をいれるので、サーバアプリケーションの検証目的には使えませんが、クライアント側に速度制限アプリをいれる手間を省いたり、リクエストごとに通信速度を可変にするといったアプリ層ならではの柔軟性があったりします。
rack のレスポンス
rails
もsinatra
もpadrino
すべてはrack
の土台の上にできています。また、webrick
もpassenger
もunicorn
もpuma
もrack
に対応しています。rack
はこのようにWebフレームワークやWebアプリケーションとアプリケーションサーバの間にたち、データのやりとりを標準化しています。
その仕様は非常にシンプルで、ステータスコードと、レスポンスヘッダと、レスポンスボディの3つを返すことが定められています。
ステータスコードはto_i
された場合に100以上の数字になること、レスポンスヘッダはeach
できて、keyとvalueをyield
すること、レスポンスボディはeach
できて、String
をyield
することと(一部はしょってますが)決められています。
Hijacking API
ヘッダでrack.
で始まるヘッダは特別なヘッダとして定義されています。そのうちの一つにrack.hijack?
というヘッダがあります。
これは、Rack 1.5 から導入された Hijacking APIで、このrack.hijack?
がtrueなアプリケーションサーバは、Hijacking APIに対応しているということになります。
Hijacking APIでできることは、リクエストをハイジャックすることと、レスポンスをハイジャックする2通りがありますが、今回はダウンロードに時間をかけたいので、レスポンスをハイジャックする方法を説明します。
レスポンスをハイジャックするためには、rack.hijack
というレスポンスヘッダを利用します。これは、call
に反応できればいいので、通常はProcを使います。ちなみにこのやりかたは partial hijacking というレスポンスボディの返し方だけをハイジャックするやりかたです。
Hijacking API を利用してゆっくりデータを返すrackアプリケーションのコードは以下のようになります。
require "rack"
require "thread"
class App
def call(env)
headers, body = { "Content-Type" => "text/plain", "Connection" => "close" }, nil
if env["rack.hijack?"]
headers["rack.hijack"] = lambda do |io|
begin
Thread.new do
10.times do
io.write("0123456789\n")
io.flush
sleep 1
end
end.join
ensure
io.close
end
end
else
body = 10.times.map { "0123456789\n" }
end
[ 200, headers, body ]
end
end
use Rack::ContentLength
run App.new
$ rackup -s thin -p 3000
$ passenger start
thin と passenger でそれぞれサーバを起動し、http://localhost:3000
にアクセスすると、thinは、hijackに対応していないため、すぐに結果が返り、passengerは10秒かけて結果がゆっくりかえってきていることがわかります。
Chromeのdev toolsでみると、Content Downloadが10秒かかっていますし、tcpdump とかで、port 3000をみてみると、(localhostをみているときは -i lo0 とか気をつけてくださいね)1秒ごとにchunckっぽいのが返っていることがわかります。
なお、これを最初はpassengerでなくて、puma
で試していたのですが、Broken Pipeと怒られてうまくうごきませんでした。
Rack::BodyProxy を利用する
先ほどのやり方は、puma
で動かなかったり、thin
のようにhijackに対応していないアプリケーションサーバがあったりとなかなか未だこなれていない感があります。
そこで、レスポンスボディをラップするやりかたがあります。これがRack::BodyProxy
です。なんとなく名前で想像付きそうな感じがしますね。
以下がRack::BodyProxy
を利用をしたゆっくりとレスポンスを返すコードになります。
require "rack"
require "thread"
class SlowResponse < Rack::BodyProxy
def each(&block)
Thread.new do
@body.each {|body|
sleep 1
yield body
}
end.join
end
end
class App
def call(env)
headers = { "Content-Type" => "text/plain" }
body = SlowResponse.new(10.times.map { "0123456789\n" }) {}
[ 200, headers, body ]
end
end
use Rack::ContentLength
run App.new
Rack::Response
を継承したSlowResponse
というクラスのeach
を上書きして、10秒ごとにbodyをyieldするようにしています。これだと、puma
やunicorn
でも動作しました。
ちなみに、new
するときに、渡しているブロックは、クローズの時に呼ばれるので、指定しないとエラーになります。
たぶんこの仕組でthin
とかwebrick
でも頑張ればできそうな気がするけど、ちょっと時間切れになってしまいました。
まとめ
- Rack Hijacking API でやるやり方
- Rack::BodyProxy を使うやり方
今回紹介したやり方はものすごいシンプルなコードなので、実際には何かしらのパラメータを渡して、例えばn[KB/s]の速度にするにはーとか、レスポンス時間がn[sec]にするにはーいうことを計算してやるのでもう少し複雑なコードになるかとは思います。
それでは楽しいスローライフを