Ruby
Rails
Typhoeus

Rubyでhttpリクエストを並列処理

モチベーション

api叩く時、いちいちループ処理のなかで叩くと時間がかかる。どうせならhttpリクエストを一気にやって時間を短縮したい

Typhoeusの紹介

rubyでhttpリクエストの並列化を調べて見た結果、typhoeusのgemが具合が良さそうである。Rubygem.orgでも1000万ダウンロードを超えてるし、まだメンテもされてそう。

Typhoeusとは

libcurlのラッパーgem。そもそもlibcurlとはクライアントサイドのurl転送ライブラリであり、よく使うcurlコマンドはこのlibcurlをベースにして動く。つまりruby標準のNet::httpとは別に、curlコマンド的な物を実現するものと認識しておけばOK?ちなみにtypoeusとはティポーエウス(ティポーン)とはギリシャ神話に出てくる神(怪物)らしい。

コードの例

Rubyの並列HttpリクエストGem Typhoeusを使ってみる

使い方の日本語訳

導入

Gemfile

gem 'typhoeus'

簡単な例

1つのリクエストのみ

Typhoeus.get("www.example.com", followlocation: true)

並列リクエスト

hydra = Typhoeus::Hydra.new
10.times.map{ hydra.queue(Typhoeus::Request.new("www.example.com", followlocation: true)) }
hydra.run

文法

typhoeusの基本的なインターフェースは3つのクラスで構成される。Request,Response,Hydraである。 RequestはHTTPリクエストのオブジェクトを、ResponseはHTTPレスポンスを扱うクラスである。HydraはHTTP接続を並列しておこなうクラスである。

request = Typhoeus::Request.new(
  "www.example.com",
  method: :post,
  body: "this is a request body",
  params: { field1: "a field" },
  headers: { Accept: "text/html" }
)

最初の引数はurlで、次の一連の引数はオプションである。オプションは必要なときに書くと良い。ちなみにデフォルトではmethodはgetである。

もしURLパラメーターを送りたい時は、:paramsハッシュが使える。しかしもしx-www-form-urlencodedパラメーター経由でリクエスを送りたいときは:bodyハッシュを代わりに使うこと。:paramsはURLパラメータのためにあり、:bodyはリクエストボディのためにある。

プロキシをとおしてリクエストを送る
オプションのリストにプロキシのurlを追加する。

options = {proxy: 'http://myproxy.org'}
req = Typhoeus::Request.new(url, options)

もしプロキシが認証を要求する場合、proxyuserpwdオプションキーを加える。

options = {proxy: 'http://proxyurl.com', proxyuserpwd: 'user:password'}
req = Typhoeus::Request.new(url, options)

このときusernameとpsswordはコロンで別れていることに注意。

クエリーをそれ自身かhydraを使って実装することもできる。

request.run
#=> <Typhoeus::Response ... >
hydra = Typhoeus::Hydra.hydra
hydra.queue(request)
hydra.run```

リクエストオブジェクトはリクエストがrunした後にセットされる。

```ruby
response = request.response
response.code
response.total_time
response.headers
response.body

単純なリクエストをおくる

typhoeusはシングルリクエストのためにいくつかの便利なメソッドがある。

Typhoeus.get("www.example.com")
Typhoeus.head("www.example.com")
Typhoeus.put("www.example.com/posts/1", body: "whoo, a body")
Typhoeus.patch("www.example.com/posts/1", body: "a new body")
Typhoeus.post("www.example.com/posts", body: { title: "test post", content: "this is my test"})
Typhoeus.delete("www.example.com/posts/1")
Typhoeus.options("www.example.com")

putでbodyにパラメタを書いて送る

postを使う時はcontent-typeなどは自動的に'application/x-www-form-urlencoded'にセットされる。これはbodyを使うか使わないかにかかわらず、put,patch,headなどの他のメソッドには当てはまらない。postのような結果を得るために、content-typeを以下のように示さなければならない。

Typhoeus.put("www.example.com/posts/1",
        headers: {'Content-Type'=> "application/x-www-form-urlencoded"},
        body: {title:"test post updated title", content: "this is my updated content"}
    )

エラーを扱う

リクエストが成功したかどうか確認するためにtyphoeusにはエラーを扱うメソッドがある。コールバックはリクエストの後に実行される。リクエストをrunする前にコールバックの内容を実装することを忘れないように。

request = Typhoeus::Request.new("www.example.com", followlocation: true)

