typhoeus 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +3 -0
- data/CHANGELOG.markdown +31 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +30 -0
- data/README.textile +333 -0
- data/Rakefile +38 -0
- data/VERSION +1 -0
- data/benchmarks/profile.rb +25 -0
- data/benchmarks/vs_nethttp.rb +35 -0
- data/examples/twitter.rb +21 -0
- data/ext/typhoeus/.gitignore +6 -0
- data/ext/typhoeus/extconf.rb +3 -2
- data/ext/typhoeus/native.h +7 -0
- data/ext/typhoeus/typhoeus_multi.c +1 -2
- data/lib/typhoeus/.gitignore +1 -0
- data/lib/typhoeus/easy.rb +172 -42
- data/lib/typhoeus/hydra/callbacks.rb +24 -0
- data/lib/typhoeus/hydra/connect_options.rb +45 -0
- data/lib/typhoeus/hydra/stubbing.rb +52 -0
- data/lib/typhoeus/hydra.rb +85 -59
- data/lib/typhoeus/hydra_mock.rb +131 -0
- data/lib/typhoeus/multi.rb +4 -3
- data/lib/typhoeus/normalized_header_hash.rb +58 -0
- data/lib/typhoeus/remote_proxy_object.rb +6 -6
- data/lib/typhoeus/request.rb +118 -31
- data/lib/typhoeus/response.rb +61 -4
- data/lib/typhoeus/service.rb +20 -0
- data/lib/typhoeus/utils.rb +24 -0
- data/lib/typhoeus.rb +11 -8
- data/profilers/valgrind.rb +24 -0
- data/spec/fixtures/result_set.xml +60 -0
- data/spec/servers/app.rb +48 -1
- data/spec/spec_helper.rb +1 -1
- data/spec/typhoeus/easy_spec.rb +126 -4
- data/spec/typhoeus/hydra_mock_spec.rb +300 -0
- data/spec/typhoeus/hydra_spec.rb +456 -0
- data/spec/typhoeus/normalized_header_hash_spec.rb +41 -0
- data/spec/typhoeus/remote_spec.rb +2 -2
- data/spec/typhoeus/request_spec.rb +247 -0
- data/spec/typhoeus/response_spec.rb +104 -1
- data/spec/typhoeus/utils_spec.rb +22 -0
- data/typhoeus.gemspec +123 -0
- metadata +145 -34
- data/ext/typhoeus/Makefile +0 -157
data/lib/typhoeus/hydra.rb
CHANGED
|
@@ -1,63 +1,91 @@
|
|
|
1
|
+
require 'typhoeus/hydra/callbacks'
|
|
2
|
+
require 'typhoeus/hydra/connect_options'
|
|
3
|
+
require 'typhoeus/hydra/stubbing'
|
|
4
|
+
|
|
1
5
|
module Typhoeus
|
|
2
6
|
class Hydra
|
|
3
|
-
|
|
7
|
+
include ConnectOptions
|
|
8
|
+
include Stubbing
|
|
9
|
+
extend Callbacks
|
|
10
|
+
|
|
11
|
+
def initialize(options = {})
|
|
4
12
|
@memoize_requests = true
|
|
5
13
|
@multi = Multi.new
|
|
6
14
|
@easy_pool = []
|
|
15
|
+
initial_pool_size = options[:initial_pool_size] || 10
|
|
16
|
+
@max_concurrency = options[:max_concurrency] || 200
|
|
7
17
|
initial_pool_size.times { @easy_pool << Easy.new }
|
|
8
|
-
@stubs = []
|
|
9
18
|
@memoized_requests = {}
|
|
10
19
|
@retrieved_from_cache = {}
|
|
20
|
+
@queued_requests = []
|
|
21
|
+
@running_requests = 0
|
|
22
|
+
|
|
23
|
+
self.stubs = []
|
|
24
|
+
@active_stubs = []
|
|
11
25
|
end
|
|
12
26
|
|
|
13
27
|
def self.hydra
|
|
14
28
|
@hydra ||= new
|
|
15
29
|
end
|
|
16
|
-
|
|
30
|
+
|
|
17
31
|
def self.hydra=(val)
|
|
18
32
|
@hydra = val
|
|
19
33
|
end
|
|
20
|
-
|
|
21
|
-
def
|
|
22
|
-
@
|
|
34
|
+
|
|
35
|
+
def clear_cache_callbacks
|
|
36
|
+
@cache_setter = nil
|
|
37
|
+
@cache_getter = nil
|
|
23
38
|
end
|
|
24
|
-
|
|
39
|
+
|
|
25
40
|
def fire_and_forget
|
|
41
|
+
@queued_requests.each {|r| queue(r, false)}
|
|
26
42
|
@multi.fire_and_forget
|
|
27
43
|
end
|
|
28
44
|
|
|
29
|
-
def queue(request)
|
|
45
|
+
def queue(request, obey_concurrency_limit = true)
|
|
30
46
|
return if assign_to_stub(request)
|
|
31
47
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
48
|
+
# At this point, we are running over live HTTP. Make sure we haven't
|
|
49
|
+
# disabled live requests.
|
|
50
|
+
check_allow_net_connect!(request)
|
|
51
|
+
|
|
52
|
+
if @running_requests >= @max_concurrency && obey_concurrency_limit
|
|
53
|
+
@queued_requests << request
|
|
54
|
+
else
|
|
55
|
+
if request.method == :get
|
|
56
|
+
if @memoize_requests && @memoized_requests.has_key?(request.url)
|
|
57
|
+
if response = @retrieved_from_cache[request.url]
|
|
58
|
+
request.response = response
|
|
59
|
+
request.call_handlers
|
|
60
|
+
else
|
|
61
|
+
@memoized_requests[request.url] << request
|
|
62
|
+
end
|
|
37
63
|
else
|
|
38
|
-
@memoized_requests[request.url]
|
|
64
|
+
@memoized_requests[request.url] = [] if @memoize_requests
|
|
65
|
+
get_from_cache_or_queue(request)
|
|
39
66
|
end
|
|
40
67
|
else
|
|
41
|
-
@memoized_requests[request.url] = [] if @memoize_requests
|
|
42
68
|
get_from_cache_or_queue(request)
|
|
43
69
|
end
|
|
44
|
-
else
|
|
45
|
-
get_from_cache_or_queue(request)
|
|
46
70
|
end
|
|
47
71
|
end
|
|
48
72
|
|
|
49
73
|
def run
|
|
50
|
-
|
|
51
|
-
m
|
|
52
|
-
|
|
53
|
-
|
|
74
|
+
while !@active_stubs.empty?
|
|
75
|
+
m = @active_stubs.first
|
|
76
|
+
while request = m.requests.shift
|
|
77
|
+
response = m.response
|
|
78
|
+
response.request = request
|
|
79
|
+
handle_request(request, response)
|
|
54
80
|
end
|
|
81
|
+
@active_stubs.delete(m)
|
|
55
82
|
end
|
|
83
|
+
|
|
56
84
|
@multi.perform
|
|
57
85
|
@memoized_requests = {}
|
|
58
86
|
@retrieved_from_cache = {}
|
|
59
87
|
end
|
|
60
|
-
|
|
88
|
+
|
|
61
89
|
def disable_memoization
|
|
62
90
|
@memoize_requests = false
|
|
63
91
|
end
|
|
@@ -78,17 +106,6 @@ module Typhoeus
|
|
|
78
106
|
@on_complete = proc
|
|
79
107
|
end
|
|
80
108
|
|
|
81
|
-
def stub(method, url)
|
|
82
|
-
@stubs << HydraMock.new(url, method)
|
|
83
|
-
@stubs.last
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def assign_to_stub(request)
|
|
87
|
-
m = @stubs.detect {|stub| stub.matches? request}
|
|
88
|
-
m && m.add_request(request)
|
|
89
|
-
end
|
|
90
|
-
private :assign_to_stub
|
|
91
|
-
|
|
92
109
|
def get_from_cache_or_queue(request)
|
|
93
110
|
if @cache_getter
|
|
94
111
|
val = @cache_getter.call(request)
|
|
@@ -105,17 +122,42 @@ module Typhoeus
|
|
|
105
122
|
private :get_from_cache_or_queue
|
|
106
123
|
|
|
107
124
|
def get_easy_object(request)
|
|
125
|
+
@running_requests += 1
|
|
126
|
+
|
|
108
127
|
easy = @easy_pool.pop || Easy.new
|
|
128
|
+
easy.verbose = request.verbose
|
|
129
|
+
if request.username || request.password
|
|
130
|
+
auth = { :username => request.username, :password => request.password }
|
|
131
|
+
auth[:method] = Typhoeus::Easy::AUTH_TYPES["CURLAUTH_#{request.auth_method.to_s.upcase}".to_sym] if request.auth_method
|
|
132
|
+
easy.auth = auth
|
|
133
|
+
end
|
|
109
134
|
easy.url = request.url
|
|
110
135
|
easy.method = request.method
|
|
136
|
+
easy.params = request.params if request.method == :post && !request.params.nil?
|
|
111
137
|
easy.headers = request.headers if request.headers
|
|
112
138
|
easy.request_body = request.body if request.body
|
|
113
139
|
easy.timeout = request.timeout if request.timeout
|
|
140
|
+
easy.connect_timeout = request.connect_timeout if request.connect_timeout
|
|
141
|
+
easy.follow_location = request.follow_location if request.follow_location
|
|
142
|
+
easy.max_redirects = request.max_redirects if request.max_redirects
|
|
143
|
+
easy.proxy = request.proxy if request.proxy
|
|
144
|
+
easy.disable_ssl_peer_verification if request.disable_ssl_peer_verification
|
|
145
|
+
easy.ssl_cert = request.ssl_cert
|
|
146
|
+
easy.ssl_cert_type = request.ssl_cert_type
|
|
147
|
+
easy.ssl_key = request.ssl_key
|
|
148
|
+
easy.ssl_key_type = request.ssl_key_type
|
|
149
|
+
easy.ssl_key_password = request.ssl_key_password
|
|
150
|
+
easy.ssl_cacert = request.ssl_cacert
|
|
151
|
+
easy.ssl_capath = request.ssl_capath
|
|
152
|
+
easy.verbose = request.verbose
|
|
153
|
+
|
|
114
154
|
easy.on_success do |easy|
|
|
155
|
+
queue_next
|
|
115
156
|
handle_request(request, response_from_easy(easy, request))
|
|
116
157
|
release_easy_object(easy)
|
|
117
158
|
end
|
|
118
159
|
easy.on_failure do |easy|
|
|
160
|
+
queue_next
|
|
119
161
|
handle_request(request, response_from_easy(easy, request))
|
|
120
162
|
release_easy_object(easy)
|
|
121
163
|
end
|
|
@@ -124,6 +166,12 @@ module Typhoeus
|
|
|
124
166
|
end
|
|
125
167
|
private :get_easy_object
|
|
126
168
|
|
|
169
|
+
def queue_next
|
|
170
|
+
@running_requests -= 1
|
|
171
|
+
queue(@queued_requests.pop) unless @queued_requests.empty?
|
|
172
|
+
end
|
|
173
|
+
private :queue_next
|
|
174
|
+
|
|
127
175
|
def release_easy_object(easy)
|
|
128
176
|
easy.reset
|
|
129
177
|
@easy_pool.push easy
|
|
@@ -133,6 +181,9 @@ module Typhoeus
|
|
|
133
181
|
def handle_request(request, response, live_request = true)
|
|
134
182
|
request.response = response
|
|
135
183
|
|
|
184
|
+
self.class.run_global_hooks_for(:after_request_before_on_complete,
|
|
185
|
+
request)
|
|
186
|
+
|
|
136
187
|
if live_request && request.cache_timeout && @cache_setter
|
|
137
188
|
@cache_setter.call(request)
|
|
138
189
|
end
|
|
@@ -153,34 +204,9 @@ module Typhoeus
|
|
|
153
204
|
:headers => easy.response_header,
|
|
154
205
|
:body => easy.response_body,
|
|
155
206
|
:time => easy.total_time_taken,
|
|
207
|
+
:effective_url => easy.effective_url,
|
|
156
208
|
:request => request)
|
|
157
209
|
end
|
|
158
210
|
private :response_from_easy
|
|
159
211
|
end
|
|
160
|
-
|
|
161
|
-
class HydraMock
|
|
162
|
-
attr_reader :url, :method, :response, :requests
|
|
163
|
-
|
|
164
|
-
def initialize(url, method)
|
|
165
|
-
@url = url
|
|
166
|
-
@method = method
|
|
167
|
-
@requests = []
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def add_request(request)
|
|
171
|
-
@requests << request
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def and_return(val)
|
|
175
|
-
@response = val
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def matches?(request)
|
|
179
|
-
if url.kind_of?(String)
|
|
180
|
-
request.method == method && request.url == url
|
|
181
|
-
else
|
|
182
|
-
request.method == method && url =~ request.url
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
end
|
|
186
212
|
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
module Typhoeus
|
|
2
|
+
class HydraMock
|
|
3
|
+
attr_reader :url, :method, :requests, :uri
|
|
4
|
+
|
|
5
|
+
def initialize(url, method, options = {})
|
|
6
|
+
@url = url
|
|
7
|
+
@uri = URI.parse(url) if url.kind_of?(String)
|
|
8
|
+
@method = method
|
|
9
|
+
@requests = []
|
|
10
|
+
@options = options
|
|
11
|
+
if @options[:headers]
|
|
12
|
+
@options[:headers] = Typhoeus::NormalizedHeaderHash.new(@options[:headers])
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
@current_response_index = 0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def body
|
|
19
|
+
@options[:body]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def body?
|
|
23
|
+
@options.has_key?(:body)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def headers
|
|
27
|
+
@options[:headers]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def headers?
|
|
31
|
+
@options.has_key?(:headers)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def add_request(request)
|
|
35
|
+
@requests << request
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def and_return(val)
|
|
39
|
+
if val.respond_to?(:each)
|
|
40
|
+
@responses = val
|
|
41
|
+
else
|
|
42
|
+
@responses = [val]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# make sure to mark them as a mock.
|
|
46
|
+
@responses.each { |r| r.mock = true }
|
|
47
|
+
|
|
48
|
+
val
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def response
|
|
52
|
+
if @current_response_index == (@responses.length - 1)
|
|
53
|
+
@responses.last
|
|
54
|
+
else
|
|
55
|
+
value = @responses[@current_response_index]
|
|
56
|
+
@current_response_index += 1
|
|
57
|
+
value
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def matches?(request)
|
|
62
|
+
if !method_matches?(request) or !url_matches?(request)
|
|
63
|
+
return false
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
if body?
|
|
67
|
+
return false unless body_matches?(request)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if headers?
|
|
71
|
+
return false unless headers_match?(request)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
true
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
def method_matches?(request)
|
|
79
|
+
self.method == :any or self.method == request.method
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def url_matches?(request)
|
|
83
|
+
if url.kind_of?(String)
|
|
84
|
+
request_uri = URI.parse(request.url)
|
|
85
|
+
request_uri == self.uri
|
|
86
|
+
else
|
|
87
|
+
self.url =~ request.url
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def body_matches?(request)
|
|
92
|
+
!request.body.nil? && !request.body.empty? && request.body == self.body
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def headers_match?(request)
|
|
96
|
+
request_headers = NormalizedHeaderHash.new(request.headers)
|
|
97
|
+
|
|
98
|
+
if empty_headers?(self.headers)
|
|
99
|
+
empty_headers?(request_headers)
|
|
100
|
+
else
|
|
101
|
+
return false if empty_headers?(request_headers)
|
|
102
|
+
|
|
103
|
+
headers.each do |key, value|
|
|
104
|
+
return false unless header_value_matches?(value, request_headers[key])
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
true
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def header_value_matches?(mock_value, request_value)
|
|
112
|
+
mock_arr = mock_value.is_a?(Array) ? mock_value : [mock_value]
|
|
113
|
+
request_arr = request_value.is_a?(Array) ? request_value : [request_value]
|
|
114
|
+
|
|
115
|
+
return false unless mock_arr.size == request_arr.size
|
|
116
|
+
mock_arr.all? do |value|
|
|
117
|
+
request_arr.any? { |a| value === a }
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def empty_headers?(headers)
|
|
122
|
+
# We consider the default User-Agent header to be empty since
|
|
123
|
+
# Typhoeus always adds that.
|
|
124
|
+
headers.nil? || headers.empty? || default_typhoeus_headers?(headers)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def default_typhoeus_headers?(headers)
|
|
128
|
+
headers.size == 1 && headers['User-Agent'] == Typhoeus::USER_AGENT
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
data/lib/typhoeus/multi.rb
CHANGED
|
@@ -9,19 +9,20 @@ module Typhoeus
|
|
|
9
9
|
def remove(easy)
|
|
10
10
|
multi_remove_handle(easy)
|
|
11
11
|
end
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
def add(easy)
|
|
14
|
+
easy.set_headers() if easy.headers.empty?
|
|
14
15
|
@easy_handles << easy
|
|
15
16
|
multi_add_handle(easy)
|
|
16
17
|
end
|
|
17
|
-
|
|
18
|
+
|
|
18
19
|
def perform()
|
|
19
20
|
while active_handle_count > 0 do
|
|
20
21
|
multi_perform
|
|
21
22
|
end
|
|
22
23
|
reset_easy_handles
|
|
23
24
|
end
|
|
24
|
-
|
|
25
|
+
|
|
25
26
|
def cleanup()
|
|
26
27
|
multi_cleanup
|
|
27
28
|
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module Typhoeus
|
|
2
|
+
class NormalizedHeaderHash < ::Hash
|
|
3
|
+
def initialize(constructor = {})
|
|
4
|
+
if constructor.is_a?(Hash)
|
|
5
|
+
super
|
|
6
|
+
update(constructor)
|
|
7
|
+
else
|
|
8
|
+
super(constructor)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def fetch(key, *extras)
|
|
13
|
+
super(convert_key(key), *extras)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def key?(key)
|
|
17
|
+
super(convert_key(key))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
[:include?, :has_key?, :member?].each do |method|
|
|
21
|
+
alias_method method, :key?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def [](key)
|
|
25
|
+
super(convert_key(key))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def []=(key, value)
|
|
29
|
+
super(convert_key(key), value)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def update(other_hash)
|
|
33
|
+
other_hash.each_pair do |key, value|
|
|
34
|
+
self[convert_key(key)] = value
|
|
35
|
+
end
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
alias_method :merge!, :update
|
|
40
|
+
|
|
41
|
+
def dup
|
|
42
|
+
self.class.new(self)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def merge(hash)
|
|
46
|
+
self.dup.update(hash)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def delete(key)
|
|
50
|
+
super(convert_key(key))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
def convert_key(key)
|
|
55
|
+
key.to_s.split(/_|-/).map { |segment| segment.capitalize }.join("-")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module Typhoeus
|
|
2
2
|
class RemoteProxyObject
|
|
3
|
-
instance_methods.each { |m| undef_method m unless m =~ /^__/ }
|
|
4
|
-
|
|
3
|
+
instance_methods.each { |m| undef_method m unless m =~ /^__|object_id/ }
|
|
4
|
+
|
|
5
5
|
def initialize(clear_memoized_store_proc, easy, options = {})
|
|
6
6
|
@clear_memoized_store_proc = clear_memoized_store_proc
|
|
7
7
|
@easy = easy
|
|
@@ -12,13 +12,13 @@ module Typhoeus
|
|
|
12
12
|
@timeout = options.delete(:cache_timeout)
|
|
13
13
|
Typhoeus.add_easy_request(@easy)
|
|
14
14
|
end
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
def method_missing(sym, *args, &block)
|
|
17
17
|
unless @proxied_object
|
|
18
18
|
if @cache && @cache_key
|
|
19
19
|
@proxied_object = @cache.get(@cache_key) rescue nil
|
|
20
20
|
end
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
unless @proxied_object
|
|
23
23
|
Typhoeus.perform_easy_requests
|
|
24
24
|
response = Response.new(:code => @easy.response_code,
|
|
@@ -31,7 +31,7 @@ module Typhoeus
|
|
|
31
31
|
if @easy.response_code >= 200 && @easy.response_code < 300
|
|
32
32
|
Typhoeus.release_easy_object(@easy)
|
|
33
33
|
@proxied_object = @success.nil? ? response : @success.call(response)
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
if @cache && @cache_key
|
|
36
36
|
@cache.set(@cache_key, @proxied_object, @timeout)
|
|
37
37
|
end
|
|
@@ -41,7 +41,7 @@ module Typhoeus
|
|
|
41
41
|
@clear_memoized_store_proc.call
|
|
42
42
|
end
|
|
43
43
|
end
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
@proxied_object.__send__(sym, *args, &block)
|
|
46
46
|
end
|
|
47
47
|
end
|