gravis-typhoeus 0.1.29

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.
Files changed (44) hide show
  1. data/.gitignore +2 -0
  2. data/README.textile +301 -0
  3. data/Rakefile +39 -0
  4. data/VERSION +1 -0
  5. data/benchmarks/profile.rb +25 -0
  6. data/benchmarks/vs_nethttp.rb +35 -0
  7. data/examples/twitter.rb +21 -0
  8. data/ext/typhoeus/.gitignore +5 -0
  9. data/ext/typhoeus/Makefile +157 -0
  10. data/ext/typhoeus/extconf.rb +65 -0
  11. data/ext/typhoeus/native.c +11 -0
  12. data/ext/typhoeus/native.h +21 -0
  13. data/ext/typhoeus/typhoeus_easy.c +207 -0
  14. data/ext/typhoeus/typhoeus_easy.h +19 -0
  15. data/ext/typhoeus/typhoeus_multi.c +225 -0
  16. data/ext/typhoeus/typhoeus_multi.h +16 -0
  17. data/lib/typhoeus/.gitignore +1 -0
  18. data/lib/typhoeus/easy.rb +322 -0
  19. data/lib/typhoeus/filter.rb +28 -0
  20. data/lib/typhoeus/hydra.rb +227 -0
  21. data/lib/typhoeus/multi.rb +35 -0
  22. data/lib/typhoeus/remote.rb +306 -0
  23. data/lib/typhoeus/remote_method.rb +108 -0
  24. data/lib/typhoeus/remote_proxy_object.rb +48 -0
  25. data/lib/typhoeus/request.rb +124 -0
  26. data/lib/typhoeus/response.rb +49 -0
  27. data/lib/typhoeus/service.rb +20 -0
  28. data/lib/typhoeus.rb +55 -0
  29. data/profilers/valgrind.rb +24 -0
  30. data/spec/fixtures/result_set.xml +60 -0
  31. data/spec/servers/app.rb +73 -0
  32. data/spec/spec.opts +2 -0
  33. data/spec/spec_helper.rb +11 -0
  34. data/spec/typhoeus/easy_spec.rb +228 -0
  35. data/spec/typhoeus/filter_spec.rb +35 -0
  36. data/spec/typhoeus/hydra_spec.rb +311 -0
  37. data/spec/typhoeus/multi_spec.rb +74 -0
  38. data/spec/typhoeus/remote_method_spec.rb +141 -0
  39. data/spec/typhoeus/remote_proxy_object_spec.rb +65 -0
  40. data/spec/typhoeus/remote_spec.rb +695 -0
  41. data/spec/typhoeus/request_spec.rb +169 -0
  42. data/spec/typhoeus/response_spec.rb +63 -0
  43. data/typhoeus.gemspec +112 -0
  44. metadata +203 -0
