ajmurmann_right_http_connection 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,62 @@
1
+ == 0.0.1 2007-05-15
2
+ * 1 major enhancement:
3
+ * Initial release
4
+
5
+ == 0.1.2 2007-06-27
6
+
7
+ * No major changes.
8
+
9
+ == 0.1.3 2007-07-09
10
+
11
+ * No change.
12
+
13
+ == 0.1.4 2007-08-10
14
+
15
+ * r1442, todd, 2007-08-07 15:45:24
16
+ * # 373, Add support in right_http_connection for bailing out to a block while
17
+ reading the HTTP response (to support GET streaming...)
18
+
19
+ * r1411, todd, 2007-08-03 15:14:45
20
+ * # 373, Stream uploads (PUTs) if the source is a file, stream, or anything
21
+ read()-able
22
+
23
+ == 1.1.0 2007-08-15
24
+ Initial public release
25
+
26
+ == 1.2.0 2007-10-05
27
+
28
+ * r1867, konstantin, 2007-10-05 06:19:45
29
+ * # 220, (re)open connection to server if none exists or connection params
30
+ have changed
31
+
32
+ == 1.2.1
33
+
34
+ * r2648, konstantin, 01-24-08 11:12:00
35
+ * net_fix.rb moved from right_aws gem to fix the problem with uploading the streamable
36
+ objects to S3
37
+
38
+ * r2764, konstantin, 02-08-08 00:05:00 +03:00
39
+ * "RightAws: incompatible Net::HTTP monkey-patch" exception is raised if our net_fix
40
+ patch was overriden (by attachment_fu for example, to avoid this load attachment_fu
41
+ before loading the right_http_connection gem).
42
+
43
+ == 1.2.2
44
+
45
+ * r3524, konstantin, 2008-04-17 11:35:42 +0400
46
+ * Fixed a problem with incorrect error handling (connection retries always failed).
47
+
48
+ == 1.2.3
49
+
50
+ - Added support for setting retry & timeout parameters in the constructor
51
+ - Improve handling of data streams during upload: if there is a failure and a retry, reset
52
+ the seek pointer for the subsequent re-request
53
+
54
+ == 1.2.4
55
+
56
+ * r4984, konstantin, 2008-08-11 14:49:18 +0400
57
+ * fixed a bug: <NoMethodError: You have a nil object when you didn't expect it!
58
+ The error occurred while evaluating nil.body_stream>
59
+
60
+ == 1.2.5 (not released yet)
61
+
62
+ - ActiveSupport dependency removal
data/Manifest.txt ADDED
@@ -0,0 +1,8 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ lib/net_fix.rb
6
+ lib/right_http_connection.rb
7
+ lib/support.rb
8
+ setup.rb
data/README.txt ADDED
@@ -0,0 +1,66 @@
1
+ ABOUT THIS GEM
2
+ ===============
3
+
4
+ RightScale hasn't pushed a new gem for neither right_aws nor right_http_connection since April 2010 and February 2009.
5
+ However, there aresome important fixes the HEAD available on Github. I need these fixes and don't want to point my gemfile at a
6
+ Github repo that can change on me. So I just build and push the gem myself.
7
+
8
+ ===============
9
+
10
+ README FROM ORIGINAL REPO
11
+ ==========================
12
+
13
+ RightScale::HttpConnection
14
+ by RightScale, Inc.
15
+ www.RightScale.com
16
+
17
+ == DESCRIPTION:
18
+
19
+ Rightscale::HttpConnection is a robust HTTP/S library. It implements a retry
20
+ algorithm for low-level network errors.
21
+
22
+ == FEATURES:
23
+
24
+ - provides put/get streaming
25
+ - does configurable retries on connect and read timeouts, DNS failures, etc.
26
+ - HTTPS certificate checking
27
+
28
+ == SYNOPSIS:
29
+
30
+
31
+ == REQUIREMENTS:
32
+
33
+ - 2/11/08: If you use RightScale::HttpConnection in conjunction with attachment_fu, the
34
+ HttpConnection gem must be included (using the require statement) AFTER
35
+ attachment_fu.
36
+ This is due to a conflict between the HttpConnection gem and another
37
+ gem required by attachment_fu.
38
+
39
+
40
+
41
+ == INSTALL:
42
+
43
+ sudo gem install right_http_connection
44
+
45
+ == LICENSE:
46
+
47
+ Copyright (c) 2007-2008 RightScale, Inc.
48
+
49
+ Permission is hereby granted, free of charge, to any person obtaining
50
+ a copy of this software and associated documentation files (the
51
+ 'Software'), to deal in the Software without restriction, including
52
+ without limitation the rights to use, copy, modify, merge, publish,
53
+ distribute, sublicense, and/or sell copies of the Software, and to
54
+ permit persons to whom the Software is furnished to do so, subject to
55
+ the following conditions:
56
+
57
+ The above copyright notice and this permission notice shall be
58
+ included in all copies or substantial portions of the Software.
59
+
60
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
61
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
62
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
63
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
64
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
65
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
66
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,89 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler::GemHelper.install_tasks
4
+ require 'rake'
5
+ require 'rake/clean'
6
+ require 'rake/testtask'
7
+ require 'rake/packagetask'
8
+ require 'rake/gempackagetask'
9
+ require 'rake/rdoctask'
10
+ require 'rake/contrib/rubyforgepublisher'
11
+ require 'rspec/core/rake_task'
12
+ require 'cucumber/rake/task'
13
+ require 'fileutils'
14
+ include FileUtils
15
+ require File.join(File.dirname(__FILE__), 'lib', 'right_http_connection')
16
+
17
+ Bundler::GemHelper.install_tasks
18
+
19
+ # == Gem == #
20
+
21
+ gemtask = Rake::GemPackageTask.new(Gem::Specification.load("right_http_connection.gemspec")) do |package|
22
+ package.package_dir = ENV['PACKAGE_DIR'] || 'pkg'
23
+ package.need_zip = true
24
+ package.need_tar = true
25
+ end
26
+
27
+ directory gemtask.package_dir
28
+
29
+ CLEAN.include(gemtask.package_dir)
30
+
31
+ desc 'Generate website files'
32
+ task :website_generate do
33
+ Dir['website/**/*.txt'].each do |txt|
34
+ sh %{ ruby scripts/txt2html #{txt} > #{txt.gsub(/txt$/,'html')} }
35
+ end
36
+ end
37
+
38
+ desc 'Upload website files to rubyforge'
39
+ task :website_upload do
40
+ config = YAML.load(File.read(File.expand_path("~/.rubyforge/user-config.yml")))
41
+ host = "#{config["username"]}@rubyforge.org"
42
+ remote_dir = "/var/www/gforge-projects/#{RUBYFORGE_PROJECT}/"
43
+ # remote_dir = "/var/www/gforge-projects/#{RUBYFORGE_PROJECT}/#{GEM_NAME}"
44
+ local_dir = 'website'
45
+ sh %{rsync -av #{local_dir}/ #{host}:#{remote_dir}}
46
+ end
47
+
48
+ desc 'Generate and upload website files'
49
+ task :website => [:website_generate, :website_upload]
50
+
51
+ desc 'Release the website and new gem version'
52
+ task :deploy => [:check_version, :website, :release]
53
+
54
+ task :check_version do
55
+ unless ENV['VERSION']
56
+ puts 'Must pass a VERSION=x.y.z release version'
57
+ exit
58
+ end
59
+ unless ENV['VERSION'] == VERS
60
+ puts "Please update your version.rb to match the release version, currently #{VERS}"
61
+ exit
62
+ end
63
+ end
64
+
65
+ task :default => 'spec'
66
+
67
+ # == Unit Tests == #
68
+
69
+ desc "Run unit tests"
70
+ RSpec::Core::RakeTask.new
71
+
72
+ namespace :spec do
73
+ desc "Run unit tests with RCov"
74
+ RSpec::Core::RakeTask.new(:rcov) do |t|
75
+ t.rcov = true
76
+ t.rcov_opts = %q[--exclude "spec"]
77
+ end
78
+
79
+ desc "Print Specdoc for unit tests"
80
+ RSpec::Core::RakeTask.new(:doc) do |t|
81
+ t.rspec_opts = ["--format", "documentation"]
82
+ end
83
+ end
84
+
85
+ # == Functional tests == #
86
+ desc "Run functional tests"
87
+ Cucumber::Rake::Task.new do |t|
88
+ t.cucumber_opts = %w{--color --format pretty}
89
+ end
data/lib/net_fix.rb ADDED
@@ -0,0 +1,160 @@
1
+ #
2
+ # Copyright (c) 2008 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #
23
+ #
24
+
25
+ # Net::HTTP and Net::HTTPGenericRequest fixes to support 100-continue on
26
+ # POST and PUT. The request must have 'expect' field set to '100-continue'.
27
+
28
+
29
+ module Net
30
+
31
+ class BufferedIO #:nodoc:
32
+ # Monkey-patch Net::BufferedIO to read > 1024 bytes from the socket at a time
33
+
34
+ # Default size (in bytes) of the max read from a socket into the user space read buffers for socket IO
35
+ DEFAULT_SOCKET_READ_SIZE = 16*1024
36
+
37
+ @@socket_read_size = DEFAULT_SOCKET_READ_SIZE
38
+
39
+ def self.socket_read_size=(readsize)
40
+ if(readsize <= 0)
41
+ return
42
+ end
43
+ @@socket_read_size = readsize
44
+ end
45
+
46
+ def self.socket_read_size?()
47
+ @@socket_read_size
48
+ end
49
+
50
+ def rbuf_fill
51
+ timeout(@read_timeout) {
52
+ @rbuf << @io.sysread(@@socket_read_size)
53
+ }
54
+ end
55
+ end
56
+
57
+
58
+ #-- Net::HTTPGenericRequest --
59
+
60
+ class HTTPGenericRequest
61
+ # Monkey-patch Net::HTTPGenericRequest to read > 1024 bytes from the local data
62
+ # source at a time (used in streaming PUTs)
63
+
64
+ # Default size (in bytes) of the max read from a local source (File, String,
65
+ # etc.) to the user space write buffers for socket IO.
66
+ DEFAULT_LOCAL_READ_SIZE = 16*1024
67
+
68
+ @@local_read_size = DEFAULT_LOCAL_READ_SIZE
69
+
70
+ def self.local_read_size=(readsize)
71
+ if(readsize <= 0)
72
+ return
73
+ end
74
+ @@local_read_size = readsize
75
+ end
76
+
77
+ def self.local_read_size?()
78
+ @@local_read_size
79
+ end
80
+
81
+ def exec(sock, ver, path, send_only=nil) #:nodoc: internal use only
82
+ if @body
83
+ send_request_with_body sock, ver, path, @body, send_only
84
+ elsif @body_stream
85
+ send_request_with_body_stream sock, ver, path, @body_stream, send_only
86
+ else
87
+ write_header(sock, ver, path)
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ def send_request_with_body(sock, ver, path, body, send_only=nil)
94
+ self.content_length = body.length
95
+ delete 'Transfer-Encoding'
96
+ supply_default_content_type
97
+ write_header(sock, ver, path) unless send_only == :body
98
+ sock.write(body) unless send_only == :header
99
+ end
100
+
101
+ def send_request_with_body_stream(sock, ver, path, f, send_only=nil)
102
+ unless content_length() or chunked?
103
+ raise ArgumentError,
104
+ "Content-Length not given and Transfer-Encoding is not `chunked'"
105
+ end
106
+ supply_default_content_type
107
+ write_header(sock, ver, path) unless send_only == :body
108
+ unless send_only == :header
109
+ if chunked?
110
+ while s = f.read(@@local_read_size)
111
+ sock.write(sprintf("%x\r\n", s.length) << s << "\r\n")
112
+ end
113
+ sock.write "0\r\n\r\n"
114
+ else
115
+ while s = f.read(@@local_read_size)
116
+ sock.write s
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+
124
+ #-- Net::HTTP --
125
+
126
+ class HTTP
127
+ def request(req, body = nil, &block) # :yield: +response+
128
+ unless started?
129
+ start {
130
+ req['connection'] ||= 'close'
131
+ return request(req, body, &block)
132
+ }
133
+ end
134
+ if proxy_user()
135
+ unless use_ssl?
136
+ req.proxy_basic_auth proxy_user(), proxy_pass()
137
+ end
138
+ end
139
+ # set body
140
+ req.set_body_internal body
141
+ begin_transport req
142
+ # if we expect 100-continue then send a header first
143
+ send_only = ((req.is_a?(Post)||req.is_a?(Put)) && (req['expect']=='100-continue')) ? :header : nil
144
+ req.exec @socket, @curr_http_version, edit_path(req.path), send_only
145
+ begin
146
+ res = HTTPResponse.read_new(@socket)
147
+ # if we expected 100-continue then send a body
148
+ if res.is_a?(HTTPContinue) && send_only && req['content-length'].to_i > 0
149
+ req.exec @socket, @curr_http_version, edit_path(req.path), :body
150
+ end
151
+ end while res.kind_of?(HTTPContinue)
152
+ res.reading_body(@socket, req.response_body_permitted?) {
153
+ yield res if block_given?
154
+ }
155
+ end_transport req, res
156
+ res
157
+ end
158
+ end
159
+
160
+ end
@@ -0,0 +1,479 @@
1
+ #
2
+ # Copyright (c) 2007-2008 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #
23
+
24
+ require "net/https"
25
+ require "uri"
26
+ require "time"
27
+ require "logger"
28
+
29
+ $:.unshift(File.dirname(__FILE__))
30
+ require 'version'
31
+ require 'support'
32
+ require "net_fix"
33
+
34
+ module Rightscale
35
+
36
+ =begin rdoc
37
+ HttpConnection maintains a persistent HTTP connection to a remote
38
+ server. Each instance maintains its own unique connection to the
39
+ HTTP server. HttpConnection makes a best effort to receive a proper
40
+ HTTP response from the server, although it does not guarantee that
41
+ this response contains a HTTP Success code.
42
+
43
+ On low-level errors (TCP/IP errors) HttpConnection invokes a reconnect
44
+ and retry algorithm. Note that although each HttpConnection object
45
+ has its own connection to the HTTP server, error handling is shared
46
+ across all connections to a server. For example, if there are three
47
+ connections to www.somehttpserver.com, a timeout error on one of those
48
+ connections will cause all three connections to break and reconnect.
49
+ A connection will not break and reconnect, however, unless a request
50
+ becomes active on it within a certain amount of time after the error
51
+ (as specified by HTTP_CONNECTION_RETRY_DELAY). An idle connection will not
52
+ break even if other connections to the same server experience errors.
53
+
54
+ A HttpConnection will retry a request a certain number of times (as
55
+ defined by HTTP_CONNNECTION_RETRY_COUNT). If all the retries fail,
56
+ an exception is thrown and all HttpConnections associated with a
57
+ server enter a probationary period defined by HTTP_CONNECTION_RETRY_DELAY.
58
+ If the user makes a new request subsequent to entering probation,
59
+ the request will fail immediately with the same exception thrown
60
+ on probation entry. This is so that if the HTTP server has gone
61
+ down, not every subsequent request must wait for a connect timeout
62
+ before failing. After the probation period expires, the internal
63
+ state of the HttpConnection is reset and subsequent requests have
64
+ the full number of potential reconnects and retries available to
65
+ them.
66
+ =end
67
+
68
+ class HttpConnection
69
+
70
+ # Number of times to retry the request after encountering the first error
71
+ HTTP_CONNECTION_RETRY_COUNT = 3 unless defined?(HTTP_CONNECTION_RETRY_COUNT)
72
+ # Throw a Timeout::Error if a connection isn't established within this number of seconds
73
+ HTTP_CONNECTION_OPEN_TIMEOUT = 5 unless defined?(HTTP_CONNECTION_OPEN_TIMEOUT)
74
+ # Throw a Timeout::Error if no data have been read on this connnection within this number of seconds
75
+ HTTP_CONNECTION_READ_TIMEOUT = 120 unless defined?(HTTP_CONNECTION_READ_TIMEOUT)
76
+ # Length of the post-error probationary period during which all requests will fail
77
+ HTTP_CONNECTION_RETRY_DELAY = 15 unless defined?(HTTP_CONNECTION_RETRY_DELAY)
78
+
79
+ #--------------------
80
+ # class methods
81
+ #--------------------
82
+ #
83
+ @@params = {}
84
+ @@params[:http_connection_retry_count] = HTTP_CONNECTION_RETRY_COUNT
85
+ @@params[:http_connection_open_timeout] = HTTP_CONNECTION_OPEN_TIMEOUT
86
+ @@params[:http_connection_read_timeout] = HTTP_CONNECTION_READ_TIMEOUT
87
+ @@params[:http_connection_retry_delay] = HTTP_CONNECTION_RETRY_DELAY
88
+
89
+ # Query the global (class-level) parameters:
90
+ #
91
+ # :user_agent => 'www.HostName.com' # String to report as HTTP User agent
92
+ # :ca_file => 'path_to_file' # Path to a CA certification file in PEM format. The file can contain several CA certificates. If this parameter isn't set, HTTPS certs won't be verified.
93
+ # :fail_if_ca_mismatch => Boolean # If ca_file is set and the server certificate doesn't verify, a log line is generated regardless, but normally right_http_connection continues on past the failure. If this is set, fail to connect in that case. Defaults to false.
94
+ # :logger => Logger object # If omitted, HttpConnection logs to STDOUT
95
+ # :exception => Exception to raise # The type of exception to raise
96
+ # # if a request repeatedly fails. RuntimeError is raised if this parameter is omitted.
97
+ # :proxy_host => 'hostname' # hostname of HTTP proxy host to use, default none.
98
+ # :proxy_port => port # port of HTTP proxy host to use, default none.
99
+ # :proxy_username => 'username' # username to use for proxy authentication, default none.
100
+ # :proxy_password => 'password' # password to use for proxy authentication, default none.
101
+ # :http_connection_retry_count # by default == Rightscale::HttpConnection::HTTP_CONNECTION_RETRY_COUNT
102
+ # :http_connection_open_timeout # by default == Rightscale::HttpConnection::HTTP_CONNECTION_OPEN_TIMEOUT
103
+ # :http_connection_read_timeout # by default == Rightscale::HttpConnection::HTTP_CONNECTION_READ_TIMEOUT
104
+ # :http_connection_retry_delay # by default == Rightscale::HttpConnection::HTTP_CONNECTION_RETRY_DELAY
105
+ # :raise_on_timeout # do not perform a retry if timeout is received (false by default)
106
+ def self.params
107
+ @@params
108
+ end
109
+
110
+ # Set the global (class-level) parameters
111
+ def self.params=(params)
112
+ @@params = params
113
+ end
114
+
115
+ #------------------
116
+ # instance methods
117
+ #------------------
118
+ attr_accessor :http
119
+ attr_accessor :server
120
+ attr_accessor :params # see @@params
121
+ attr_accessor :logger
122
+
123
+ # Params hash:
124
+ # :user_agent => 'www.HostName.com' # String to report as HTTP User agent
125
+ # :ca_file => 'path_to_file' # A path of a CA certification file in PEM format. The file can contain several CA certificates.
126
+ # :fail_if_ca_mismatch => Boolean # If ca_file is set and the server certificate doesn't verify, a log line is generated regardless, but normally right_http_connection continues on past the failure. If this is set, fail to connect in that case. Defaults to false.
127
+ # :logger => Logger object # If omitted, HttpConnection logs to STDOUT
128
+ # :exception => Exception to raise # The type of exception to raise if a request repeatedly fails. RuntimeError is raised if this parameter is omitted.
129
+ # :proxy_host => 'hostname' # hostname of HTTP proxy host to use, default none.
130
+ # :proxy_port => port # port of HTTP proxy host to use, default none.
131
+ # :proxy_username => 'username' # username to use for proxy authentication, default none.
132
+ # :proxy_password => 'password' # password to use for proxy authentication, default none.
133
+ # :http_connection_retry_count # by default == Rightscale::HttpConnection.params[:http_connection_retry_count]
134
+ # :http_connection_open_timeout # by default == Rightscale::HttpConnection.params[:http_connection_open_timeout]
135
+ # :http_connection_read_timeout # by default == Rightscale::HttpConnection.params[:http_connection_read_timeout]
136
+ # :http_connection_retry_delay # by default == Rightscale::HttpConnection.params[:http_connection_retry_delay]
137
+ # :raise_on_timeout # do not perform a retry if timeout is received (false by default)
138
+ def initialize(params={})
139
+ @params = params
140
+ @params[:http_connection_retry_count] ||= @@params[:http_connection_retry_count]
141
+ @params[:http_connection_open_timeout] ||= @@params[:http_connection_open_timeout]
142
+ @params[:http_connection_read_timeout] ||= @@params[:http_connection_read_timeout]
143
+ @params[:http_connection_retry_delay] ||= @@params[:http_connection_retry_delay]
144
+ @params[:proxy_host] ||= @@params[:proxy_host]
145
+ @params[:proxy_port] ||= @@params[:proxy_port]
146
+ @params[:proxy_username] ||= @@params[:proxy_username]
147
+ @params[:proxy_password] ||= @@params[:proxy_password]
148
+ @http = nil
149
+ @server = nil
150
+ @logger = get_param(:logger) ||
151
+ (RAILS_DEFAULT_LOGGER if defined?(RAILS_DEFAULT_LOGGER)) ||
152
+ Logger.new(STDOUT)
153
+ #--------------
154
+ # Retry state - Keep track of errors on a per-server basis
155
+ #--------------
156
+ @state = {} # retry state indexed by server: consecutive error count, error time, and error
157
+ @eof = {}
158
+ end
159
+
160
+ def get_param(name, custom_options={})
161
+ custom_options [name] || @params[name] || @@params[name]
162
+ end
163
+
164
+ # Query for the maximum size (in bytes) of a single read from the underlying
165
+ # socket. For bulk transfer, especially over fast links, this is value is
166
+ # critical to performance.
167
+ def socket_read_size?
168
+ Net::BufferedIO.socket_read_size?
169
+ end
170
+
171
+ # Set the maximum size (in bytes) of a single read from the underlying
172
+ # socket. For bulk transfer, especially over fast links, this is value is
173
+ # critical to performance.
174
+ def socket_read_size=(newsize)
175
+ Net::BufferedIO.socket_read_size=(newsize)
176
+ end
177
+
178
+ # Query for the maximum size (in bytes) of a single read from local data
179
+ # sources like files. This is important, for example, in a streaming PUT of a
180
+ # large buffer.
181
+ def local_read_size?
182
+ Net::HTTPGenericRequest.local_read_size?
183
+ end
184
+
185
+ # Set the maximum size (in bytes) of a single read from local data
186
+ # sources like files. This can be used to tune the performance of, for example, a streaming PUT of a
187
+ # large buffer.
188
+ def local_read_size=(newsize)
189
+ Net::HTTPGenericRequest.local_read_size=(newsize)
190
+ end
191
+
192
+ private
193
+
194
+ # number of consecutive errors seen for server, 0 all is ok
195
+ def error_count
196
+ @state[@server] ? @state[@server][:count] : 0
197
+ end
198
+
199
+ # time of last error for server, nil if all is ok
200
+ def error_time
201
+ @state[@server] && @state[@server][:time]
202
+ end
203
+
204
+ # message for last error for server, "" if all is ok
205
+ def error_message
206
+ @state[@server] ? @state[@server][:message] : ""
207
+ end
208
+
209
+ # add an error for a server
210
+ def error_add(message)
211
+ @state[@server] = { :count => error_count+1, :time => Time.now, :message => message }
212
+ end
213
+
214
+ # reset the error state for a server (i.e. a request succeeded)
215
+ def error_reset
216
+ @state.delete(@server)
217
+ end
218
+
219
+ # Error message stuff...
220
+ def banana_message
221
+ return "#{@server} temporarily unavailable: (#{error_message})"
222
+ end
223
+
224
+ def err_header
225
+ return "#{self.class.name} :"
226
+ end
227
+
228
+ # Adds new EOF timestamp.
229
+ # Returns the number of seconds to wait before new conection retry:
230
+ # 0.5, 1, 2, 4, 8
231
+ def add_eof
232
+ (@eof[@server] ||= []).unshift Time.now
233
+ 0.25 * 2 ** @eof[@server].size
234
+ end
235
+
236
+ # Returns first EOF timestamp or nul if have no EOFs being tracked.
237
+ def eof_time
238
+ @eof[@server] && @eof[@server].last
239
+ end
240
+
241
+ # Returns true if we are receiving EOFs during last @params[:http_connection_retry_delay] seconds
242
+ # and there were no successful response from server
243
+ def raise_on_eof_exception?
244
+ @eof[@server].nil? ? false : ( (Time.now.to_i-@params[:http_connection_retry_delay]) > @eof[@server].last.to_i )
245
+ end
246
+
247
+ # Reset a list of EOFs for this server.
248
+ # This is being called when we have got an successful response from server.
249
+ def eof_reset
250
+ @eof.delete(@server)
251
+ end
252
+
253
+ # Detects if an object is 'streamable' - can we read from it, and can we know the size?
254
+ def setup_streaming(request)
255
+ if(request.body && request.body.respond_to?(:read))
256
+ body = request.body
257
+ request.content_length = body.respond_to?(:lstat) ? body.lstat.size : body.size
258
+ request.body_stream = request.body
259
+ true
260
+ end
261
+ end
262
+
263
+ def get_fileptr_offset(request_params)
264
+ request_params[:request].body.pos
265
+ rescue Exception => e
266
+ # Probably caught this because the body doesn't support the pos() method, like if it is a socket.
267
+ # Just return 0 and get on with life.
268
+ 0
269
+ end
270
+
271
+ def reset_fileptr_offset(request, offset = 0)
272
+ if(request.body_stream && request.body_stream.respond_to?(:pos))
273
+ begin
274
+ request.body_stream.pos = offset
275
+ rescue Exception => e
276
+ @logger.warn("Failed file pointer reset; aborting HTTP retries." +
277
+ " -- #{err_header} #{e.inspect}")
278
+ raise e
279
+ end
280
+ end
281
+ end
282
+
283
+ # Start a fresh connection. The object closes any existing connection and
284
+ # opens a new one.
285
+ def start(request_params)
286
+ # close the previous if exists
287
+ finish
288
+ # create new connection
289
+ @server = request_params[:server]
290
+ @port = request_params[:port]
291
+ @protocol = request_params[:protocol]
292
+ @proxy_host = request_params[:proxy_host]
293
+ @proxy_port = request_params[:proxy_port]
294
+ @proxy_username = request_params[:proxy_username]
295
+ @proxy_password = request_params[:proxy_password]
296
+
297
+ @logger.info("Opening new #{@protocol.upcase} connection to #@server:#@port")
298
+ @logger.info("Connecting to proxy #{@proxy_host}:#{@proxy_port} with username" +
299
+ " #{@proxy_username}") unless @proxy_host.nil?
300
+
301
+ @http = Net::HTTP.new(@server, @port, @proxy_host, @proxy_port, @proxy_username,
302
+ @proxy_password)
303
+ @http.open_timeout = get_param(:http_connection_open_timeout, request_params)
304
+ @http.read_timeout = get_param(:http_connection_read_timeout, request_params)
305
+
306
+ if @protocol == 'https'
307
+ verifyCallbackProc = Proc.new{ |ok, x509_store_ctx|
308
+ code = x509_store_ctx.error
309
+ msg = x509_store_ctx.error_string
310
+ #debugger
311
+ @logger.warn("##### #{@server} certificate verify failed: #{msg}") unless code == 0
312
+ if request_params[:fail_if_ca_mismatch] && code != 0
313
+ false
314
+ else
315
+ true
316
+ end
317
+ }
318
+ @http.use_ssl = true
319
+ ca_file = get_param(:ca_file)
320
+ if ca_file
321
+ @http.verify_mode = OpenSSL::SSL::VERIFY_PEER
322
+ @http.verify_callback = verifyCallbackProc
323
+ @http.ca_file = ca_file
324
+ else
325
+ @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
326
+ end
327
+ end
328
+ # open connection
329
+ @http.start
330
+ end
331
+
332
+ public
333
+
334
+ =begin rdoc
335
+ Send HTTP request to server
336
+
337
+ request_params hash:
338
+ :server => 'www.HostName.com' # Hostname or IP address of HTTP server
339
+ :port => '80' # Port of HTTP server
340
+ :protocol => 'https' # http and https are supported on any port
341
+ :request => 'requeststring' # Fully-formed HTTP request to make
342
+ :proxy_host => 'hostname' # hostname of HTTP proxy host to use, default none.
343
+ :proxy_port => port # port of HTTP proxy host to use, default none.
344
+ :proxy_username => 'username' # username to use for proxy authentication, default none.
345
+ :proxy_password => 'password' # password to use for proxy authentication, default none.
346
+
347
+ :raise_on_timeout # do not perform a retry if timeout is received (false by default)
348
+ :http_connection_retry_count
349
+ :http_connection_open_timeout
350
+ :http_connection_read_timeout
351
+ :http_connection_retry_delay
352
+ :user_agent
353
+ :exception
354
+
355
+ Raises RuntimeError, Interrupt, and params[:exception] (if specified in new).
356
+
357
+ =end
358
+ def request(request_params, &block)
359
+ current_params = @params.merge(request_params)
360
+ exception = get_param(:exception, current_params) || RuntimeError
361
+
362
+ # We save the offset here so that if we need to retry, we can return the file pointer to its initial position
363
+ mypos = get_fileptr_offset(current_params)
364
+ loop do
365
+ current_params[:protocol] ||= (current_params[:port] == 443 ? 'https' : 'http')
366
+ # (re)open connection to server if none exists or params has changed
367
+ same_server_as_before = @server == current_params[:server] &&
368
+ @port == current_params[:port] &&
369
+ @protocol == current_params[:protocol]
370
+
371
+ # if we are inside a delay between retries: no requests this time!
372
+ # (skip this step if the endpoint has changed)
373
+ if error_count > current_params[:http_connection_retry_count] &&
374
+ error_time + current_params[:http_connection_retry_delay] > Time.now &&
375
+ same_server_as_before
376
+
377
+ # store the message (otherwise it will be lost after error_reset and
378
+ # we will raise an exception with an empty text)
379
+ banana_message_text = banana_message
380
+ @logger.warn("#{err_header} re-raising same error: #{banana_message_text} " +
381
+ "-- error count: #{error_count}, error age: #{Time.now.to_i - error_time.to_i}")
382
+ raise exception.new(banana_message_text)
383
+ end
384
+
385
+ # try to connect server(if connection does not exist) and get response data
386
+ begin
387
+ request = current_params[:request]
388
+ request['User-Agent'] = get_param(:user_agent, current_params) || ''
389
+ unless @http &&
390
+ @http.started? &&
391
+ same_server_as_before
392
+ start(current_params)
393
+ end
394
+
395
+ # Detect if the body is a streamable object like a file or socket. If so, stream that
396
+ # bad boy.
397
+ setup_streaming(request)
398
+ # update READ_TIMEOUT value (it can be passed with request_params hash)
399
+ @http.read_timeout = get_param(:http_connection_read_timeout, current_params)
400
+ response = @http.request(request, &block)
401
+
402
+ error_reset
403
+ eof_reset
404
+ return response
405
+
406
+ # We treat EOF errors and the timeout/network errors differently. Both
407
+ # are tracked in different statistics blocks. Note below that EOF
408
+ # errors will sleep for a certain (exponentially increasing) period.
409
+ # Other errors don't sleep because there is already an inherent delay
410
+ # in them; connect and read timeouts (for example) have already
411
+ # 'slept'. It is still not clear which way we should treat errors
412
+ # like RST and resolution failures. For now, there is no additional
413
+ # delay for these errors although this may change in the future.
414
+
415
+ # EOFError means the server closed the connection on us.
416
+ rescue EOFError => e
417
+ @logger.debug("#{err_header} server #{@server} closed connection")
418
+ @http = nil
419
+
420
+ # if we have waited long enough - raise an exception...
421
+ if raise_on_eof_exception?
422
+ @logger.warn("#{err_header} raising #{exception} due to permanent EOF being received from #{@server}, error age: #{Time.now.to_i - eof_time.to_i}")
423
+ raise exception.new("Permanent EOF is being received from #{@server}.")
424
+ else
425
+ # ... else just sleep a bit before new retry
426
+ sleep(add_eof)
427
+ # We will be retrying the request, so reset the file pointer
428
+ reset_fileptr_offset(request, mypos)
429
+ end
430
+ rescue Exception => e # See comment at bottom for the list of errors seen...
431
+ @http = nil
432
+ timeout_exception = e.is_a?(Errno::ETIMEDOUT) || e.is_a?(Timeout::Error)
433
+ # Omit retries if it was explicitly requested
434
+ if current_params[:raise_on_timeout] && timeout_exception
435
+ # #6481:
436
+ # ... When creating a resource in EC2 (instance, volume, snapshot, etc) it is undetermined what happened if the call times out.
437
+ # The resource may or may not have been created in EC2. Retrying the call may cause multiple resources to be created...
438
+ raise e
439
+ end
440
+ # if ctrl+c is pressed - we have to reraise exception to terminate proggy
441
+ if e.is_a?(Interrupt) && !timeout_exception
442
+ @logger.debug( "#{err_header} request to server #{@server} interrupted by ctrl-c")
443
+ raise
444
+ elsif e.is_a?(ArgumentError) && e.message.include?('wrong number of arguments (5 for 4)')
445
+ # seems our net_fix patch was overriden...
446
+ raise exception.new('incompatible Net::HTTP monkey-patch')
447
+ end
448
+ # oops - we got a banana: log it
449
+ error_add(e.message)
450
+ @logger.warn("#{err_header} request failure count: #{error_count}, exception: #{e.inspect}")
451
+
452
+ # We will be retrying the request, so reset the file pointer
453
+ reset_fileptr_offset(request, mypos)
454
+
455
+ end
456
+ end
457
+ end
458
+
459
+ def finish(reason = '')
460
+ if @http && @http.started?
461
+ reason = ", reason: '#{reason}'" unless reason.empty?
462
+ @logger.info("Closing #{@http.use_ssl? ? 'HTTPS' : 'HTTP'} connection to #{@http.address}:#{@http.port}#{reason}")
463
+ @http.finish
464
+ end
465
+ end
466
+
467
+ # Errors received during testing:
468
+ #
469
+ # #<Timeout::Error: execution expired>
470
+ # #<Errno::ETIMEDOUT: Connection timed out - connect(2)>
471
+ # #<SocketError: getaddrinfo: Name or service not known>
472
+ # #<SocketError: getaddrinfo: Temporary failure in name resolution>
473
+ # #<EOFError: end of file reached>
474
+ # #<Errno::ECONNRESET: Connection reset by peer>
475
+ # #<OpenSSL::SSL::SSLError: SSL_write:: bad write retry>
476
+ end
477
+
478
+ end
479
+