request.on_complete do |response|
  if response.success?
    # hell yeah
  elsif response.timed_out?
    # aw hell no
    log("got a time out")
  elsif response.code == 0
    # Could not get an http response, something's wrong.
    log(response.return_message)
  else
    # Received a non-successful http response.
    log("HTTP request failed: " + response.code.to_s)
  end
end

request.run

これらは並列処理のブロックの中でも同様に動く。

アップロードを扱う

ファイルはpostリクエストでサーバーにアップロードできる。typhoeusはファイル名をそのままでアップロードし、content typeをセットするためにMime::Typesを使う。

Typhoeus.post(
  "http://localhost:3000/posts",
  body: {
    title: "test post",
    content: "this is my test",
    file: File.open("thesis.txt","r")
  }
)

レスポンスボディをストリーミングする

レスポンスをストリームすることも可能である。レスポンスが巨大であることが予想される場合、リクエストにon_bodyをセットすること。typhoeusはレスポンスの塊(チャンク)をコールバックに渡す。もしon_bodyをセットした時、typhoeusは完全なレスポンスを保持しない。

downloaded_file = File.open 'huge.iso', 'wb'
request = Typhoeus::Request.new("www.example.com/huge.iso")
request.on_headers do |response|
  if response.code != 200
    raise "Request failed"
  end
end
request.on_body do |chunk|
  downloaded_file.write(chunk)
end
request.on_complete do |response|
  downloaded_file.close
  # Note that response.body is ""
end
request.run

もしストリームを途中でやめたい時、:abortシンボルをon_blockから返すことができる。例えば

request.on_body do |chunk|
  buffer << chunk
  :abort if buffer.size > 1024 * 1024
end

これで適切にストリームは途中で止まり、もしreturnやthrowやraiseで邪魔されると起きるかもしれないメモリリークを防ぐことができる。

並列リクエストを送る

大抵はhydraを使ってリクエストをおくるべきである。

hydra = Typhoeus::Hydra.hydra

first_request = Typhoeus::Request.new("http://example.com/posts/1")
first_request.on_complete do |response|
  third_url = response.body
  third_request = Typhoeus::Request.new(third_url)
  hydra.queue third_request
end
second_request = Typhoeus::Request.new("http://example.com/posts/2")

hydra.queue first_request
hydra.queue second_request
hydra.run # this is a blocking call that returns once all requests are complete

第1と第2のリクエストは作成され、キューされる。もしhydraがrunした時、第1と第2のリクエストを並列処理される。第1のリクエストが終わったら、第3のリクエストが作成されキューされる。この例では第1のリクエストの結果を元にしている。第3のリクエストがキューされた瞬間、hydraはそれを実行し始める。しばらくして第2のリクエストがrunを続ける(第1のリクエストの前に終わる可能性もある)。第3のリクエストが完了すると、hydra.runからreturnされる。

図1.png

キューが実行された後に戻ってくるレスポンスの配列を取得する方法

hydra = Typhoeus::Hydra.new
requests = 10.times.map {
  request = Typhoeus::Request.new("www.example.com", followlocation: true)
  hydra.queue(request)
  request
}
hydra.run

responses = requests.map { |request|
  request.response.body
}

最大の並列処理数を設定する

Hydra will also handle how many requests you can make in parallel. Things will get flakey if you try to make too many requests at the same time. The built in limit is 200. When more requests than that are queued up, hydra will save them for later and start the requests as others are finished. You can raise or lower the concurrency limit through the Hydra constructor.

Typhoeus::Hydra.new(max_concurrency: 20)

メモ化する

Hydraは1回のrunコールの中でリクエストをメモ化する。メモ化はこの記事を参照。この時memoizationを有効化しなければならない。こうして1つのリクエストが発行される。しかし、両方のon_conpleteハンドラーが呼ばれる。

Typhoeus::Config.memoize = true

hydra = Typhoeus::Hydra.new(max_concurrency: 1)
2.times do
  hydra.queue Typhoeus::Request.new("www.example.com")
end
hydra.run

下では2つのリクエストとなる。

Typhoeus::Config.memoize = false

hydra = Typhoeus::Hydra.new(max_concurrency: 1)
2.times do
  hydra.queue Typhoeus::Request.new("www.example.com")
end
hydra.run

キャッシュ

Typhoeusはキャッシュをサポートするビルドを含む。例えば次の例では、キャッシュ化されたオブジェクトがリクエストオブジェクトのon_completeハンドラーに渡されている。

class Cache
  def initialize
    @memory = {}
  end

  def get(request)
    @memory[request]
  end

  def set(request, response)
    @memory[request] = response
  end
end