@@ -0,0 +1,322 @@
1
+ module Typhoeus
2
+ class Easy
3
+ attr_reader :response_body, :response_header, :method, :headers, :url
4
+ attr_accessor :start_time
5
+
6
+ # These integer codes are available in curl/curl.h
7
+ CURLINFO_STRING = 1048576
8
+ OPTION_VALUES = {
9
+ :CURLOPT_URL => 10002,
10
+ :CURLOPT_HTTPGET => 80,
11
+ :CURLOPT_HTTPPOST => 10024,
12
+ :CURLOPT_UPLOAD => 46,
13
+ :CURLOPT_CUSTOMREQUEST => 10036,
14
+ :CURLOPT_POSTFIELDS => 10015,
15
+ :CURLOPT_POSTFIELDSIZE => 60,
16
+ :CURLOPT_USERAGENT => 10018,
17
+ :CURLOPT_TIMEOUT_MS => 155,
18
+ :CURLOPT_NOSIGNAL => 99,
19
+ :CURLOPT_HTTPHEADER => 10023,
20
+ :CURLOPT_FOLLOWLOCATION => 52,
21
+ :CURLOPT_MAXREDIRS => 68,
22
+ :CURLOPT_HTTPAUTH => 107,
23
+ :CURLOPT_USERPWD => 10000 + 5,
24
+ :CURLOPT_VERBOSE => 41,
25
+ :CURLOPT_PROXY => 10004,
26
+ :CURLOPT_VERIFYPEER => 64,
27
+ :CURLOPT_NOBODY => 44,
28
+ :CURLOPT_ENCODING => 102,
29
+ :CURLOPT_SSLCERT => 10025,
30
+ :CURLOPT_SSLCERTTYPE => 10086,
31
+ :CURLOPT_SSLKEY => 10087,
32
+ :CURLOPT_SSLKEYTYPE => 10088,
33
+ :CURLOPT_KEYPASSWD => 10026,
34
+ :CURLOPT_CAINFO => 10065,
35
+ :CURLOPT_CAPATH => 10097
36
+ }
37
+ INFO_VALUES = {
38
+ :CURLINFO_RESPONSE_CODE => 2097154,
39
+ :CURLINFO_TOTAL_TIME => 3145731,
40
+ :CURLINFO_HTTPAUTH_AVAIL => 0x200000 + 23,
41
+ :CURLINFO_EFFECTIVE_URL => 0x100000 + 1
42
+ }
43
+ AUTH_TYPES = {
44
+ :CURLAUTH_BASIC => 1,
45
+ :CURLAUTH_DIGEST => 2,
46
+ :CURLAUTH_GSSNEGOTIATE => 4,
47
+ :CURLAUTH_NTLM => 8,
48
+ :CURLAUTH_DIGEST_IE => 16
49
+ }
50
+
51
+ def initialize
52
+ @method = :get
53
+ @post_dat_set = nil
54
+ @headers = {}
55
+
56
+ set_option(OPTION_VALUES[:CURLOPT_ENCODING], 'zlib') if supports_zlib?
57
+ end
58
+
59
+ def headers=(hash)
60
+ @headers = hash
61
+ end
62
+
63
+ def proxy=(proxy)
64
+ set_option(OPTION_VALUES[:CURLOPT_PROXY], proxy)
65
+ end
66
+
67
+ def auth=(authinfo)
68
+ set_option(OPTION_VALUES[:CURLOPT_USERPWD], "#{authinfo[:username]}:#{authinfo[:password]}")
69
+ set_option(OPTION_VALUES[:CURLOPT_HTTPAUTH], authinfo[:method]) if authinfo[:method]
70
+ end
71
+
72
+ def auth_methods
73
+ get_info_long(INFO_VALUES[:CURLINFO_HTTPAUTH_AVAIL])
74
+ end
75
+
76
+ def verbose=(boolean)
77
+ set_option(OPTION_VALUES[:CURLOPT_VERBOSE], !!boolean ? 1 : 0)
78
+ end
79
+
80
+ def total_time_taken
81
+ get_info_double(INFO_VALUES[:CURLINFO_TOTAL_TIME])
82
+ end
83
+
84
+ def effective_url
85
+ get_info_string(INFO_VALUES[:CURLINFO_EFFECTIVE_URL])
86
+ end
87
+
88
+ def response_code
89
+ get_info_long(INFO_VALUES[:CURLINFO_RESPONSE_CODE])
90
+ end
91
+
92
+ def follow_location=(boolean)
93
+ if boolean
94
+ set_option(OPTION_VALUES[:CURLOPT_FOLLOWLOCATION], 1)
95
+ else
96
+ set_option(OPTION_VALUES[:CURLOPT_FOLLOWLOCATION], 0)
97
+ end
98
+ end
99
+
100
+ def max_redirects=(redirects)
101
+ set_option(OPTION_VALUES[:CURLOPT_MAXREDIRS], redirects)
102
+ end
103
+
104
+ def timeout=(milliseconds)
105
+ @timeout = milliseconds
106
+ set_option(OPTION_VALUES[:CURLOPT_NOSIGNAL], 1)
107
+ set_option(OPTION_VALUES[:CURLOPT_TIMEOUT_MS], milliseconds)
108
+ end
109
+
110
+ def timed_out?
111
+ @timeout && total_time_taken > @timeout && response_code == 0
112
+ end
113
+
114
+ def supports_zlib?
115
+ !!(curl_version.match(/zlib/))
116
+ end
117
+
118
+ def request_body=(request_body)
119
+ @request_body = request_body
120
+ if @method == :put
121
+ easy_set_request_body(@request_body)
122
+ headers["Transfer-Encoding"] = ""
123
+ headers["Expect"] = ""
124
+ else
125
+ self.post_data = request_body
126
+ end
127
+ end
128
+
129
+ def user_agent=(user_agent)
130
+ set_option(OPTION_VALUES[:CURLOPT_USERAGENT], user_agent)
131
+ end
132
+
133
+ def url=(url)
134
+ @url = url
135
+ set_option(OPTION_VALUES[:CURLOPT_URL], url)
136
+ end
137
+
138
+ def disable_ssl_peer_verification
139
+ set_option(OPTION_VALUES[:CURLOPT_VERIFYPEER], 0)
140
+ end
141
+
142
+ def method=(method)
143
+ @method = method
144
+ if method == :get
145
+ set_option(OPTION_VALUES[:CURLOPT_HTTPGET], 1)
146
+ elsif method == :post
147
+ set_option(OPTION_VALUES[:CURLOPT_HTTPPOST], 1)
148
+ self.post_data = ""
149
+ elsif method == :put
150
+ set_option(OPTION_VALUES[:CURLOPT_UPLOAD], 1)
151
+ self.request_body = "" unless @request_body
152
+ elsif method == :head
153
+ set_option(OPTION_VALUES[:CURLOPT_NOBODY], 1)
154
+ else
155
+ set_option(OPTION_VALUES[:CURLOPT_CUSTOMREQUEST], "DELETE")
156
+ end
157
+ end
158
+
159
+ def post_data=(data)
160
+ @post_data_set = true
161
+ set_option(OPTION_VALUES[:CURLOPT_POSTFIELDS], data)
162
+ set_option(OPTION_VALUES[:CURLOPT_POSTFIELDSIZE], data.length)
163
+ end
164
+
165
+ def params=(params)
166
+ params_string = params.keys.collect do |k|
167
+ value = params[k]
168
+ if value.is_a? Hash
169
+ value.keys.collect {|sk| Rack::Utils.escape("#{k}[#{sk}]") + "=" + Rack::Utils.escape(value[sk].to_s)}
170
+ elsif value.is_a? Array
171
+ key = Rack::Utils.escape(k.to_s)
172
+ value.collect { |v| "#{key}=#{Rack::Utils.escape(v.to_s)}" }.join('&')
173
+ else
174
+ "#{Rack::Utils.escape(k.to_s)}=#{Rack::Utils.escape(params[k].to_s)}"
175
+ end
176
+ end.flatten.join("&")
177
+
178
+ if method == :post
179
+ self.post_data = params_string
180
+ else
181
+ self.url = "#{url}?#{params_string}"
182
+ end
183
+ end
184
+
185
+ # Set SSL certificate
186
+ # " The string should be the file name of your certificate. "
187
+ # The default format is "PEM" and can be changed with ssl_cert_type=
188
+ def ssl_cert=(cert, cert_type = "PEM")
189
+ set_option(OPTION_VALUES[:CURLOPT_SSLCERT], cert)
190
+ end
191
+
192
+ # Set SSL certificate type
193
+ # " The string should be the format of your certificate. Supported formats are "PEM" and "DER" "
194
+ def ssl_cert_type=(cert_type)
195
+ raise "Invalid ssl cert type : '#{cert_type}'..." unless %w(PEM DER).include?(cert_type)
196
+ set_option(OPTION_VALUES[:CURLOPT_SSLCERTTYPE], cert_type)
197
+ end
198
+
199
+ # Set SSL Key file
200
+ # " The string should be the file name of your private key. "
201
+ # The default format is "PEM" and can be changed with ssl_key_type=
202
+ #
203
+ def ssl_key=(key, options = {})
204
+ set_option(OPTION_VALUES[:CURLOPT_SSLKEY], key)
205
+ end
206
+
207
+ # Set SSL Key type
208
+ # " The string should be the format of your private key. Supported formats are "PEM", "DER" and "ENG". "
209
+ #
210
+ def ssk_key_type=(key_type)
211
+ raise "Invalid ssl key type : '#{key_type}'..." unless %w(PEM DER ENG).include?(cert_type)
212
+ set_option(OPTION_VALUES[:CURLOPT_SSLKEYTYPE], key_type)
213
+ end
214
+
215
+ def ssl_key_password=(key_password)
216
+ set_option(OPTION_VALUES[:CURLOPT_KEYPASSWD], key_password)
217
+ end
218
+
219
+ # Set SSL CACERT
220
+ # " File holding one or more certificates to verify the peer with. "
221
+ #
222
+ def ssl_cacert=(cacert)
223
+ set_option(OPTION_VALUES[:CURLOPT_CAINFO], cacert)
224
+ end
225
+
226
+ # Set CAPATH
227
+ # " directory holding multiple CA certificates to verify the peer with. The certificate directory must be prepared using the openssl c_rehash utility. "
228
+ #
229
+ def ssl_capath=(capath)
230
+ set_option(OPTION_VALUES[:CURLOPT_CAPATH], capath)
231
+ end
232
+
233
+ def set_option(option, value)
234
+ if value.class == String
235
+ easy_setopt_string(option, value)
236
+ else
237
+ easy_setopt_long(option, value)
238
+ end
239
+ end
240
+
241
+ def perform
242
+ set_headers()
243
+ easy_perform()
244
+ response_code()
245
+ end
246
+
247
+ def set_headers
248
+ headers.each_pair do |key, value|
249
+ easy_add_header("#{key}: #{value}")
250
+ end
251
+ easy_set_headers() unless headers.empty?
252
+ end
253
+
254
+ # gets called when finished and response code is 200-299
255
+ def success
256
+ @success.call(self) if @success
257
+ end
258
+
259
+ def on_success(&block)
260
+ @success = block
261
+ end
262
+
263
+ def on_success=(block)
264
+ @success = block
265
+ end
266
+
267
+ # gets called when finished and response code is 300-599
268
+ def failure
269
+ @failure.call(self) if @failure
270
+ end
271
+
272
+ def on_failure(&block)
273
+ @failure = block
274
+ end
275
+
276
+ def on_failure=(block)
277
+ @failure = block
278
+ end
279
+
280
+ def retries
281
+ @retries ||= 0
282
+ end
283
+
284
+ def increment_retries
285
+ @retries ||= 0
286
+ @retries += 1
287
+ end
288
+
289
+ def max_retries
290
+ @max_retries ||= 40
291
+ end
292
+
293
+ def max_retries?
294
+ retries >= max_retries
295
+ end
296
+
297
+ def reset
298
+ @retries = 0
299
+ @response_code = 0
300
+ @response_header = ""
301
+ @response_body = ""
302
+ easy_reset()
303
+ end
304
+
305
+ def get_info_string(option)
306
+ easy_getinfo_string(option)
307
+ end
308
+
309
+ def get_info_long(option)
310
+ easy_getinfo_long(option)
311
+ end
312
+
313
+ def get_info_double(option)
314
+ easy_getinfo_double(option)
315
+ end
316
+
317
+ def curl_version
318
+ version
319
+ end
320
+
321
+ end
322
+ end
@@ -0,0 +1,28 @@
1
+ module Typhoeus
2
+ class Filter
3
+ attr_reader :method_name
4
+
5
+ def initialize(method_name, options = {})
6
+ @method_name = method_name
7
+ @options = options
8
+ end
9
+
10
+ def apply_filter?(method_name)
11
+ if @options[:only]
12
+ if @options[:only].instance_of? Symbol
13
+ @options[:only] == method_name
14
+ else
15
+ @options[:only].include?(method_name)
16
+ end
17
+ elsif @options[:except]
18
+ if @options[:except].instance_of? Symbol
19
+ @options[:except] != method_name
20
+ else
21
+ !@options[:except].include?(method_name)
22
+ end
23
+ else
24
+ true
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,227 @@
1
+ module Typhoeus
2
+ class Hydra
3
+ def initialize(options = {})
4
+ @memoize_requests = true
5
+ @multi = Multi.new
6
+ @easy_pool = []
7
+ initial_pool_size = options[:initial_pool_size] || 10
8
+ @max_concurrency = options[:max_concurrency] || 200
9
+ initial_pool_size.times { @easy_pool << Easy.new }
10
+ @stubs = []
11
+ @memoized_requests = {}
12
+ @retrieved_from_cache = {}
13
+ @queued_requests = []
14
+ @running_requests = 0
15
+ @stubbed_request_count = 0
16
+ end
17
+
18
+ def self.hydra
19
+ @hydra ||= new
20
+ end
21
+
22
+ def self.hydra=(val)
23
+ @hydra = val
24
+ end
25
+
26
+ def clear_cache_callbacks
27
+ @cache_setter = nil
28
+ @cache_getter = nil
29
+ end
30
+
31
+ def clear_stubs
32
+ @stubs = []
33
+ end
34
+
35
+ def fire_and_forget
36
+ @queued_requests.each {|r| queue(r, false)}
37
+ @multi.fire_and_forget
38
+ end
39
+
40
+ def queue(request, obey_concurrency_limit = true)
41
+ return if assign_to_stub(request)
42
+
43
+ if @running_requests >= @max_concurrency && obey_concurrency_limit
44
+ @queued_requests << request
45
+ else
46
+ if request.method == :get
47
+ if @memoize_requests && @memoized_requests.has_key?(request.url)
48
+ if response = @retrieved_from_cache[request.url]
49
+ request.response = response
50
+ request.call_handlers
51
+ else
52
+ @memoized_requests[request.url] << request
53
+ end
54
+ else
55
+ @memoized_requests[request.url] = [] if @memoize_requests
56
+ get_from_cache_or_queue(request)
57
+ end
58
+ else
59
+ get_from_cache_or_queue(request)
60
+ end
61
+ end
62
+ end
63
+
64
+ def run
65
+ while @stubbed_request_count > 0
66
+ @stubs.each do |m|
67
+ while request = m.requests.shift
68
+ @stubbed_request_count -= 1
69
+ m.response.request = request
70
+ handle_request(request, m.response)
71
+ end
72
+ end
73
+ end
74
+
75
+ @multi.perform
76
+ @memoized_requests = {}
77
+ @retrieved_from_cache = {}
78
+ end
79
+
80
+ def disable_memoization
81
+ @memoize_requests = false
82
+ end
83
+
84
+ def cache_getter(&block)
85
+ @cache_getter = block
86
+ end
87
+
88
+ def cache_setter(&block)
89
+ @cache_setter = block
90
+ end
91
+
92
+ def on_complete(&block)
93
+ @on_complete = block
94
+ end
95
+
96
+ def on_complete=(proc)
97
+ @on_complete = proc
98
+ end
99
+
100
+ def stub(method, url)
101
+ @stubs << HydraMock.new(url, method)
102
+ @stubs.last
103
+ end
104
+
105
+ def assign_to_stub(request)
106
+ m = @stubs.detect {|stub| stub.matches? request}
107
+ if m
108
+ m.add_request(request)
109
+ @stubbed_request_count += 1
110
+ else
111
+ nil
112
+ end
113
+ end
114
+ private :assign_to_stub
115
+
116
+ def get_from_cache_or_queue(request)
117
+ if @cache_getter
118
+ val = @cache_getter.call(request)
119
+ if val
120
+ @retrieved_from_cache[request.url] = val
121
+ handle_request(request, val, false)
122
+ else
123
+ @multi.add(get_easy_object(request))
124
+ end
125
+ else
126
+ @multi.add(get_easy_object(request))
127
+ end
128
+ end
129
+ private :get_from_cache_or_queue
130
+
131
+ def get_easy_object(request)
132
+ @running_requests += 1
133
+
134
+ easy = @easy_pool.pop || Easy.new
135
+ easy.url = request.url
136
+ easy.method = request.method
137
+ easy.params = request.params if request.method == :post && !request.params.nil?
138
+ easy.headers = request.headers if request.headers
139
+ easy.request_body = request.body if request.body
140
+ easy.timeout = request.timeout if request.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
+
146
+ easy.on_success do |easy|
147
+ queue_next
148
+ handle_request(request, response_from_easy(easy, request))
149
+ release_easy_object(easy)
150
+ end
151
+ easy.on_failure do |easy|
152
+ queue_next
153
+ handle_request(request, response_from_easy(easy, request))
154
+ release_easy_object(easy)
155
+ end
156
+ easy.set_headers
157
+ easy
158
+ end
159
+ private :get_easy_object
160
+
161
+ def queue_next
162
+ @running_requests -= 1
163
+ queue(@queued_requests.pop) unless @queued_requests.empty?
164
+ end
165
+ private :queue_next
166
+
167
+ def release_easy_object(easy)
168
+ easy.reset
169
+ @easy_pool.push easy
170
+ end
171
+ private :release_easy_object
172
+
173
+ def handle_request(request, response, live_request = true)
174
+ request.response = response
175
+
176
+ if live_request && request.cache_timeout && @cache_setter
177
+ @cache_setter.call(request)
178
+ end
179
+ @on_complete.call(response) if @on_complete
180
+
181
+ request.call_handlers
182
+ if requests = @memoized_requests[request.url]
183
+ requests.each do |r|
184
+ r.response = response
185
+ r.call_handlers
186
+ end
187
+ end
188
+ end
189
+ private :handle_request
190
+
191
+ def response_from_easy(easy, request)
192
+ Response.new(:code => easy.response_code,
193
+ :headers => easy.response_header,
194
+ :body => easy.response_body,
195
+ :time => easy.total_time_taken,
196
+ :effective_url => easy.effective_url,
197
+ :request => request)
198
+ end
199
+ private :response_from_easy
200
+ end
201
+
202
+ class HydraMock
203
+ attr_reader :url, :method, :response, :requests
204
+
205
+ def initialize(url, method)
206
+ @url = url
207
+ @method = method
208
+ @requests = []
209
+ end
210
+
211
+ def add_request(request)
212
+ @requests << request
213
+ end
214
+
215
+ def and_return(val)
216
+ @response = val
217
+ end
218
+
219
+ def matches?(request)
220
+ if url.kind_of?(String)
221
+ request.method == method && request.url == url
222
+ else
223
+ request.method == method && url =~ request.url
224
+ end
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,35 @@
1
+ module Typhoeus
2
+ class Multi
3
+ attr_reader :easy_handles
4
+
5
+ def initialize
6
+ reset_easy_handles
7
+ end
8
+
9
+ def remove(easy)
10
+ multi_remove_handle(easy)
11
+ end
12
+
13
+ def add(easy)
14
+ easy.set_headers()
15
+ @easy_handles << easy
16
+ multi_add_handle(easy)
17
+ end
18
+
19
+ def perform()
20
+ while active_handle_count > 0 do
21
+ multi_perform
22
+ end
23
+ reset_easy_handles
24
+ end
25
+
26
+ def cleanup()
27
+ multi_cleanup
28
+ end
29
+
30
+ private
31
+ def reset_easy_handles
32
+ @easy_handles = []
33
+ end
34
+ end
35
+ end