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 +4 -0
- data/History.txt +4 -0
- data/LICENSE +20 -0
- data/README.rdoc +58 -0
- data/Rakefile +16 -0
- data/VERSION +1 -0
- data/lib/persistent_http/faster.rb +27 -0
- data/lib/persistent_http.rb +444 -0
- data/persistent_http.gemspec +50 -0
- data/test/persistent_http_test.rb +647 -0
- metadata +72 -0
data/.gitignore
ADDED
data/History.txt
ADDED
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
|