Typhoeus::Config.cache = Cache.new

Typhoeus.get("www.example.com").cached?
#=> false
Typhoeus.get("www.example.com").cached?
#=> true

Dalliを使う場合、

dalli = Dalli::Client.new(...)
Typhoeus::Config.cache = Typhoeus::Cache::Dalli.new(dalli)

Railsを使う場合,

Typhoeus::Config.cache = Typhoeus::Cache::Rails.new

Redisを使う場合

redis = Redis.new(...)
Typhoeus::Config.cache = Typhoeus::Cache::Redis.new(redis)

3つの全てのアダプターはdefault_ttlというオプションの引数を持つ。これはキャッシュのTTLセットを持たないリクエストのために、キャッシュ化されたレスポンスのデフォルトのTTLをセットする。

直接スタブする

hydraによって特定のurlにスタブアウトできたり、テスト中にリモートサーバーにヒットするのを回避できたりする。

response = Typhoeus::Response.new(code: 200, body: "{'name' : 'paul'}")
Typhoeus.stub('www.example.com').and_return(response)

Typhoeus.get("www.example.com") == response
#=> true

キューされたリクエストはスタブを当てることができる。正規表現でマッチしたurlも特定することもできる。

response = Typhoeus::Response.new(code: 200, body: "{'name' : 'paul'}")
Typhoeus.stub(/example/).and_return(response)

Typhoeus.get("www.example.com") == response
#=> true

順次結果を返すスタブの配列を到底することもできる。

Typhoeus.stub('www.example.com').and_return([response1, response2])

Typhoeus.get('www.example.com') == response1 #=> true
Typhoeus.get('www.example.com') == response2 #=> true

テストをする時、自身が期待するものをはっきりさせると、スタブがテストの間持続する。以下はspec_helper.rbファイルに自動的に行うために含めることができる。

RSpec.configure do |config|
  config.before :each do
    Typhoeus::Expectation.clear
  end
end

タイムアウトの設定

HTTPタイムアウトしてもexceptionは起きない。リクエストがタイムアウトしたかどうかは次のメソッドで確認できる。

Typhoeus.get("www.example.com", timeout: 1).timed_out?

タイムアウトレスポンスはsuccess?メソッドもfalseを返す。

タイムアウトには2種類ある。timeoutconnecttimeoutである。timeoutはリクエストが何秒でおわるのかの時間制限である。connecttimeoutは単に接続段階が何秒で終わるのかの時間制限である.

さらに2つの細かなオプションtiemout_msconnecttimeout_msがある。これらのオプションはミリ秒の精度を提供するが、いつも使えるとは限らない。(リナックス上でnosignalがtrueになっていない時のインスタンスなど)

TODO 後で訳す
When you pass a floating point timeout (or connecttimeout) Typhoeus will set timeout_ms for you if it has not been defined. The actual timeout values passed to curl will always be rounded up.

DNS timeouts of less than one second are not supported unless curl is compiled with an asynchronous resolver.

The default timeout is 0 (zero) which means curl never times out during transfer. The default connecttimeout is 300 seconds. A connecttimeout of 0 will also result in the default connecttimeout of 300 seconds.

リダイレクトを許可する

Typhoeus.get("www.example.com", followlocation: true)

Basic認証

Typhoeus::Request.get("www.example.com", userpwd: "user:password")

圧縮

Typhoeus.get("www.example.com", accept_encoding: "gzip")

上記はヘッダーに直接ハッシュを記述したときと異なる動作をする。

Typhoeus.get("www.example.com", headers: {"Accept-Encoding" => "gzip"})

ヘッダーに直接書くと--comressedフラグがlibcurlコマンドに含まれない。だから、libcurlはレスポンスを解凍しない。もし--compressedフラグを自動的に加えたいのなら、:accept-encodingをオプションとして加えること。

Cookies

Typhoeus::Request.get("www.example.com", cookiefile: "/path/to/file", cookiejar: "/path/to/file")

cookiefileはクッキーが読まれるファイルでcookiejarは受け取ったクッキーが書き込まれるファイル。もしクッキーを有効にしたいのなら、2つのファイルパスを同じにしなければならない。

他のcurlのオプション

ここを参照

SSL

デバッグのアウトプット

デフォルトのユーザーエージェント

specsをrunする

bundle install
bundle exec rake

Semantic Versioning

This project conforms to sermver.

LICENSE

(The MIT License)

Copyright © 2009-2010 Paul Dix

Copyright © 2011-2012 David Balatero

Copyright © 2012-2016 Hans Hasselberg

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.