tech-angels-typhoeus 0.1.36

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 +312 -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.rb +55 -0
  18. data/lib/typhoeus/.gitignore +1 -0
  19. data/lib/typhoeus/easy.rb +329 -0
  20. data/lib/typhoeus/filter.rb +28 -0
  21. data/lib/typhoeus/hydra.rb +235 -0
  22. data/lib/typhoeus/multi.rb +35 -0
  23. data/lib/typhoeus/remote.rb +306 -0
  24. data/lib/typhoeus/remote_method.rb +108 -0
  25. data/lib/typhoeus/remote_proxy_object.rb +48 -0
  26. data/lib/typhoeus/request.rb +159 -0
  27. data/lib/typhoeus/response.rb +49 -0
  28. data/lib/typhoeus/service.rb +20 -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 +236 -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,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,235 @@
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
+ 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
+
154
+ easy.on_success do |easy|
155
+ queue_next
156
+ handle_request(request, response_from_easy(easy, request))
157
+ release_easy_object(easy)
158
+ end
159
+ easy.on_failure do |easy|
160
+ queue_next
161
+ handle_request(request, response_from_easy(easy, request))
162
+ release_easy_object(easy)
163
+ end
164
+ easy.set_headers
165
+ easy
166
+ end
167
+ private :get_easy_object
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
+
175
+ def release_easy_object(easy)
176
+ easy.reset
177
+ @easy_pool.push easy
178
+ end
179
+ private :release_easy_object
180
+
181
+ def handle_request(request, response, live_request = true)
182
+ request.response = response
183
+
184
+ if live_request && request.cache_timeout && @cache_setter
185
+ @cache_setter.call(request)
186
+ end
187
+ @on_complete.call(response) if @on_complete
188
+
189
+ request.call_handlers
190
+ if requests = @memoized_requests[request.url]
191
+ requests.each do |r|
192
+ r.response = response
193
+ r.call_handlers
194
+ end
195
+ end
196
+ end
197
+ private :handle_request
198
+
199
+ def response_from_easy(easy, request)
200
+ Response.new(:code => easy.response_code,
201
+ :headers => easy.response_header,
202
+ :body => easy.response_body,
203
+ :time => easy.total_time_taken,
204
+ :effective_url => easy.effective_url,
205
+ :request => request)
206
+ end
207
+ private :response_from_easy
208
+ end
209
+
210
+ class HydraMock
211
+ attr_reader :url, :method, :response, :requests
212
+
213
+ def initialize(url, method)
214
+ @url = url
215
+ @method = method
216
+ @requests = []
217
+ end
218
+
219
+ def add_request(request)
220
+ @requests << request
221
+ end
222
+
223
+ def and_return(val)
224
+ @response = val
225
+ end
226
+
227
+ def matches?(request)
228
+ if url.kind_of?(String)
229
+ request.method == method && request.url == url
230
+ else
231
+ request.method == method && url =~ request.url
232
+ end
233
+ end
234
+ end
235
+ 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() if easy.headers.empty?
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
@@ -0,0 +1,306 @@
1
+ module Typhoeus
2
+ USER_AGENT = "Typhoeus - http://github.com/pauldix/typhoeus/tree/master"
3
+
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ class MockExpectedError < StandardError; end
9
+
10
+ module ClassMethods
11
+ def allow_net_connect
12
+ @allow_net_connect = true if @allow_net_connect.nil?
13
+ @allow_net_connect
14
+ end
15
+
16
+ def allow_net_connect=(value)
17
+ @allow_net_connect = value
18
+ end
19
+
20
+ def mock(method, args = {})
21
+ @remote_mocks ||= {}
22
+ @remote_mocks[method] ||= {}
23
+ args[:code] ||= 200
24
+ args[:body] ||= ""
25
+ args[:headers] ||= ""
26
+ args[:time] ||= 0
27
+ url = args.delete(:url)
28
+ url ||= :catch_all
29
+ params = args.delete(:params)
30
+
31
+ key = mock_key_for(url, params)
32
+
33
+ @remote_mocks[method][key] = args
34
+ end
35
+
36
+ # Returns a key for a given URL and passed in
37
+ # set of Typhoeus options to be used to store/retrieve
38
+ # a corresponding mock.
39
+ def mock_key_for(url, params = nil)
40
+ if url == :catch_all
41
+ url
42
+ else
43
+ key = url
44
+ if params and !params.empty?
45
+ key += flatten_and_sort_hash(params).to_s
46
+ end
47
+ key
48
+ end
49
+ end
50
+
51
+ def flatten_and_sort_hash(params)
52
+ params = params.dup
53
+
54
+ # Flatten any sub-hashes to a single string.
55
+ params.keys.each do |key|
56
+ if params[key].is_a?(Hash)
57
+ params[key] = params[key].sort_by { |k, v| k.to_s.downcase }.to_s
58
+ end
59
+ end
60
+
61
+ params.sort_by { |k, v| k.to_s.downcase }
62
+ end
63
+
64
+ def get_mock(method, url, options)
65
+ return nil unless @remote_mocks
66
+ if @remote_mocks.has_key? method
67
+ extra_response_args = { :requested_http_method => method,
68
+ :requested_url => url,
69
+ :start_time => Time.now }
70
+ mock_key = mock_key_for(url, options[:params])
71
+ if @remote_mocks[method].has_key? mock_key
72
+ get_mock_and_run_handlers(method,
73
+ @remote_mocks[method][mock_key].merge(
74
+ extra_response_args),
75
+ options)
76
+ elsif @remote_mocks[method].has_key? :catch_all
77
+ get_mock_and_run_handlers(method,
78
+ @remote_mocks[method][:catch_all].merge(
79
+ extra_response_args),
80
+ options)
81
+ else
82
+ nil
83
+ end
84
+ else
85
+ nil
86
+ end
87
+ end
88
+
89
+ def enforce_allow_net_connect!(http_verb, url, params = nil)
90
+ if !allow_net_connect
91
+ message = "Real HTTP connections are disabled. Unregistered request: " <<
92
+ "#{http_verb.to_s.upcase} #{url}\n" <<
93
+ " Try: mock(:#{http_verb}, :url => \"#{url}\""
94
+ if params
95
+ message << ",\n :params => #{params.inspect}"
96
+ end
97
+
98
+ message << ")"
99
+
100
+ raise MockExpectedError, message
101
+ end
102
+ end
103
+
104
+ def check_expected_headers!(response_args, options)
105
+ missing_headers = {}
106
+
107
+ response_args[:expected_headers].each do |key, value|
108
+ if options[:headers].nil?
109
+ missing_headers[key] = [value, nil]
110
+ elsif ((options[:headers][key] && value != :anything) &&
111
+ options[:headers][key] != value)
112
+
113
+ missing_headers[key] = [value, options[:headers][key]]
114
+ end
115
+ end
116
+
117
+ unless missing_headers.empty?
118
+ raise headers_error_summary(response_args, options, missing_headers, 'expected')
119
+ end
120
+ end
121
+
122
+ def check_unexpected_headers!(response_args, options)
123
+ bad_headers = {}
124
+ response_args[:unexpected_headers].each do |key, value|
125
+ if (options[:headers][key] && value == :anything) ||
126
+ (options[:headers][key] == value)
127
+ bad_headers[key] = [value, options[:headers][key]]
128
+ end
129
+ end
130
+
131
+ unless bad_headers.empty?
132
+ raise headers_error_summary(response_args, options, bad_headers, 'did not expect')
133
+ end
134
+ end
135
+
136
+ def headers_error_summary(response_args, options, missing_headers, lead_in)
137
+ error = "#{lead_in} the following headers: #{response_args[:expected_headers].inspect}, but received: #{options[:headers].inspect}\n\n"
138
+ error << "Differences:\n"
139
+ error << "------------\n"
140
+ missing_headers.each do |key, values|
141
+ error << " - #{key}: #{lead_in} #{values[0].inspect}, got #{values[1].inspect}\n"
142
+ end
143
+
144
+ error
145
+ end
146
+ private :headers_error_summary
147
+
148
+ def get_mock_and_run_handlers(method, response_args, options)
149
+ response = Response.new(response_args)
150
+
151
+ if response_args.has_key? :expected_body
152
+ raise "#{method} expected body of \"#{response_args[:expected_body]}\" but received #{options[:body]}" if response_args[:expected_body] != options[:body]
153
+ end
154
+
155
+ if response_args.has_key? :expected_headers
156
+ check_expected_headers!(response_args, options)
157
+ end
158
+
159
+ if response_args.has_key? :unexpected_headers
160
+ check_unexpected_headers!(response_args, options)
161
+ end
162
+
163
+ if response.code >= 200 && response.code < 300 && options.has_key?(:on_success)
164
+ response = options[:on_success].call(response)
165
+ elsif options.has_key?(:on_failure)
166
+ response = options[:on_failure].call(response)
167
+ end
168
+
169
+ encode_nil_response(response)
170
+ end
171
+
172
+ [:get, :post, :put, :delete].each do |method|
173
+ line = __LINE__ + 2 # get any errors on the correct line num
174
+ code = <<-SRC
175
+ def #{method.to_s}(url, options = {})
176
+ mock_object = get_mock(:#{method.to_s}, url, options)
177
+ unless mock_object.nil?
178
+ decode_nil_response(mock_object)
179
+ else
180
+ enforce_allow_net_connect!(:#{method.to_s}, url, options[:params])
181
+ remote_proxy_object(url, :#{method.to_s}, options)
182
+ end
183
+ end
184
+ SRC
185
+ module_eval(code, "./lib/typhoeus/remote.rb", line)
186
+ end
187
+
188
+ def remote_proxy_object(url, method, options)
189
+ easy = Typhoeus.get_easy_object
190
+
191
+ easy.url = url
192
+ easy.method = method
193
+ easy.headers = options[:headers] if options.has_key?(:headers)
194
+ easy.headers["User-Agent"] = (options[:user_agent] || Typhoeus::USER_AGENT)
195
+ easy.params = options[:params] if options[:params]
196
+ easy.request_body = options[:body] if options[:body]
197
+ easy.timeout = options[:timeout] if options[:timeout]
198
+ easy.set_headers
199
+
200
+ proxy = Typhoeus::RemoteProxyObject.new(clear_memoized_proxy_objects, easy, options)
201
+ set_memoized_proxy_object(method, url, options, proxy)
202
+ end
203
+
204
+ def remote_defaults(options)
205
+ @remote_defaults ||= {}
206
+ @remote_defaults.merge!(options) if options
207
+ @remote_defaults
208
+ end
209
+
210
+ # If we get subclassed, make sure that child inherits the remote defaults
211
+ # of the parent class.
212
+ def inherited(child)
213
+ child.__send__(:remote_defaults, @remote_defaults)
214
+ end
215
+
216
+ def call_remote_method(method_name, args)
217
+ m = @remote_methods[method_name]
218
+
219
+ base_uri = args.delete(:base_uri) || m.base_uri || ""
220
+
221
+ if args.has_key? :path
222
+ path = args.delete(:path)
223
+ else
224
+ path = m.interpolate_path_with_arguments(args)
225
+ end
226
+ path ||= ""
227
+
228
+ http_method = m.http_method
229
+ url = base_uri + path
230
+ options = m.merge_options(args)
231
+
232
+ # proxy_object = memoized_proxy_object(http_method, url, options)
233
+ # return proxy_object unless proxy_object.nil?
234
+ #
235
+ # if m.cache_responses?
236
+ # object = @cache.get(get_memcache_response_key(method_name, args))
237
+ # if object
238
+ # set_memoized_proxy_object(http_method, url, options, object)
239
+ # return object
240
+ # end
241
+ # end
242
+
243
+ proxy = memoized_proxy_object(http_method, url, options)
244
+ unless proxy
245
+ if m.cache_responses?
246
+ options[:cache] = @cache
247
+ options[:cache_key] = get_memcache_response_key(method_name, args)
248
+ options[:cache_timeout] = m.cache_ttl
249
+ end
250
+ proxy = send(http_method, url, options)
251
+ end
252
+ proxy
253
+ end
254
+
255
+ def set_memoized_proxy_object(http_method, url, options, object)
256
+ @memoized_proxy_objects ||= {}
257
+ @memoized_proxy_objects["#{http_method}_#{url}_#{options.to_s}"] = object
258
+ end
259
+
260
+ def memoized_proxy_object(http_method, url, options)
261
+ @memoized_proxy_objects ||= {}
262
+ @memoized_proxy_objects["#{http_method}_#{url}_#{options.to_s}"]
263
+ end
264
+
265
+ def clear_memoized_proxy_objects
266
+ lambda { @memoized_proxy_objects = {} }
267
+ end
268
+
269
+ def get_memcache_response_key(remote_method_name, args)
270
+ result = "#{remote_method_name.to_s}-#{args.to_s}"
271
+ (Digest::SHA2.new << result).to_s
272
+ end
273
+
274
+ def cache=(cache)
275
+ @cache = cache
276
+ end
277
+
278
+ def define_remote_method(name, args = {})
279
+ @remote_defaults ||= {}
280
+ args[:method] ||= @remote_defaults[:method]
281
+ args[:on_success] ||= @remote_defaults[:on_success]
282
+ args[:on_failure] ||= @remote_defaults[:on_failure]
283
+ args[:base_uri] ||= @remote_defaults[:base_uri]
284
+ args[:path] ||= @remote_defaults[:path]
285
+ m = RemoteMethod.new(args)
286
+
287
+ @remote_methods ||= {}
288
+ @remote_methods[name] = m
289
+
290
+ class_eval <<-SRC
291
+ def self.#{name.to_s}(args = {})
292
+ call_remote_method(:#{name.to_s}, args)
293
+ end
294
+ SRC
295
+ end
296
+
297
+ private
298
+ def encode_nil_response(response)
299
+ response == nil ? :__nil__ : response
300
+ end
301
+
302
+ def decode_nil_response(response)
303
+ response == :__nil__ ? nil : response
304
+ end
305
+ end # ClassMethods
306
+ end