モチベーション
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される。
キューが実行された後に戻ってくるレスポンスの配列を取得する方法
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種類ある。timeout
とconnecttimeout
である。timeout
はリクエストが何秒でおわるのかの時間制限である。connecttimeout
は単に接続段階が何秒で終わるのかの時間制限である.
さらに2つの細かなオプションtiemout_ms
とconnecttimeout_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.