dcu-typhoeus 0.4.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.
@@ -0,0 +1,246 @@
1
+ require 'typhoeus/hydra/callbacks'
2
+ require 'typhoeus/hydra/connect_options'
3
+ require 'typhoeus/hydra/stubbing'
4
+
5
+ module Typhoeus
6
+ class Hydra
7
+ include ConnectOptions
8
+ include Stubbing
9
+ extend Callbacks
10
+
11
+ def initialize(options = {})
12
+ @memoize_requests = true
13
+ @multi = Multi.new
14
+ @easy_pool = []
15
+ initial_pool_size = options[:initial_pool_size] || 10
16
+ @max_concurrency = options[:max_concurrency] || 200
17
+ initial_pool_size.times { @easy_pool << Easy.new }
18
+ @memoized_requests = {}
19
+ @retrieved_from_cache = {}
20
+ @queued_requests = []
21
+ @running_requests = 0
22
+
23
+ self.stubs = []
24
+ @active_stubs = []
25
+ end
26
+
27
+ def self.hydra
28
+ @hydra ||= new
29
+ end
30
+
31
+ def self.hydra=(val)
32
+ @hydra = val
33
+ end
34
+
35
+ #
36
+ # Abort the run on a best-effort basis.
37
+ #
38
+ # It won't abort the current burst of @max_concurrency requests,
39
+ # however it won't fire the rest of the queued requests so the run
40
+ # will be aborted as soon as possible...
41
+ #
42
+ def abort
43
+ @queued_requests.clear
44
+ end
45
+
46
+ def clear_cache_callbacks
47
+ @cache_setter = nil
48
+ @cache_getter = nil
49
+ end
50
+
51
+ def fire_and_forget
52
+ @queued_requests.each {|r| queue(r, false)}
53
+ @multi.fire_and_forget
54
+ end
55
+
56
+ def queue(request, obey_concurrency_limit = true)
57
+ return if assign_to_stub(request)
58
+
59
+ # At this point, we are running over live HTTP. Make sure we haven't
60
+ # disabled live requests.
61
+ check_allow_net_connect!(request)
62
+
63
+ if @running_requests >= @max_concurrency && obey_concurrency_limit
64
+ @queued_requests << request
65
+ else
66
+ if request.method == :get
67
+ if @memoize_requests && @memoized_requests.key?(request.url)
68
+ if response = @retrieved_from_cache[request.url]
69
+ request.response = response
70
+ request.call_handlers
71
+ else
72
+ @memoized_requests[request.url] << request
73
+ end
74
+ else
75
+ @memoized_requests[request.url] = [] if @memoize_requests
76
+ get_from_cache_or_queue(request)
77
+ end
78
+ else
79
+ get_from_cache_or_queue(request)
80
+ end
81
+ end
82
+ end
83
+
84
+ def run
85
+ while !@active_stubs.empty?
86
+ m = @active_stubs.first
87
+ while request = m.requests.shift
88
+ response = m.response
89
+ response.request = request
90
+ handle_request(request, response)
91
+ end
92
+ @active_stubs.delete(m)
93
+ end
94
+
95
+ @multi.perform
96
+ ensure
97
+ @multi.reset_easy_handles{|easy| release_easy_object(easy)}
98
+ @memoized_requests = {}
99
+ @retrieved_from_cache = {}
100
+ @running_requests = 0
101
+ end
102
+
103
+ def disable_memoization
104
+ @memoize_requests = false
105
+ end
106
+
107
+ def cache_getter(&block)
108
+ @cache_getter = block
109
+ end
110
+
111
+ def cache_setter(&block)
112
+ @cache_setter = block
113
+ end
114
+
115
+ def on_complete(&block)
116
+ @on_complete = block
117
+ end
118
+
119
+ def on_complete=(proc)
120
+ @on_complete = proc
121
+ end
122
+
123
+ private
124
+
125
+ def get_from_cache_or_queue(request)
126
+ if @cache_getter
127
+ val = @cache_getter.call(request)
128
+ if val
129
+ @retrieved_from_cache[request.url] = val
130
+ queue_next
131
+ handle_request(request, val, false)
132
+ else
133
+ @multi.add(get_easy_object(request))
134
+ end
135
+ else
136
+ @multi.add(get_easy_object(request))
137
+ end
138
+ end
139
+
140
+ def get_easy_object(request)
141
+ @running_requests += 1
142
+
143
+ easy = @easy_pool.pop || Easy.new
144
+ easy.verbose = request.verbose
145
+ if request.username || request.password
146
+ auth = { :username => request.username, :password => request.password }
147
+ auth[:method] = request.auth_method if request.auth_method
148
+ easy.auth = auth
149
+ end
150
+
151
+ if request.proxy
152
+ proxy = { :server => request.proxy }
153
+ proxy[:type] = request.proxy_type if request.proxy_type
154
+ easy.proxy = proxy if request.proxy
155
+ end
156
+
157
+ if request.proxy_username || request.proxy_password
158
+ auth = { :username => request.proxy_username, :password => request.proxy_password }
159
+ auth[:method] = request.proxy_auth_method if request.proxy_auth_method
160
+ easy.proxy_auth = auth
161
+ end
162
+
163
+ easy.url = request.url
164
+ easy.method = request.method
165
+ easy.params = request.params if request.method == :post && request.params.present?
166
+ easy.headers = request.headers if request.headers
167
+ easy.request_body = request.body if request.method == :post && request.body.present?
168
+ easy.timeout = request.timeout if request.timeout
169
+ easy.connect_timeout = request.connect_timeout if request.connect_timeout
170
+ easy.interface = request.interface if request.interface
171
+ easy.follow_location = request.follow_location if request.follow_location
172
+ easy.max_redirects = request.max_redirects if request.max_redirects
173
+ easy.disable_ssl_peer_verification if request.disable_ssl_peer_verification
174
+ easy.disable_ssl_host_verification if request.disable_ssl_host_verification
175
+ easy.ssl_cert = request.ssl_cert
176
+ easy.ssl_cert_type = request.ssl_cert_type
177
+ easy.ssl_key = request.ssl_key
178
+ easy.ssl_key_type = request.ssl_key_type
179
+ easy.ssl_key_password = request.ssl_key_password
180
+ easy.ssl_cacert = request.ssl_cacert
181
+ easy.ssl_capath = request.ssl_capath
182
+ easy.ssl_version = request.ssl_version || :default
183
+ easy.verbose = request.verbose
184
+
185
+ easy.on_success do |easy|
186
+ queue_next
187
+ handle_request(request, response_from_easy(easy, request))
188
+ release_easy_object(easy)
189
+ end
190
+ easy.on_failure do |easy|
191
+ queue_next
192
+ handle_request(request, response_from_easy(easy, request))
193
+ release_easy_object(easy)
194
+ end
195
+ easy.set_headers
196
+ easy
197
+ end
198
+
199
+ def queue_next
200
+ @running_requests -= 1
201
+ queue(@queued_requests.shift) unless @queued_requests.empty?
202
+ end
203
+
204
+ def release_easy_object(easy)
205
+ easy.reset
206
+ @easy_pool.push easy
207
+ end
208
+
209
+ def handle_request(request, response, live_request = true)
210
+ request.response = response
211
+
212
+ self.class.run_global_hooks_for(:after_request_before_on_complete,
213
+ request)
214
+
215
+ if live_request && request.cache_timeout && @cache_setter
216
+ @cache_setter.call(request)
217
+ end
218
+ @on_complete.call(response) if @on_complete
219
+
220
+ request.call_handlers
221
+ if requests = @memoized_requests[request.url]
222
+ requests.each do |r|
223
+ r.response = response
224
+ r.call_handlers
225
+ end
226
+ end
227
+ end
228
+
229
+ def response_from_easy(easy, request)
230
+ Response.new(:code => easy.response_code,
231
+ :headers => easy.response_header,
232
+ :body => easy.response_body,
233
+ :time => easy.total_time_taken,
234
+ :start_transfer_time => easy.start_transfer_time,
235
+ :app_connect_time => easy.app_connect_time,
236
+ :pretransfer_time => easy.pretransfer_time,
237
+ :connect_time => easy.connect_time,
238
+ :name_lookup_time => easy.name_lookup_time,
239
+ :effective_url => easy.effective_url,
240
+ :primary_ip => easy.primary_ip,
241
+ :curl_return_code => easy.curl_return_code,
242
+ :curl_error_message => easy.curl_error_message,
243
+ :request => request)
244
+ end
245
+ end
246
+ 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::Header.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 = Header.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
@@ -0,0 +1,146 @@
1
+ module Typhoeus
2
+ class Multi
3
+ attr_reader :easy_handles
4
+
5
+ def initialize
6
+ Curl.init
7
+
8
+ @handle = Curl.multi_init
9
+ @active = 0
10
+ @running = 0
11
+ @easy_handles = []
12
+
13
+ @timeout = ::FFI::MemoryPointer.new(:long)
14
+ @timeval = Curl::Timeval.new
15
+ @fd_read = Curl::FDSet.new
16
+ @fd_write = Curl::FDSet.new
17
+ @fd_excep = Curl::FDSet.new
18
+ @max_fd = ::FFI::MemoryPointer.new(:int)
19
+
20
+ ObjectSpace.define_finalizer(self, self.class.finalizer(self))
21
+ end
22
+
23
+ def self.finalizer(multi)
24
+ proc { Curl.multi_cleanup(multi.handle) }
25
+ end
26
+
27
+ def add(easy)
28
+ raise "trying to add easy handle twice" if @easy_handles.include?(easy)
29
+ easy.set_headers() if easy.headers.empty?
30
+
31
+ code = Curl.multi_add_handle(@handle, easy.handle)
32
+ raise RuntimeError.new("An error occured adding the handle: #{code}: #{Curl.multi_strerror(code)}") if code != :call_multi_perform and code != :ok
33
+
34
+ do_perform if code == :call_multi_perform
35
+
36
+ @active += 1
37
+ @easy_handles << easy
38
+ easy
39
+ end
40
+
41
+ def remove(easy)
42
+ if @easy_handles.include?(easy)
43
+ @active -= 1
44
+ Curl.multi_remove_handle(@handle, easy.handle)
45
+ @easy_handles.delete(easy)
46
+ end
47
+ end
48
+
49
+ def perform
50
+ while @active > 0
51
+ run
52
+ while @running > 0
53
+ # get the curl-suggested timeout
54
+ code = Curl.multi_timeout(@handle, @timeout)
55
+ raise RuntimeError.new("an error occured getting the timeout: #{code}: #{Curl.multi_strerror(code)}") if code != :ok
56
+ timeout = @timeout.read_long
57
+ if timeout == 0 # no delay
58
+ run
59
+ next
60
+ elsif timeout < 0
61
+ timeout = 1
62
+ end
63
+
64
+ # load the fd sets from the multi handle
65
+ @fd_read.clear
66
+ @fd_write.clear
67
+ @fd_excep.clear
68
+ code = Curl.multi_fdset(@handle, @fd_read, @fd_write, @fd_excep, @max_fd)
69
+ raise RuntimeError.new("an error occured getting the fdset: #{code}: #{Curl.multi_strerror(code)}") if code != :ok
70
+
71
+ max_fd = @max_fd.read_int
72
+ if max_fd == -1
73
+ # curl is doing something special so let it run for a moment
74
+ sleep(0.001)
75
+ else
76
+ @timeval[:sec] = timeout / 1000
77
+ @timeval[:usec] = (timeout * 1000) % 1000000
78
+
79
+ code = Curl.select(max_fd + 1, @fd_read, @fd_write, @fd_excep, @timeval)
80
+ raise RuntimeError.new("error on thread select: #{::FFI.errno}") if code < 0
81
+ end
82
+
83
+ run
84
+ end
85
+ end
86
+ reset_easy_handles
87
+ end
88
+
89
+ def fire_and_forget
90
+ run
91
+ end
92
+
93
+ # check for finished easy handles and remove from the multi handle
94
+ def read_info
95
+ msgs_left = ::FFI::MemoryPointer.new(:int)
96
+ while not (msg = Curl.multi_info_read(@handle, msgs_left)).null?
97
+ next if msg[:code] != :done
98
+
99
+ easy = @easy_handles.find {|easy| easy.handle == msg[:easy_handle] }
100
+ next if not easy
101
+
102
+ response_code = ::FFI::MemoryPointer.new(:long)
103
+ response_code.write_long(-1)
104
+ Curl.easy_getinfo(easy.handle, :response_code, response_code)
105
+ response_code = response_code.read_long
106
+ remove(easy)
107
+
108
+ easy.curl_return_code = msg[:data][:code]
109
+ if easy.curl_return_code != 0 then easy.failure
110
+ elsif (200..299).member?(response_code) or response_code == 0 then easy.success
111
+ else easy.failure
112
+ end
113
+ end
114
+ end
115
+
116
+ def cleanup
117
+ Curl.multi_cleanup(@handle)
118
+ @active = 0
119
+ @running = 0
120
+ @easy_handles = []
121
+ end
122
+
123
+ def reset_easy_handles
124
+ @easy_handles.dup.each do |easy|
125
+ remove(easy)
126
+ yield easy if block_given?
127
+ end
128
+ end
129
+
130
+ private
131
+
132
+ # called by perform and fire_and_forget
133
+ def run
134
+ begin code = do_perform end while code == :call_multi_perform
135
+ raise RuntimeError.new("an error occured while running perform: #{code}: #{Curl.multi_strerror(code)}") if code != :ok
136
+ read_info
137
+ end
138
+
139
+ def do_perform
140
+ running = ::FFI::MemoryPointer.new(:int)
141
+ code = Curl.multi_perform(@handle, running)
142
+ @running = running.read_int
143
+ code
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,43 @@
1
+ require 'tempfile'
2
+
3
+ module Typhoeus
4
+ class ParamProcessor
5
+ class << self
6
+ def traverse_params_hash(hash, result = nil, current_key = nil)
7
+ result ||= { :files => [], :params => [] }
8
+
9
+ hash.keys.sort { |a, b| a.to_s <=> b.to_s }.collect do |key|
10
+ new_key = (current_key ? "#{current_key}[#{key}]" : key).to_s
11
+ current_value = hash[key]
12
+ process_value current_value, :result => result, :new_key => new_key
13
+ end
14
+ result
15
+ end
16
+
17
+ def process_value(current_value, options)
18
+ result = options[:result]
19
+ new_key = options[:new_key]
20
+
21
+ case current_value
22
+ when Hash
23
+ traverse_params_hash(current_value, result, new_key)
24
+ when Array
25
+ current_value.each do |v|
26
+ result[:params] << [new_key, v.to_s]
27
+ end
28
+ when File, Tempfile
29
+ filename = File.basename(current_value.path)
30
+ types = MIME::Types.type_for(filename)
31
+ result[:files] << [
32
+ new_key,
33
+ filename,
34
+ types.empty? ? 'application/octet-stream' : types[0].to_s,
35
+ File.expand_path(current_value.path)
36
+ ]
37
+ else
38
+ result[:params] << [new_key, current_value.to_s]
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end