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.
Files changed (44) hide show
  1. data/.gitignore +3 -0
  2. data/CHANGELOG.markdown +31 -0
  3. data/Gemfile +9 -0
  4. data/Gemfile.lock +30 -0
  5. data/README.textile +333 -0
  6. data/Rakefile +38 -0
  7. data/VERSION +1 -0
  8. data/benchmarks/profile.rb +25 -0
  9. data/benchmarks/vs_nethttp.rb +35 -0
  10. data/examples/twitter.rb +21 -0
  11. data/ext/typhoeus/.gitignore +6 -0
  12. data/ext/typhoeus/extconf.rb +3 -2
  13. data/ext/typhoeus/native.h +7 -0
  14. data/ext/typhoeus/typhoeus_multi.c +1 -2
  15. data/lib/typhoeus/.gitignore +1 -0
  16. data/lib/typhoeus/easy.rb +172 -42
  17. data/lib/typhoeus/hydra/callbacks.rb +24 -0
  18. data/lib/typhoeus/hydra/connect_options.rb +45 -0
  19. data/lib/typhoeus/hydra/stubbing.rb +52 -0
  20. data/lib/typhoeus/hydra.rb +85 -59
  21. data/lib/typhoeus/hydra_mock.rb +131 -0
  22. data/lib/typhoeus/multi.rb +4 -3
  23. data/lib/typhoeus/normalized_header_hash.rb +58 -0
  24. data/lib/typhoeus/remote_proxy_object.rb +6 -6
  25. data/lib/typhoeus/request.rb +118 -31
  26. data/lib/typhoeus/response.rb +61 -4
  27. data/lib/typhoeus/service.rb +20 -0
  28. data/lib/typhoeus/utils.rb +24 -0
  29. data/lib/typhoeus.rb +11 -8
  30. data/profilers/valgrind.rb +24 -0
  31. data/spec/fixtures/result_set.xml +60 -0
  32. data/spec/servers/app.rb +48 -1
  33. data/spec/spec_helper.rb +1 -1
  34. data/spec/typhoeus/easy_spec.rb +126 -4
  35. data/spec/typhoeus/hydra_mock_spec.rb +300 -0
  36. data/spec/typhoeus/hydra_spec.rb +456 -0
  37. data/spec/typhoeus/normalized_header_hash_spec.rb +41 -0
  38. data/spec/typhoeus/remote_spec.rb +2 -2
  39. data/spec/typhoeus/request_spec.rb +247 -0
  40. data/spec/typhoeus/response_spec.rb +104 -1
  41. data/spec/typhoeus/utils_spec.rb +22 -0
  42. data/typhoeus.gemspec +123 -0
  43. metadata +145 -34
  44. data/ext/typhoeus/Makefile +0 -157
@@ -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
- def initialize(initial_pool_size = 10)
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 clear_stubs
22
- @stubs = []
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
- if request.method == :get
33
- if @memoize_requests && @memoized_requests.has_key?(request.url)
34
- if response = @retrieved_from_cache[request.url]
35
- request.response = response
36
- request.call_handlers
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] << request
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
- @stubs.each do |m|
51
- m.requests.each do |request|
52
- m.response.request = request
53
- handle_request(request, m.response)
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
@@ -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