persistent_http 1.0.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 ADDED
@@ -0,0 +1,4 @@
1
+ /TAGS
2
+ /pkg
3
+ /doc
4
+ *.swp
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ === 1.0.0 / 2010-09-16
2
+
3
+ * Major Enhancements
4
+ * Based on drbrain/net-http-persistent but uses a connection pool instead of connection/thread
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Eric Hodel, Aaron Patterson
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ 'Software'), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,58 @@
1
+ = persistent_http
2
+
3
+ * http://github.com/bpardee/persistent_http
4
+
5
+ == DESCRIPTION:
6
+
7
+ Persistent connections using Net::HTTP with a connection pool.
8
+
9
+ This is based on Eric Holder's Net::HTTP::Persistent libary but uses
10
+ a connection pool of Net::HTTP objects instead of a connection per
11
+ thread. C/T is fine if you're only using your http threads to make
12
+ connections but if you use them in child threads then I suspect you
13
+ will have a thread memory leak. Also, you will generally get less
14
+ connection resets if the most recently used connection is always
15
+ returned.
16
+
17
+ == FEATURES/PROBLEMS:
18
+
19
+ * Supports SSL
20
+ * Thread-safe
21
+ * Pure ruby
22
+ * Timeout-less speed boost for 1.8 (by Aaron Patterson)
23
+
24
+ == INSTALL:
25
+
26
+ gem install persistent_http
27
+
28
+ == EXAMPLE USAGE:
29
+
30
+ require 'persistent_http'
31
+
32
+ class MyHTTPClient
33
+ @@persistent_http = PersistentHTTP.new(
34
+ :name => 'MyHTTPClient',
35
+ :logger => Rails.logger,
36
+ :pool_size => 10,
37
+ :warn_timeout => 0.25,
38
+ :force_retry => true,
39
+ :url => 'https://www.example.com/echo/foo' # equivalent to :use_ssl => true, :host => 'www.example.com', :default_path => '/echo/foo'
40
+ )
41
+
42
+ def send_get_message
43
+ response = @@persistent_http.request
44
+ ... Handle response as you would a normal Net::HTTPResponse ...
45
+ end
46
+
47
+ def send_post_message
48
+ request = Net::HTTP::Post.new('/perform_service)
49
+ ... Modify request as needed ...
50
+ response = @@persistent_http.request(request)
51
+ ... Handle response as you would a normal Net::HTTPResponse ...
52
+ end
53
+ end
54
+
55
+
56
+ == Copyright
57
+
58
+ Copyright (c) 2010 Eric Hodel, Aaron Patterson, Brad Pardee. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gemspec|
7
+ gemspec.name = "persistent_http"
8
+ gemspec.summary = "Persistent HTTP connections using a connection pool"
9
+ gemspec.description = "Persistent HTTP connections using a connection pool"
10
+ gemspec.email = "bradpardee@gmail.com"
11
+ gemspec.homepage = "http://github.com/bpardee/persistent_http"
12
+ gemspec.authors = ["Brad Pardee"]
13
+ end
14
+ rescue LoadError
15
+ puts "Jeweler not available. Install it with: gem install jeweler"
16
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,27 @@
1
+ require 'net/protocol'
2
+
3
+ ##
4
+ # Aaron Patterson's monkeypatch (accepted into 1.9.1) to fix Net::HTTP's speed
5
+ # problems.
6
+ #
7
+ # http://gist.github.com/251244
8
+
9
+ class Net::BufferedIO #:nodoc:
10
+ alias :old_rbuf_fill :rbuf_fill
11
+
12
+ def rbuf_fill
13
+ if @io.respond_to? :read_nonblock then
14
+ begin
15
+ @rbuf << @io.read_nonblock(65536)
16
+ rescue Errno::EWOULDBLOCK => e
17
+ retry if IO.select [@io], nil, nil, @read_timeout
18
+ raise Timeout::Error, e.message
19
+ end
20
+ else # SSL sockets do not have read_nonblock
21
+ timeout @read_timeout do
22
+ @rbuf << @io.sysread(65536)
23
+ end
24
+ end
25
+ end
26
+ end if RUBY_VERSION < '1.9'
27
+
@@ -0,0 +1,444 @@
1
+ require 'net/http'
2
+ require 'persistent_http/faster'
3
+ require 'uri'
4
+ require 'gene_pool'
5
+
6
+ ##
7
+ # Persistent connections for Net::HTTP
8
+ #
9
+ # PersistentHTTP maintains a connection pool of Net::HTTP persistent connections.
10
+ # When connections fail due to resets or bad responses, the connection is renewed
11
+ # and the request is retried per RFC 2616 (POST requests will only get retried if
12
+ # the :force_retry option is set to true).
13
+ #
14
+ # Example:
15
+ #
16
+ # @@persistent_http = PersistentHTTP.new(
17
+ # :name => 'MyHTTPClient',
18
+ # :logger => Rails.logger,
19
+ # :pool_size => 10,
20
+ # :warn_timeout => 0.25,
21
+ # :force_retry => true,
22
+ # :url => 'https://www.example.com/echo/foo' # equivalent to :use_ssl => true, :host => 'www.example.com', :default_path => '/echo/foo'
23
+ # )
24
+ #
25
+ # def send_get_message
26
+ # response = @@persistent_http.request
27
+ # ... Handle response as you would a normal Net::HTTPResponse ...
28
+ # end
29
+ #
30
+ # def send_post_message
31
+ # request = Net::HTTP::Post.new('/perform_service)
32
+ # ... Modify request as needed ...
33
+ # response = @@persistent_http.request(request)
34
+ # ... Handle response as you would a normal Net::HTTPResponse ...
35
+ # end
36
+
37
+ class PersistentHTTP
38
+
39
+ ##
40
+ # The version of PersistentHTTP use are using
41
+ VERSION = '1.0.0'
42
+
43
+ ##
44
+ # Error class for errors raised by PersistentHTTP. Various
45
+ # SystemCallErrors are re-raised with a human-readable message under this
46
+ # class.
47
+ class Error < StandardError; end
48
+
49
+ ##
50
+ # An SSL certificate authority. Setting this will set verify_mode to
51
+ # VERIFY_PEER.
52
+ attr_accessor :ca_file
53
+
54
+ ##
55
+ # This client's OpenSSL::X509::Certificate
56
+ attr_accessor :certificate
57
+
58
+ ##
59
+ # Sends debug_output to this IO via Net::HTTP#set_debug_output.
60
+ #
61
+ # Never use this method in production code, it causes a serious security
62
+ # hole.
63
+ attr_accessor :debug_output
64
+
65
+ ##
66
+ # Default path for the request
67
+ attr_accessor :default_path
68
+
69
+ ##
70
+ # Retry even for non-idempotent (POST) requests.
71
+ attr_accessor :force_retry
72
+
73
+ ##
74
+ # Headers that are added to every request
75
+ attr_accessor :headers
76
+
77
+ ##
78
+ # Host for the Net:HTTP connection
79
+ attr_reader :host
80
+
81
+ ##
82
+ # HTTP version to enable version specific features.
83
+ attr_reader :http_version
84
+
85
+ ##
86
+ # The value sent in the Keep-Alive header. Defaults to 30. Not needed for
87
+ # HTTP/1.1 servers.
88
+ #
89
+ # This may not work correctly for HTTP/1.0 servers
90
+ #
91
+ # This method may be removed in a future version as RFC 2616 does not
92
+ # require this header.
93
+ attr_accessor :keep_alive
94
+
95
+ ##
96
+ # Logger for message logging.
97
+ attr_accessor :logger
98
+
99
+ ##
100
+ # A name for this connection. Allows you to keep your connections apart
101
+ # from everybody else's.
102
+ attr_reader :name
103
+
104
+ ##
105
+ # Seconds to wait until a connection is opened. See Net::HTTP#open_timeout
106
+ attr_accessor :open_timeout
107
+
108
+ ##
109
+ # The maximum size of the connection pool
110
+ attr_reader :pool_size
111
+
112
+ ##
113
+ # Port for the Net:HTTP connection
114
+ attr_reader :port
115
+
116
+ ##
117
+ # This client's SSL private key
118
+ attr_accessor :private_key
119
+
120
+ ##
121
+ # The URL through which requests will be proxied
122
+ attr_reader :proxy_uri
123
+
124
+ ##
125
+ # Seconds to wait until reading one block. See Net::HTTP#read_timeout
126
+ attr_accessor :read_timeout
127
+
128
+ ##
129
+ # Use ssl if set
130
+ attr_reader :use_ssl
131
+
132
+ ##
133
+ # SSL verification callback. Used when ca_file is set.
134
+ attr_accessor :verify_callback
135
+
136
+ ##
137
+ # HTTPS verify mode. Defaults to OpenSSL::SSL::VERIFY_NONE which ignores
138
+ # certificate problems.
139
+ #
140
+ # You can use +verify_mode+ to override any default values.
141
+ attr_accessor :verify_mode
142
+
143
+ ##
144
+ # The threshold in seconds for checking out a connection at which a warning
145
+ # will be logged via the logger
146
+ attr_accessor :warn_timeout
147
+
148
+ ##
149
+ # Creates a new PersistentHTTP.
150
+ #
151
+ # Set +name+ to keep your connections apart from everybody else's. Not
152
+ # required currently, but highly recommended. Your library name should be
153
+ # good enough. This parameter will be required in a future version.
154
+ #
155
+ # +proxy+ may be set to a URI::HTTP or :ENV to pick up proxy options from
156
+ # the environment. See proxy_from_env for details.
157
+ #
158
+ # In order to use a URI for the proxy you'll need to do some extra work
159
+ # beyond URI.parse:
160
+ #
161
+ # proxy = URI.parse 'http://proxy.example'
162
+ # proxy.user = 'AzureDiamond'
163
+ # proxy.password = 'hunter2'
164
+
165
+ def initialize(options={})
166
+ @name = options[:name] || 'PersistentHTTP'
167
+ @ca_file = options[:ca_file]
168
+ @certificate = options[:certificate]
169
+ @debug_output = options[:debug_output]
170
+ @default_path = options[:default_path]
171
+ @force_retry = options[:force_retry]
172
+ @headers = options[:header] || {}
173
+ @host = options[:host]
174
+ @keep_alive = options[:keep_alive] || 30
175
+ @logger = options[:logger]
176
+ @open_timeout = options[:open_timeout]
177
+ @pool_size = options[:pool_size] || 1
178
+ @port = options[:port]
179
+ @private_key = options[:private_key]
180
+ @read_timeout = options[:read_timeout]
181
+ @use_ssl = options[:use_ssl]
182
+ @verify_callback = options[:verify_callback]
183
+ @verify_mode = options[:verify_mode]
184
+ @warn_timeout = options[:warn_timeout] || 0.5
185
+
186
+ url = options[:url]
187
+ if url
188
+ url = URI.parse(url) if url.kind_of? String
189
+ @default_path ||= url.request_uri
190
+ @host ||= url.host
191
+ @port ||= url.port
192
+ @use_ssl ||= url.scheme == 'https'
193
+ end
194
+
195
+ @port ||= (@use_ssl ? 443 : 80)
196
+
197
+ # Hash containing the request counts based on the connection
198
+ @count_hash = Hash.new(0)
199
+
200
+ raise 'host not set' unless @host
201
+ net_http_args = [@host, @port]
202
+ connection_id = net_http_args.join ':'
203
+
204
+ proxy = options[:proxy]
205
+
206
+ @proxy_uri = case proxy
207
+ when :ENV then proxy_from_env
208
+ when URI::HTTP then proxy
209
+ when nil then # ignore
210
+ else raise ArgumentError, 'proxy must be :ENV or a URI::HTTP'
211
+ end
212
+
213
+ if @proxy_uri then
214
+ @proxy_args = [
215
+ @proxy_uri.host,
216
+ @proxy_uri.port,
217
+ @proxy_uri.user,
218
+ @proxy_uri.password,
219
+ ]
220
+
221
+ @proxy_connection_id = [nil, *@proxy_args].join ':'
222
+
223
+ connection_id << @proxy_connection_id
224
+ net_http_args.concat @proxy_args
225
+ end
226
+
227
+ @pool = GenePool.new(:name => name + '-' + connection_id,
228
+ :pool_size => @pool_size,
229
+ :warn_timeout => @warn_timeout,
230
+ :logger => @logger) do
231
+ begin
232
+ connection = Net::HTTP.new(*net_http_args)
233
+ connection.set_debug_output @debug_output if @debug_output
234
+ connection.open_timeout = @open_timeout if @open_timeout
235
+ connection.read_timeout = @read_timeout if @read_timeout
236
+
237
+ ssl connection if @use_ssl
238
+
239
+ connection.start
240
+ connection
241
+ rescue Errno::ECONNREFUSED
242
+ raise Error, "connection refused: #{connection.address}:#{connection.port}"
243
+ rescue Errno::EHOSTDOWN
244
+ raise Error, "host down: #{connection.address}:#{connection.port}"
245
+ end
246
+ end
247
+ end
248
+
249
+ ##
250
+ # Makes a request per +req+. If +req+ is nil a Net::HTTP::Get is performed
251
+ # against +default_path+.
252
+ #
253
+ # If a block is passed #request behaves like Net::HTTP#request (the body of
254
+ # the response will not have been read).
255
+ #
256
+ # +req+ must be a Net::HTTPRequest subclass (see Net::HTTP for a list).
257
+ #
258
+ # If there is an error and the request is idempontent according to RFC 2616
259
+ # it will be retried automatically.
260
+
261
+ def request req = nil, &block
262
+ retried = false
263
+ bad_response = false
264
+
265
+ req = Net::HTTP::Get.new @default_path unless req
266
+
267
+ headers.each do |pair|
268
+ req.add_field(*pair)
269
+ end
270
+
271
+ req.add_field 'Connection', 'keep-alive'
272
+ req.add_field 'Keep-Alive', @keep_alive
273
+
274
+ @pool.with_connection do |connection|
275
+ begin
276
+ response = connection.request req, &block
277
+ @http_version ||= response.http_version
278
+ @count_hash[connection.object_id] += 1
279
+ return response
280
+
281
+ rescue Timeout::Error => e
282
+ due_to = "(due to #{e.message} - #{e.class})"
283
+ message = error_message connection
284
+ @logger.info "#{name}: Removing connection #{due_to} #{message}" if @logger
285
+ remove connection
286
+ raise
287
+
288
+ rescue Net::HTTPBadResponse => e
289
+ message = error_message connection
290
+ if bad_response or not (idempotent? req or @force_retry)
291
+ @logger.info "#{name}: Removing connection because of too many bad responses #{message}" if @logger
292
+ remove connection
293
+ raise Error, "too many bad responses #{message}"
294
+ else
295
+ bad_response = true
296
+ @logger.info "#{name}: Renewing connection because of bad response #{message}" if @logger
297
+ connection = renew connection
298
+ retry
299
+ end
300
+
301
+ rescue IOError, EOFError, Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EPIPE => e
302
+ due_to = "(due to #{e.message} - #{e.class})"
303
+ message = error_message connection
304
+ if retried or not (idempotent? req or @force_retry)
305
+ @logger.info "#{name}: Removing connection #{due_to} #{message}" if @logger
306
+ remove connection
307
+ raise Error, "too many connection resets #{due_to} #{message}"
308
+ else
309
+ retried = true
310
+ @logger.info "#{name}: Renewing connection #{due_to} #{message}" if @logger
311
+ connection = renew connection
312
+ retry
313
+ end
314
+ end
315
+ end
316
+ end
317
+
318
+ ##
319
+ # Shuts down all connections.
320
+
321
+ def shutdown
322
+ raise 'Shutdown not implemented'
323
+ # TBD - need to think about this one
324
+ @count_hash = nil
325
+ end
326
+
327
+ #######
328
+ private
329
+ #######
330
+
331
+ ##
332
+ # Returns an error message containing the number of requests performed on
333
+ # this connection
334
+
335
+ def error_message connection
336
+ requests = @count_hash[connection.object_id] || 0
337
+ "after #{requests} requests on #{connection.object_id}"
338
+ end
339
+
340
+ ##
341
+ # URI::escape wrapper
342
+
343
+ def escape str
344
+ URI.escape str if str
345
+ end
346
+
347
+ ##
348
+ # Finishes the Net::HTTP +connection+
349
+
350
+ def finish connection
351
+ @count_hash.delete(connection.object_id)
352
+ connection.finish
353
+ rescue IOError
354
+ end
355
+
356
+ ##
357
+ # Is +req+ idempotent according to RFC 2616?
358
+
359
+ def idempotent? req
360
+ case req
361
+ when Net::HTTP::Delete, Net::HTTP::Get, Net::HTTP::Head,
362
+ Net::HTTP::Options, Net::HTTP::Put, Net::HTTP::Trace then
363
+ true
364
+ end
365
+ end
366
+
367
+ ##
368
+ # Adds "http://" to the String +uri+ if it is missing.
369
+
370
+ def normalize_uri uri
371
+ (uri =~ /^https?:/) ? uri : "http://#{uri}"
372
+ end
373
+
374
+ ##
375
+ # Creates a URI for an HTTP proxy server from ENV variables.
376
+ #
377
+ # If +HTTP_PROXY+ is set a proxy will be returned.
378
+ #
379
+ # If +HTTP_PROXY_USER+ or +HTTP_PROXY_PASS+ are set the URI is given the
380
+ # indicated user and password unless HTTP_PROXY contains either of these in
381
+ # the URI.
382
+ #
383
+ # For Windows users lowercase ENV variables are preferred over uppercase ENV
384
+ # variables.
385
+
386
+ def proxy_from_env
387
+ env_proxy = ENV['http_proxy'] || ENV['HTTP_PROXY']
388
+
389
+ return nil if env_proxy.nil? or env_proxy.empty?
390
+
391
+ uri = URI.parse(normalize_uri(env_proxy))
392
+
393
+ unless uri.user or uri.password then
394
+ uri.user = escape ENV['http_proxy_user'] || ENV['HTTP_PROXY_USER']
395
+ uri.password = escape ENV['http_proxy_pass'] || ENV['HTTP_PROXY_PASS']
396
+ end
397
+
398
+ uri
399
+ end
400
+
401
+ ##
402
+ # Finishes then removes the Net::HTTP +connection+
403
+
404
+ def remove connection
405
+ finish connection
406
+ @pool.remove(connection)
407
+ end
408
+
409
+ ##
410
+ # Finishes then renews the Net::HTTP +connection+. It may be unnecessary
411
+ # to completely recreate the connection but connections that get timed out
412
+ # in JRuby leave the ssl context in a frozen object state.
413
+
414
+ def renew connection
415
+ finish connection
416
+ connection = @pool.renew(connection)
417
+ end
418
+
419
+ ##
420
+ # Enables SSL on +connection+
421
+
422
+ def ssl connection
423
+ require 'net/https'
424
+ connection.use_ssl = true
425
+
426
+ # suppress warning but allow override
427
+ connection.verify_mode = OpenSSL::SSL::VERIFY_NONE unless @verify_mode
428
+
429
+ if @ca_file then
430
+ connection.ca_file = @ca_file
431
+ connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
432
+ connection.verify_callback = @verify_callback if @verify_callback
433
+ end
434
+
435
+ if @certificate and @private_key then
436
+ connection.cert = @certificate
437
+ connection.key = @private_key
438
+ end
439
+
440
+ connection.verify_mode = @verify_mode if @verify_mode
441
+ end
442
+
443
+ end
444
+
@@ -0,0 +1,50 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{persistent_http}
8
+ s.version = "1.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Brad Pardee"]
12
+ s.date = %q{2010-10-03}
13
+ s.description = %q{Persistent HTTP connections using a connection pool}
14
+ s.email = %q{bradpardee@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".gitignore",
21
+ "History.txt",
22
+ "LICENSE",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "lib/persistent_http.rb",
27
+ "lib/persistent_http/faster.rb",
28
+ "persistent_http.gemspec",
29
+ "test/persistent_http_test.rb"
30
+ ]
31
+ s.homepage = %q{http://github.com/bpardee/persistent_http}
32
+ s.rdoc_options = ["--charset=UTF-8"]
33
+ s.require_paths = ["lib"]
34
+ s.rubygems_version = %q{1.3.6}
35
+ s.summary = %q{Persistent HTTP connections using a connection pool}
36
+ s.test_files = [
37
+ "test/persistent_http_test.rb"
38
+ ]
39
+
40
+ if s.respond_to? :specification_version then
41
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
42
+ s.specification_version = 3
43
+
44
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
45
+ else
46
+ end
47
+ else
48
+ end
49
+ end
50
+
@@ -0,0 +1,647 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+ require 'persistent_http'
5
+ require 'openssl'
6
+ require 'stringio'
7
+ require 'logger'
8
+
9
+ CMD_SUCCESS = 'success'
10
+ CMD_SLEEP = 'sleep'
11
+ CMD_BAD_RESPONSE = 'bad_response'
12
+ CMD_EOF_ERROR = 'eof_error'
13
+ CMD_CONNRESET = 'connreset'
14
+ CMD_ECHO = 'echo'
15
+
16
+ PASS = 'pass'
17
+ FAIL = 'fail'
18
+
19
+ DUMMY_OPEN_TIMEOUT_FOR_HOSTDOWN = 9000
20
+ DUMMY_OPEN_TIMEOUT_FOR_CONNREFUSED = 9001
21
+
22
+ $debug = false
23
+ $count = -1
24
+
25
+ class Net::HTTP
26
+ def connect
27
+ raise Errno::EHOSTDOWN if open_timeout == DUMMY_OPEN_TIMEOUT_FOR_HOSTDOWN
28
+ raise Errno::ECONNREFUSED if open_timeout == DUMMY_OPEN_TIMEOUT_FOR_CONNREFUSED
29
+ end
30
+
31
+ def successful_response
32
+ r = Net::HTTPResponse.allocate
33
+ def r.http_version() '1.1'; end
34
+ def r.read_body() :read_body; end
35
+ yield r if block_given?
36
+ r
37
+ end
38
+
39
+ def request(req, &block)
40
+ $count += 1
41
+ puts "path=#{req.path} count=#{$count}" if $debug
42
+ args = req.path[1..-1].split('/')
43
+ cmd = args.shift
44
+ i = $count % args.size if args.size > 0
45
+ puts "i=#{i}" if $debug
46
+ if cmd == CMD_ECHO
47
+ res = successful_response(&block)
48
+ eval "def res.body() \"#{req.body}\" end"
49
+ return res
50
+ elsif cmd == CMD_SUCCESS || args[i] == PASS
51
+ return successful_response(&block)
52
+ end
53
+ case cmd
54
+ when CMD_SLEEP
55
+ sleep args[i].to_i
56
+ return successful_response(&block)
57
+ when CMD_BAD_RESPONSE
58
+ raise Net::HTTPBadResponse.new('Dummy bad response')
59
+ when CMD_EOF_ERROR
60
+ raise EOFError.new('Dummy EOF error')
61
+ when CMD_CONNRESET
62
+ raise Errno::ECONNRESET
63
+ else
64
+ return successful_response(&block)
65
+ end
66
+ end
67
+ end
68
+
69
+ class PersistentHTTP
70
+ attr_reader :pool
71
+
72
+ # Make private methods public
73
+ send(:public, *(self.private_instance_methods - Object.private_instance_methods))
74
+ end
75
+
76
+ class PersistentHTTPTest < Test::Unit::TestCase
77
+
78
+ def clear_proxy_env
79
+ ENV.delete 'http_proxy'
80
+ ENV.delete 'HTTP_PROXY'
81
+ ENV.delete 'http_proxy_user'
82
+ ENV.delete 'HTTP_PROXY_USER'
83
+ ENV.delete 'http_proxy_pass'
84
+ ENV.delete 'HTTP_PROXY_PASS'
85
+ end
86
+
87
+ def uri_for(*args)
88
+ '/' + args.join('/')
89
+ end
90
+
91
+ def get_request(*args)
92
+ puts "uri=#{uri_for(args)}" if $debug
93
+ $count = -1
94
+ return Net::HTTP::Get.new(uri_for(args))
95
+ end
96
+
97
+ def post_request(*args)
98
+ puts "uri=#{uri_for(args)}" if $debug
99
+ $count = -1
100
+ return Net::HTTP::Post.new(uri_for(args))
101
+ end
102
+
103
+ def http_and_io(options={})
104
+ io = StringIO.new
105
+ logger = Logger.new(io)
106
+ logger.level = Logger::INFO
107
+ default_options = {:name => 'TestNetHTTPPersistent', :logger => logger, :pool_size => 1}
108
+ http = PersistentHTTP.new(default_options.merge(options))
109
+ [http, io]
110
+ end
111
+
112
+ context 'simple setup' do
113
+ setup do
114
+ @io = StringIO.new
115
+ logger = Logger.new(@io)
116
+ logger.level = Logger::INFO
117
+ @http = PersistentHTTP.new(:host => 'example.com', :name => 'TestNetHTTPPersistent', :logger => logger)
118
+ @http.headers['user-agent'] = 'test ua'
119
+ end
120
+
121
+ should 'have options set' do
122
+ assert_equal @http.proxy_uri, nil
123
+ assert_equal 'TestNetHTTPPersistent', @http.name
124
+ end
125
+
126
+ should 'handle escape' do
127
+ assert_equal nil, @http.escape(nil)
128
+ assert_equal '%20', @http.escape(' ')
129
+ end
130
+
131
+ should 'handle error' do
132
+ req = get_request CMD_EOF_ERROR, PASS, PASS, PASS, PASS, FAIL, PASS, PASS
133
+ 6.times do
134
+ @http.request(req)
135
+ end
136
+ assert_match "after 4 requests on", @io.string
137
+ end
138
+
139
+ should 'handle finish' do
140
+ c = Object.new
141
+ def c.finish; @finished = true end
142
+ def c.finished?; @finished end
143
+ def c.start; @started = true end
144
+ def c.started?; @started end
145
+
146
+ @http.finish c
147
+
148
+ assert !c.started?
149
+ assert c.finished?
150
+ end
151
+
152
+ should 'handle finish io error' do
153
+ c = Object.new
154
+ def c.finish; @finished = true; raise IOError end
155
+ def c.finished?; @finished end
156
+ def c.start; @started = true end
157
+ def c.started?; @started end
158
+
159
+ @http.finish c
160
+
161
+ assert !c.started?
162
+ assert c.finished?
163
+ end
164
+
165
+ should 'fill in http version' do
166
+ assert_nil @http.http_version
167
+ @http.request(get_request(CMD_SUCCESS))
168
+ assert_equal '1.1', @http.http_version
169
+ end
170
+
171
+ should 'handle idempotent' do
172
+ assert @http.idempotent? Net::HTTP::Delete.new '/'
173
+ assert @http.idempotent? Net::HTTP::Get.new '/'
174
+ assert @http.idempotent? Net::HTTP::Head.new '/'
175
+ assert @http.idempotent? Net::HTTP::Options.new '/'
176
+ assert @http.idempotent? Net::HTTP::Put.new '/'
177
+ assert @http.idempotent? Net::HTTP::Trace.new '/'
178
+
179
+ assert !@http.idempotent?(Net::HTTP::Post.new '/')
180
+ end
181
+
182
+ should 'handle normalize_uri' do
183
+ assert_equal 'http://example', @http.normalize_uri('example')
184
+ assert_equal 'http://example', @http.normalize_uri('http://example')
185
+ assert_equal 'https://example', @http.normalize_uri('https://example')
186
+ end
187
+
188
+ should 'handle simple request' do
189
+ req = get_request(CMD_SUCCESS)
190
+ res = @http.request(req)
191
+
192
+ assert_kind_of Net::HTTPResponse, res
193
+
194
+ assert_kind_of Net::HTTP::Get, req
195
+ assert_equal uri_for(CMD_SUCCESS), req.path
196
+ assert_equal 'keep-alive', req['connection']
197
+ assert_equal '30', req['keep-alive']
198
+ assert_match %r%test ua%, req['user-agent']
199
+ end
200
+
201
+ should 'handle request with block' do
202
+ body = nil
203
+
204
+ req = get_request(CMD_SUCCESS)
205
+ res = @http.request(req) do |r|
206
+ body = r.read_body
207
+ end
208
+
209
+ assert_kind_of Net::HTTPResponse, res
210
+ assert !body.nil?
211
+
212
+ assert_kind_of Net::HTTP::Get, req
213
+ assert_equal uri_for(CMD_SUCCESS), req.path
214
+ assert_equal 'keep-alive', req['connection']
215
+ assert_equal '30', req['keep-alive']
216
+ assert_match %r%test ua%, req['user-agent']
217
+ end
218
+
219
+ should 'handle bad response' do
220
+ req = get_request(CMD_BAD_RESPONSE, FAIL, FAIL)
221
+ e = assert_raises PersistentHTTP::Error do
222
+ @http.request req
223
+ end
224
+ assert_match %r%too many bad responses%, e.message
225
+ assert_match %r%Renewing connection because of bad response%, @io.string
226
+ assert_match %r%Removing connection because of too many bad responses%, @io.string
227
+
228
+ res = @http.request(get_request(CMD_SUCCESS))
229
+ assert_kind_of Net::HTTPResponse, res
230
+ end
231
+
232
+ should 'handle connection reset' do
233
+ req = get_request(CMD_CONNRESET, FAIL, FAIL)
234
+ e = assert_raises PersistentHTTP::Error do
235
+ @http.request req
236
+ end
237
+
238
+ assert_match %r%too many connection resets%, e.message
239
+ assert_match %r%Renewing connection %, @io.string
240
+ assert_match %r%Removing connection %, @io.string
241
+
242
+ res = @http.request(get_request(CMD_SUCCESS))
243
+ assert_kind_of Net::HTTPResponse, res
244
+ end
245
+
246
+ should 'retry on bad response' do
247
+ res = @http.request(get_request(CMD_BAD_RESPONSE, FAIL, PASS))
248
+ assert_match %r%Renewing connection because of bad response%, @io.string
249
+ assert_kind_of Net::HTTPResponse, res
250
+ end
251
+
252
+ should 'retry on connection reset' do
253
+ res = @http.request(get_request(CMD_CONNRESET, FAIL, PASS))
254
+ assert_match %r%Renewing connection %, @io.string
255
+ assert_kind_of Net::HTTPResponse, res
256
+ end
257
+
258
+ should 'not retry on bad response from post' do
259
+ post = post_request(CMD_BAD_RESPONSE, FAIL, PASS)
260
+ e = assert_raises PersistentHTTP::Error do
261
+ @http.request(post)
262
+ end
263
+ assert_match %r%too many bad responses%, e.message
264
+ assert_match %r%Removing connection because of too many bad responses%, @io.string
265
+
266
+ res = @http.request(get_request(CMD_SUCCESS))
267
+ assert_kind_of Net::HTTPResponse, res
268
+ end
269
+
270
+ should 'not retry on connection reset from post' do
271
+ post = post_request(CMD_CONNRESET, FAIL, PASS)
272
+ e = assert_raises PersistentHTTP::Error do
273
+ @http.request(post)
274
+ end
275
+ assert_match %r%too many connection resets%, e.message
276
+ assert_match %r%Removing connection %, @io.string
277
+
278
+ res = @http.request(get_request(CMD_SUCCESS))
279
+ assert_kind_of Net::HTTPResponse, res
280
+ end
281
+
282
+ should 'retry on bad response from post when force_retry set' do
283
+ @http.force_retry = true
284
+ post = post_request(CMD_BAD_RESPONSE, FAIL, PASS)
285
+ res = @http.request post
286
+ assert_match %r%Renewing connection because of bad response%, @io.string
287
+ assert_kind_of Net::HTTPResponse, res
288
+ end
289
+
290
+ should 'retry on connection reset from post when force_retry set' do
291
+ @http.force_retry = true
292
+ post = post_request(CMD_CONNRESET, FAIL, PASS)
293
+ res = @http.request post
294
+ assert_match %r%Renewing connection %, @io.string
295
+ assert_kind_of Net::HTTPResponse, res
296
+ end
297
+
298
+ should 'allow post' do
299
+ post = Net::HTTP::Post.new(uri_for CMD_ECHO)
300
+ post.body = 'hello PersistentHTTP'
301
+ res = @http.request(post)
302
+ assert_kind_of Net::HTTPResponse, res
303
+ assert_equal post.body, res.body
304
+ end
305
+
306
+ should 'allow ssl' do
307
+ @http.verify_callback = :callback
308
+ c = Net::HTTP.new('localhost', 80)
309
+
310
+ @http.ssl c
311
+
312
+ assert c.use_ssl?
313
+ assert_equal OpenSSL::SSL::VERIFY_NONE, c.verify_mode
314
+ assert_nil c.verify_callback
315
+ end
316
+
317
+ should 'allow ssl ca_file' do
318
+ @http.ca_file = 'ca_file'
319
+ @http.verify_callback = :callback
320
+ c = Net::HTTP.new('localhost', 80)
321
+
322
+ @http.ssl c
323
+
324
+ assert c.use_ssl?
325
+ assert_equal OpenSSL::SSL::VERIFY_PEER, c.verify_mode
326
+ assert_equal :callback, c.verify_callback
327
+ end
328
+
329
+ should 'allow ssl certificate' do
330
+ @http.certificate = :cert
331
+ @http.private_key = :key
332
+ c = Net::HTTP.new('localhost', 80)
333
+
334
+ @http.ssl c
335
+
336
+ assert c.use_ssl?
337
+ assert_equal :cert, c.cert
338
+ assert_equal :key, c.key
339
+ end
340
+
341
+ should 'allow ssl verify_mode' do
342
+ @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
343
+ c = Net::HTTP.new('localhost', 80)
344
+
345
+ @http.ssl c
346
+
347
+ assert c.use_ssl?
348
+ assert_equal OpenSSL::SSL::VERIFY_NONE, c.verify_mode
349
+ end
350
+ end
351
+
352
+ context 'initialize proxy by env' do
353
+ setup do
354
+ clear_proxy_env
355
+ ENV['HTTP_PROXY'] = 'proxy.example'
356
+ @http = PersistentHTTP.new(:host => 'foobar', :proxy => :ENV)
357
+ end
358
+
359
+ should 'match HTTP_PROXY' do
360
+ assert_equal URI.parse('http://proxy.example'), @http.proxy_uri
361
+ assert_equal 'foobar', @http.host
362
+ end
363
+ end
364
+
365
+ context 'initialize proxy by uri' do
366
+ setup do
367
+ @proxy_uri = URI.parse 'http://proxy.example'
368
+ @proxy_uri.user = 'johndoe'
369
+ @proxy_uri.password = 'muffins'
370
+ @http = PersistentHTTP.new(:url => 'https://zulu.com/foobar', :proxy => @proxy_uri)
371
+ end
372
+
373
+ should 'match proxy_uri and have proxy connection' do
374
+ assert_equal @proxy_uri, @http.proxy_uri
375
+ assert_equal true, @http.use_ssl
376
+ assert_equal 'zulu.com', @http.host
377
+ assert_equal '/foobar', @http.default_path
378
+
379
+ @http.pool.with_connection do |c|
380
+ assert c.started?
381
+ assert c.proxy?
382
+ end
383
+ end
384
+ end
385
+
386
+ context 'initialize proxy by env' do
387
+ setup do
388
+ clear_proxy_env
389
+ ENV['HTTP_PROXY'] = 'proxy.example'
390
+ ENV['HTTP_PROXY_USER'] = 'johndoe'
391
+ ENV['HTTP_PROXY_PASS'] = 'muffins'
392
+ @http = PersistentHTTP.new(:url => 'https://zulu.com/foobar', :proxy => :ENV)
393
+ end
394
+
395
+ should 'create proxy_uri from env' do
396
+ expected = URI.parse 'http://proxy.example'
397
+ expected.user = 'johndoe'
398
+ expected.password = 'muffins'
399
+
400
+ assert_equal expected, @http.proxy_uri
401
+ end
402
+ end
403
+
404
+ context 'initialize proxy by env lower' do
405
+ setup do
406
+ clear_proxy_env
407
+ ENV['http_proxy'] = 'proxy.example'
408
+ ENV['http_proxy_user'] = 'johndoe'
409
+ ENV['http_proxy_pass'] = 'muffins'
410
+ @http = PersistentHTTP.new(:url => 'https://zulu.com/foobar', :proxy => :ENV)
411
+ end
412
+
413
+ should 'create proxy_uri from env' do
414
+ expected = URI.parse 'http://proxy.example'
415
+ expected.user = 'johndoe'
416
+ expected.password = 'muffins'
417
+
418
+ assert_equal expected, @http.proxy_uri
419
+ end
420
+ end
421
+
422
+ context 'with timeouts set' do
423
+ setup do
424
+ @http = PersistentHTTP.new(:url => 'http://example.com')
425
+ @http.open_timeout = 123
426
+ @http.read_timeout = 321
427
+ end
428
+
429
+ should 'have timeouts set' do
430
+ @http.pool.with_connection do |c|
431
+ assert c.started?
432
+ assert !c.proxy?
433
+
434
+ assert_equal 123, c.open_timeout
435
+ assert_equal 321, c.read_timeout
436
+
437
+ assert_equal 'example.com', c.address
438
+ assert_equal 80, c.port
439
+ assert !@http.use_ssl
440
+ end
441
+ end
442
+
443
+ should 'reuse same connection' do
444
+ c1, c2 = nil, nil
445
+ @http.pool.with_connection do |c|
446
+ c1 = c
447
+ assert c.started?
448
+ end
449
+ @http.pool.with_connection do |c|
450
+ c2 = c
451
+ assert c.started?
452
+ end
453
+ assert_same c1,c2
454
+ end
455
+ end
456
+
457
+ context 'with debug_output' do
458
+ setup do
459
+ @io = StringIO.new
460
+ @http = PersistentHTTP.new(:url => 'http://example.com', :debug_output => @io)
461
+ end
462
+
463
+ should 'have debug_output set' do
464
+ @http.pool.with_connection do |c|
465
+ assert c.started?
466
+ assert_equal @io, c.instance_variable_get(:@debug_output)
467
+ assert_equal 'example.com', c.address
468
+ assert_equal 80, c.port
469
+ end
470
+ end
471
+ end
472
+
473
+ context 'with host down' do
474
+ setup do
475
+ @http = PersistentHTTP.new(:url => 'http://example.com', :open_timeout => DUMMY_OPEN_TIMEOUT_FOR_HOSTDOWN)
476
+ end
477
+
478
+ should 'assert error' do
479
+ e = assert_raises PersistentHTTP::Error do
480
+ @http.request(get_request(CMD_SUCCESS))
481
+ end
482
+ assert_match %r%host down%, e.message
483
+ end
484
+ end
485
+
486
+ context 'with connection refused' do
487
+ setup do
488
+ @http = PersistentHTTP.new(:url => 'http://example.com', :open_timeout => DUMMY_OPEN_TIMEOUT_FOR_CONNREFUSED)
489
+ end
490
+
491
+ should 'assert error' do
492
+ e = assert_raises PersistentHTTP::Error do
493
+ @http.request(get_request(CMD_SUCCESS))
494
+ end
495
+ assert_match %r%connection refused%, e.message
496
+ end
497
+ end
498
+
499
+ context 'with pool size of 3' do
500
+ setup do
501
+ @http = PersistentHTTP.new(:url => 'http://example.com', :pool_size => 3)
502
+ end
503
+
504
+ should 'only allow 3 connections checked out at a time' do
505
+ @http.request(get_request(CMD_SUCCESS))
506
+ pool = @http.pool
507
+ 2.times do
508
+ conns = []
509
+ pool.with_connection do |c1|
510
+ pool.with_connection do |c2|
511
+ conns << c2
512
+ pool.with_connection do |c3|
513
+ conns << c3
514
+ begin
515
+ Timeout.timeout(2) do
516
+ pool.with_connection { |c4| }
517
+ assert false, 'should NOT have been able to get 4th connection'
518
+ end
519
+ rescue Timeout::Error => e
520
+ # successfully failed to get a connection
521
+ end
522
+ @http.remove(c1)
523
+ Timeout.timeout(1) do
524
+ begin
525
+ pool.with_connection do |c4|
526
+ conns << c4
527
+ end
528
+ rescue Timeout::Error => e
529
+ assert false, 'should have been able to get 4th connection'
530
+ end
531
+ end
532
+ end
533
+ end
534
+ end
535
+ pool.with_connection do |c1|
536
+ pool.with_connection do |c2|
537
+ pool.with_connection do |c3|
538
+ assert_equal conns, [c1,c2,c3]
539
+ end
540
+ end
541
+ end
542
+ # Do it a 2nd time with finish returning an IOError
543
+ c1 = conns[0]
544
+ def c1.finish
545
+ super
546
+ raise IOError
547
+ end
548
+ end
549
+ end
550
+
551
+ should 'handle renew' do
552
+ @http.request(get_request(CMD_SUCCESS))
553
+ pool = @http.pool
554
+ 2.times do
555
+ conns = []
556
+ pool.with_connection do |c1|
557
+ pool.with_connection do |c2|
558
+ conns << c2
559
+ pool.with_connection do |c3|
560
+ conns << c3
561
+ new_c1 = @http.renew(c1)
562
+ assert c1 != new_c1
563
+ conns.unshift(new_c1)
564
+ end
565
+ end
566
+ end
567
+ pool.with_connection do |c1|
568
+ pool.with_connection do |c2|
569
+ pool.with_connection do |c3|
570
+ assert_equal conns, [c1,c2,c3]
571
+ end
572
+ end
573
+ end
574
+ # Do it a 2nd time with finish returning an IOError
575
+ c1 = conns[0]
576
+ def c1.finish
577
+ super
578
+ raise IOError
579
+ end
580
+ end
581
+ end
582
+
583
+ should 'handle renew with exception' do
584
+ pool = @http.pool
585
+ [[DUMMY_OPEN_TIMEOUT_FOR_HOSTDOWN, %r%host down%], [DUMMY_OPEN_TIMEOUT_FOR_CONNREFUSED, %r%connection refused%]].each do |pair|
586
+ dummy_open_timeout = pair.first
587
+ error_message = pair.last
588
+ pool.with_connection do |c|
589
+ old_c = c
590
+ @http.open_timeout = dummy_open_timeout
591
+ e = assert_raises PersistentHTTP::Error do
592
+ new_c = @http.renew c
593
+ end
594
+ assert_match error_message, e.message
595
+
596
+ # Make sure our pool is still in good shape
597
+ @http.open_timeout = 5 # Any valid timeout will do
598
+ pool.with_connection do |c1|
599
+ assert old_c != c1
600
+ pool.with_connection do |c2|
601
+ assert old_c != c2
602
+ end
603
+ end
604
+ end
605
+ end
606
+ end
607
+ end
608
+ #
609
+ # # def test_shutdown
610
+ # # c = connection
611
+ # # cs = conns
612
+ # # rs = reqs
613
+ # #
614
+ # # orig = @http
615
+ # # @http = PersistentHTTP.new 'name'
616
+ # # c2 = connection
617
+ # #
618
+ # # orig.shutdown
619
+ # #
620
+ # # assert c.finished?
621
+ # # refute c2.finished?
622
+ # #
623
+ # # refute_same cs, conns
624
+ # # refute_same rs, reqs
625
+ # # end
626
+ # #
627
+ # # def test_shutdown_not_started
628
+ # # c = Object.new
629
+ # # def c.finish() raise IOError end
630
+ # #
631
+ # # conns["#{@uri.host}:#{@uri.port}"] = c
632
+ # #
633
+ # # @http.shutdown
634
+ # #
635
+ # # assert_nil Thread.current[@http.connection_key]
636
+ # # assert_nil Thread.current[@http.request_key]
637
+ # # end
638
+ # #
639
+ # # def test_shutdown_no_connections
640
+ # # @http.shutdown
641
+ # #
642
+ # # assert_nil Thread.current[@http.connection_key]
643
+ # # assert_nil Thread.current[@http.request_key]
644
+ # # end
645
+ #
646
+ end
647
+
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: persistent_http
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 0
8
+ - 0
9
+ version: 1.0.0
10
+ platform: ruby
11
+ authors:
12
+ - Brad Pardee
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-10-03 00:00:00 -04:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: Persistent HTTP connections using a connection pool
22
+ email: bradpardee@gmail.com
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files:
28
+ - LICENSE
29
+ - README.rdoc
30
+ files:
31
+ - .gitignore
32
+ - History.txt
33
+ - LICENSE
34
+ - README.rdoc
35
+ - Rakefile
36
+ - VERSION
37
+ - lib/persistent_http.rb
38
+ - lib/persistent_http/faster.rb
39
+ - persistent_http.gemspec
40
+ - test/persistent_http_test.rb
41
+ has_rdoc: true
42
+ homepage: http://github.com/bpardee/persistent_http
43
+ licenses: []
44
+
45
+ post_install_message:
46
+ rdoc_options:
47
+ - --charset=UTF-8
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ segments:
55
+ - 0
56
+ version: "0"
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ segments:
62
+ - 0
63
+ version: "0"
64
+ requirements: []
65
+
66
+ rubyforge_project:
67
+ rubygems_version: 1.3.6
68
+ signing_key:
69
+ specification_version: 3
70
+ summary: Persistent HTTP connections using a connection pool
71
+ test_files:
72
+ - test/persistent_http_test.rb