rackレベルで速度制限をかける方法


ダウンロードに時間がかかるときの挙動を確認したいことがあります。

一般的には、クライアントに速度制限アプリをいれたり、nginxのようなサーバで`rate_limit`のような速度制限を入れるのが一般的です。

本記事ではそういったアプローチではなく、rackレベルで速度制限をかける方法を紹介します。

当然、サーバ側のコードに手をいれるので、サーバアプリケーションの検証目的には使えませんが、クライアント側に速度制限アプリをいれる手間を省いたり、リクエストごとに通信速度を可変にするといったアプリ層ならではの柔軟性があったりします。

## rack のレスポンス

`rails`も`sinatra`も`padrino`すべては`rack`の土台の上にできています。また、`webrick`も`passenger`も`unicorn`も`puma`も`rack`に対応しています。`rack`はこのようにWebフレームワークやWebアプリケーションとアプリケーションサーバの間にたち、データのやりとりを標準化しています。

その[仕様](http://www.rubydoc.info/github/rack/rack/master/file/SPEC)は非常にシンプルで、ステータスコードと、レスポンスヘッダと、レスポンスボディの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アプリケーションのコードは以下のようになります。

```ruby:config.ru
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
```

```bash
$ 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`を利用をしたゆっくりとレスポンスを返すコードになります。

```ruby:config.ru
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]にするにはーいうことを計算してやるのでもう少し複雑なコードになるかとは思います。

それでは楽しいスローライフを