devver-right_http_connection 1.2.99

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,59 @@
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
+
@@ -0,0 +1,7 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ lib/net_fix.rb
6
+ lib/right_http_connection.rb
7
+ setup.rb
@@ -0,0 +1,54 @@
1
+ RightScale::HttpConnection
2
+ by RightScale, Inc.
3
+ www.RightScale.com
4
+
5
+ == DESCRIPTION:
6
+
7
+ Rightscale::HttpConnection is a robust HTTP/S library. It implements a retry
8
+ algorithm for low-level network errors.
9
+
10
+ == FEATURES:
11
+
12
+ - provides put/get streaming
13
+ - does configurable retries on connect and read timeouts, DNS failures, etc.
14
+ - HTTPS certificate checking
15
+
16
+ == SYNOPSIS:
17
+
18
+
19
+ == REQUIREMENTS:
20
+
21
+ - 2/11/08: If you use RightScale::HttpConnection in conjunction with attachment_fu, the
22
+ HttpConnection gem must be included (using the require statement) AFTER
23
+ attachment_fu.
24
+ This is due to a conflict between the HttpConnection gem and another
25
+ gem required by attachment_fu.
26
+
27
+
28
+
29
+ == INSTALL:
30
+
31
+ sudo gem install right_http_connection
32
+
33
+ == LICENSE:
34
+
35
+ Copyright (c) 2007-2008 RightScale, Inc.
36
+
37
+ Permission is hereby granted, free of charge, to any person obtaining
38
+ a copy of this software and associated documentation files (the
39
+ 'Software'), to deal in the Software without restriction, including
40
+ without limitation the rights to use, copy, modify, merge, publish,
41
+ distribute, sublicense, and/or sell copies of the Software, and to
42
+ permit persons to whom the Software is furnished to do so, subject to
43
+ the following conditions:
44
+
45
+ The above copyright notice and this permission notice shall be
46
+ included in all copies or substantial portions of the Software.
47
+
48
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
49
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
50
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
51
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
52
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
53
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
54
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,103 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/clean'
4
+ require 'rake/testtask'
5
+ require 'rake/packagetask'
6
+ require 'rake/gempackagetask'
7
+ require 'rake/rdoctask'
8
+ require 'rake/contrib/rubyforgepublisher'
9
+ require 'fileutils'
10
+ require 'hoe'
11
+ include FileUtils
12
+ require File.join(File.dirname(__FILE__), 'lib', 'right_http_connection')
13
+
14
+ AUTHOR = 'RightScale' # can also be an array of Authors
15
+ EMAIL = "rubygems@rightscale.com"
16
+ DESCRIPTION = "RightScale's robust HTTP/S connection module"
17
+ GEM_NAME = 'devver-right_http_connection' # what ppl will type to install your gem
18
+ RUBYFORGE_PROJECT = 'rightscale' # The unix name for your project
19
+ HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
20
+ DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
21
+
22
+ NAME = "right_http_connection"
23
+ REV = nil # UNCOMMENT IF REQUIRED: File.read(".svn/entries")[/committed-rev="(d+)"/, 1] rescue nil
24
+ VERS = RightHttpConnection::VERSION::STRING + (REV ? ".#{REV}" : "")
25
+ CLEAN.include ['**/.*.sw?', '*.gem', '.config', '**/.DS_Store']
26
+ RDOC_OPTS = ['--quiet', '--title', 'right_http_connection documentation',
27
+ "--opname", "index.html",
28
+ "--line-numbers",
29
+ "--main", "README",
30
+ "--inline-source"]
31
+
32
+ # Suppress Hoe's self-inclusion as a dependency for our Gem. This also keeps
33
+ # Rake & rubyforge out of the dependency list. Users must manually install
34
+ # these gems to run tests, etc.
35
+ # TRB 2/19/09: also do this for the extra_dev_deps array present in newer hoes.
36
+ # Older versions of RubyGems will try to install developer-dependencies as
37
+ # required runtime dependencies....
38
+ class Hoe
39
+ def extra_deps
40
+ @extra_deps.reject do |x|
41
+ Array(x).first == 'hoe'
42
+ end
43
+ end
44
+ def extra_dev_deps
45
+ @extra_dev_deps.reject do |x|
46
+ Array(x).first == 'hoe'
47
+ end
48
+ end
49
+ end
50
+
51
+ # Generate all the Rake tasks
52
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
53
+ hoe = Hoe.new(GEM_NAME, VERS) do |p|
54
+ p.author = AUTHOR
55
+ p.description = DESCRIPTION
56
+ p.email = EMAIL
57
+ p.summary = DESCRIPTION
58
+ p.url = HOMEPATH
59
+ p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
60
+ p.test_globs = ["test/**/test_*.rb"]
61
+ p.clean_globs = CLEAN #An array of file patterns to delete on clean.
62
+ p.remote_rdoc_dir = "right_http_gem_doc"
63
+
64
+ # == Optional
65
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
66
+ #p.extra_deps = [] # An array of rubygem dependencies [name, version], e.g. [ ['active_support', '>= 1.3.1'] ]
67
+ #p.spec_extras = {} # A hash of extra values to set in the gemspec.
68
+ end
69
+
70
+
71
+ desc 'Generate website files'
72
+ task :website_generate do
73
+ Dir['website/**/*.txt'].each do |txt|
74
+ sh %{ ruby scripts/txt2html #{txt} > #{txt.gsub(/txt$/,'html')} }
75
+ end
76
+ end
77
+
78
+ desc 'Upload website files to rubyforge'
79
+ task :website_upload do
80
+ config = YAML.load(File.read(File.expand_path("~/.rubyforge/user-config.yml")))
81
+ host = "#{config["username"]}@rubyforge.org"
82
+ remote_dir = "/var/www/gforge-projects/#{RUBYFORGE_PROJECT}/"
83
+ # remote_dir = "/var/www/gforge-projects/#{RUBYFORGE_PROJECT}/#{GEM_NAME}"
84
+ local_dir = 'website'
85
+ sh %{rsync -av #{local_dir}/ #{host}:#{remote_dir}}
86
+ end
87
+
88
+ desc 'Generate and upload website files'
89
+ task :website => [:website_generate, :website_upload]
90
+
91
+ desc 'Release the website and new gem version'
92
+ task :deploy => [:check_version, :website, :release]
93
+
94
+ task :check_version do
95
+ unless ENV['VERSION']
96
+ puts 'Must pass a VERSION=x.y.z release version'
97
+ exit
98
+ end
99
+ unless ENV['VERSION'] == VERS
100
+ puts "Please update your version.rb to match the release version, currently #{VERS}"
101
+ exit
102
+ end
103
+ end
@@ -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,472 @@
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 "net_fix"
31
+
32
+
33
+ module RightHttpConnection #:nodoc:
34
+ module VERSION #:nodoc:
35
+ MAJOR = 1
36
+ MINOR = 2
37
+ TINY = 99 #4
38
+
39
+ STRING = [MAJOR, MINOR, TINY].join('.')
40
+ end
41
+ end
42
+
43
+
44
+ module Rightscale
45
+
46
+ =begin rdoc
47
+ HttpConnection maintains a persistent HTTP connection to a remote
48
+ server. Each instance maintains its own unique connection to the
49
+ HTTP server. HttpConnection makes a best effort to receive a proper
50
+ HTTP response from the server, although it does not guarantee that
51
+ this response contains a HTTP Success code.
52
+
53
+ On low-level errors (TCP/IP errors) HttpConnection invokes a reconnect
54
+ and retry algorithm. Note that although each HttpConnection object
55
+ has its own connection to the HTTP server, error handling is shared
56
+ across all connections to a server. For example, if there are three
57
+ connections to www.somehttpserver.com, a timeout error on one of those
58
+ connections will cause all three connections to break and reconnect.
59
+ A connection will not break and reconnect, however, unless a request
60
+ becomes active on it within a certain amount of time after the error
61
+ (as specified by HTTP_CONNECTION_RETRY_DELAY). An idle connection will not
62
+ break even if other connections to the same server experience errors.
63
+
64
+ A HttpConnection will retry a request a certain number of times (as
65
+ defined by HTTP_CONNNECTION_RETRY_COUNT). If all the retries fail,
66
+ an exception is thrown and all HttpConnections associated with a
67
+ server enter a probationary period defined by HTTP_CONNECTION_RETRY_DELAY.
68
+ If the user makes a new request subsequent to entering probation,
69
+ the request will fail immediately with the same exception thrown
70
+ on probation entry. This is so that if the HTTP server has gone
71
+ down, not every subsequent request must wait for a connect timeout
72
+ before failing. After the probation period expires, the internal
73
+ state of the HttpConnection is reset and subsequent requests have
74
+ the full number of potential reconnects and retries available to
75
+ them.
76
+ =end
77
+
78
+ class HttpConnection
79
+
80
+ # Number of times to retry the request after encountering the first error
81
+ HTTP_CONNECTION_RETRY_COUNT = 3
82
+ # Throw a Timeout::Error if a connection isn't established within this number of seconds
83
+ HTTP_CONNECTION_OPEN_TIMEOUT = 5
84
+ # Throw a Timeout::Error if no data have been read on this connnection within this number of seconds
85
+ HTTP_CONNECTION_READ_TIMEOUT = 120
86
+ # Length of the post-error probationary period during which all requests will fail
87
+ HTTP_CONNECTION_RETRY_DELAY = 15
88
+
89
+ # Location of the system CA certificates file on Debian/Ubuntu systems, as
90
+ # generated by the ca-certificates package.
91
+ DEBIAN_CA_FILE = '/etc/ssl/certs/ca-certificates.crt'
92
+
93
+ #--------------------
94
+ # class methods
95
+ #--------------------
96
+ #
97
+ @@params = {}
98
+ @@params[:http_connection_retry_count] = HTTP_CONNECTION_RETRY_COUNT
99
+ @@params[:http_connection_open_timeout] = HTTP_CONNECTION_OPEN_TIMEOUT
100
+ @@params[:http_connection_read_timeout] = HTTP_CONNECTION_READ_TIMEOUT
101
+ @@params[:http_connection_retry_delay] = HTTP_CONNECTION_RETRY_DELAY
102
+
103
+ # Query the global (class-level) parameters:
104
+ #
105
+ # :user_agent => 'www.HostName.com' # String to report as HTTP User agent
106
+ # :ca_file => 'path_to_file' # Path to a CA certification file in
107
+ # # PEM format. The file can contain
108
+ # # several CA certificates. If this
109
+ # # parameter isn't set,
110
+ # # HttpConnection will check for a
111
+ # # system CA file. If no CA file is
112
+ # # found, HTTPS certs won't be
113
+ # # verified.
114
+ # #
115
+ # # This parameter may be overridden
116
+ # # with the RIGHT_HTTP_CA_FILE
117
+ # # environment variable.
118
+ # :logger => Logger object # If omitted, HttpConnection logs to STDOUT
119
+ # :exception => Exception to raise # The type of exception to raise
120
+ # # if a request repeatedly fails. RuntimeError is raised if this parameter is omitted.
121
+ # :http_connection_retry_count # by default == Rightscale::HttpConnection::HTTP_CONNECTION_RETRY_COUNT
122
+ # :http_connection_open_timeout # by default == Rightscale::HttpConnection::HTTP_CONNECTION_OPEN_TIMEOUT
123
+ # :http_connection_read_timeout # by default == Rightscale::HttpConnection::HTTP_CONNECTION_READ_TIMEOUT
124
+ # :http_connection_retry_delay # by default == Rightscale::HttpConnection::HTTP_CONNECTION_RETRY_DELAY
125
+ def self.params
126
+ @@params
127
+ end
128
+
129
+ # Set the global (class-level) parameters
130
+ def self.params=(params)
131
+ @@params = params
132
+ end
133
+
134
+ #------------------
135
+ # instance methods
136
+ #------------------
137
+ attr_accessor :http
138
+ attr_accessor :server
139
+ attr_accessor :params # see @@params
140
+ attr_accessor :logger
141
+
142
+ # Params hash:
143
+ # :user_agent => 'www.HostName.com' # String to report as HTTP User agent
144
+ # :ca_file => 'path_to_file' # A path of a CA certification file in PEM format. The file can contain several CA certificates.
145
+ # :logger => Logger object # If omitted, HttpConnection logs to STDOUT
146
+ # :exception => Exception to raise # The type of exception to raise if a request repeatedly fails. RuntimeError is raised if this parameter is omitted.
147
+ # :http_connection_retry_count # by default == Rightscale::HttpConnection.params[:http_connection_retry_count]
148
+ # :http_connection_open_timeout # by default == Rightscale::HttpConnection.params[:http_connection_open_timeout]
149
+ # :http_connection_read_timeout # by default == Rightscale::HttpConnection.params[:http_connection_read_timeout]
150
+ # :http_connection_retry_delay # by default == Rightscale::HttpConnection.params[:http_connection_retry_delay]
151
+ #
152
+ def initialize(params={})
153
+ @params = params
154
+ @params[:http_connection_retry_count] ||= @@params[:http_connection_retry_count]
155
+ @params[:http_connection_open_timeout] ||= @@params[:http_connection_open_timeout]
156
+ @params[:http_connection_read_timeout] ||= @@params[:http_connection_read_timeout]
157
+ @params[:http_connection_retry_delay] ||= @@params[:http_connection_retry_delay]
158
+ @http = nil
159
+ @server = nil
160
+ @logger = get_param(:logger) ||
161
+ (RAILS_DEFAULT_LOGGER if defined?(RAILS_DEFAULT_LOGGER)) ||
162
+ Logger.new(STDOUT)
163
+ end
164
+
165
+ def get_param(name)
166
+ @params[name] || @@params[name]
167
+ end
168
+
169
+ # Query for the maximum size (in bytes) of a single read from the underlying
170
+ # socket. For bulk transfer, especially over fast links, this is value is
171
+ # critical to performance.
172
+ def socket_read_size?
173
+ Net::BufferedIO.socket_read_size?
174
+ end
175
+
176
+ # Set the maximum size (in bytes) of a single read from the underlying
177
+ # socket. For bulk transfer, especially over fast links, this is value is
178
+ # critical to performance.
179
+ def socket_read_size=(newsize)
180
+ Net::BufferedIO.socket_read_size=(newsize)
181
+ end
182
+
183
+ # Query for the maximum size (in bytes) of a single read from local data
184
+ # sources like files. This is important, for example, in a streaming PUT of a
185
+ # large buffer.
186
+ def local_read_size?
187
+ Net::HTTPGenericRequest.local_read_size?
188
+ end
189
+
190
+ # Set the maximum size (in bytes) of a single read from local data
191
+ # sources like files. This can be used to tune the performance of, for example, a streaming PUT of a
192
+ # large buffer.
193
+ def local_read_size=(newsize)
194
+ Net::HTTPGenericRequest.local_read_size=(newsize)
195
+ end
196
+
197
+ private
198
+ #--------------
199
+ # Retry state - Keep track of errors on a per-server basis
200
+ #--------------
201
+ @@state = {} # retry state indexed by server: consecutive error count, error time, and error
202
+ @@eof = {}
203
+
204
+ # number of consecutive errors seen for server, 0 all is ok
205
+ def error_count
206
+ @@state[@server] ? @@state[@server][:count] : 0
207
+ end
208
+
209
+ # time of last error for server, nil if all is ok
210
+ def error_time
211
+ @@state[@server] && @@state[@server][:time]
212
+ end
213
+
214
+ # message for last error for server, "" if all is ok
215
+ def error_message
216
+ @@state[@server] ? @@state[@server][:message] : ""
217
+ end
218
+
219
+ # add an error for a server
220
+ def error_add(message)
221
+ @@state[@server] = { :count => error_count+1, :time => Time.now, :message => message }
222
+ end
223
+
224
+ # reset the error state for a server (i.e. a request succeeded)
225
+ def error_reset
226
+ @@state.delete(@server)
227
+ end
228
+
229
+ # Error message stuff...
230
+ def banana_message
231
+ return "#{@server} temporarily unavailable: (#{error_message})"
232
+ end
233
+
234
+ def err_header
235
+ return "#{self.class.name} :"
236
+ end
237
+
238
+ # Adds new EOF timestamp.
239
+ # Returns the number of seconds to wait before new conection retry:
240
+ # 0.5, 1, 2, 4, 8
241
+ def add_eof
242
+ (@@eof[@server] ||= []).unshift Time.now
243
+ 0.25 * 2 ** @@eof[@server].size
244
+ end
245
+
246
+ # Returns first EOF timestamp or nul if have no EOFs being tracked.
247
+ def eof_time
248
+ @@eof[@server] && @@eof[@server].last
249
+ end
250
+
251
+ # Returns true if we are receiving EOFs during last @params[:http_connection_retry_delay] seconds
252
+ # and there were no successful response from server
253
+ def raise_on_eof_exception?
254
+ @@eof[@server].blank? ? false : ( (Time.now.to_i-@params[:http_connection_retry_delay]) > @@eof[@server].last.to_i )
255
+ end
256
+
257
+ # Reset a list of EOFs for this server.
258
+ # This is being called when we have got an successful response from server.
259
+ def eof_reset
260
+ @@eof.delete(@server)
261
+ end
262
+
263
+ # Detects if an object is 'streamable' - can we read from it, and can we know the size?
264
+ def setup_streaming(request)
265
+ if(request.body && request.body.respond_to?(:read))
266
+ body = request.body
267
+ request.content_length = body.respond_to?(:lstat) ? body.lstat.size : body.size
268
+ request.body_stream = request.body
269
+ true
270
+ end
271
+ end
272
+
273
+ def get_fileptr_offset(request_params)
274
+ request_params[:request].body.pos
275
+ rescue Exception => e
276
+ # Probably caught this because the body doesn't support the pos() method, like if it is a socket.
277
+ # Just return 0 and get on with life.
278
+ 0
279
+ end
280
+
281
+ def reset_fileptr_offset(request, offset = 0)
282
+ if(request.body_stream && request.body_stream.respond_to?(:pos))
283
+ begin
284
+ request.body_stream.pos = offset
285
+ rescue Exception => e
286
+ @logger.warn("Failed file pointer reset; aborting HTTP retries." +
287
+ " -- #{err_header} #{e.inspect}")
288
+ raise e
289
+ end
290
+ end
291
+ end
292
+
293
+ # Start a fresh connection. The object closes any existing connection and
294
+ # opens a new one.
295
+ def start(request_params)
296
+ # close the previous if exists
297
+ finish
298
+ # create new connection
299
+ @server = request_params[:server]
300
+ @port = request_params[:port]
301
+ @protocol = request_params[:protocol]
302
+
303
+ @logger.info("Opening new #{@protocol.upcase} connection to #@server:#@port")
304
+ # raise 1/0
305
+
306
+ @http = Net::HTTP.new(@server, @port)
307
+ @http.open_timeout = @params[:http_connection_open_timeout]
308
+ @http.read_timeout = @params[:http_connection_read_timeout]
309
+
310
+ if @protocol == 'https'
311
+ verifyCallbackProc = Proc.new{ |ok, x509_store_ctx|
312
+ code = x509_store_ctx.error
313
+ msg = x509_store_ctx.error_string
314
+ #debugger
315
+ @logger.warn("##### #{@server} certificate verify failed: #{msg}") unless code == 0
316
+ true
317
+ }
318
+ @http.use_ssl = true
319
+ ca_file = get_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
+ end
325
+ end
326
+ # open connection
327
+ @http.start
328
+ end
329
+
330
+ public
331
+
332
+ =begin rdoc
333
+ Send HTTP request to server
334
+
335
+ request_params hash:
336
+ :server => 'www.HostName.com' # Hostname or IP address of HTTP server
337
+ :port => '80' # Port of HTTP server
338
+ :protocol => 'https' # http and https are supported on any port
339
+ :request => 'requeststring' # Fully-formed HTTP request to make
340
+
341
+ Raises RuntimeError, Interrupt, and params[:exception] (if specified in new).
342
+
343
+ =end
344
+ def request(request_params, &block)
345
+ # We save the offset here so that if we need to retry, we can return the file pointer to its initial position
346
+ mypos = get_fileptr_offset(request_params)
347
+ loop do
348
+ request_params[:protocol] ||= (request_params[:port] == 443 ? 'https' : 'http')
349
+ # (re)open connection to server if none exists or params has changed
350
+ same_server_as_before = @server == request_params[:server] &&
351
+ @port == request_params[:port] &&
352
+ @protocol == request_params[:protocol]
353
+
354
+ # if we are inside a delay between retries: no requests this time!
355
+ # (skip this step if the endpoint has changed)
356
+ if error_count > @params[:http_connection_retry_count] &&
357
+ error_time + @params[:http_connection_retry_delay] > Time.now &&
358
+ same_server_as_before
359
+
360
+ # store the message (otherwise it will be lost after error_reset and
361
+ # we will raise an exception with an empty text)
362
+ banana_message_text = banana_message
363
+ @logger.warn("#{err_header} re-raising same error: #{banana_message_text} " +
364
+ "-- error count: #{error_count}, error age: #{Time.now.to_i - error_time.to_i}")
365
+ exception = get_param(:exception) || RuntimeError
366
+ raise exception.new(banana_message_text)
367
+ end
368
+
369
+ # try to connect server(if connection does not exist) and get response data
370
+ begin
371
+ request = request_params[:request]
372
+ request['User-Agent'] = get_param(:user_agent) || ''
373
+ unless @http &&
374
+ @http.started? &&
375
+ same_server_as_before
376
+ start(request_params)
377
+ end
378
+
379
+ # Detect if the body is a streamable object like a file or socket. If so, stream that
380
+ # bad boy.
381
+ setup_streaming(request)
382
+ response = @http.request(request, &block)
383
+
384
+ error_reset
385
+ eof_reset
386
+ return response
387
+
388
+ # We treat EOF errors and the timeout/network errors differently. Both
389
+ # are tracked in different statistics blocks. Note below that EOF
390
+ # errors will sleep for a certain (exponentially increasing) period.
391
+ # Other errors don't sleep because there is already an inherent delay
392
+ # in them; connect and read timeouts (for example) have already
393
+ # 'slept'. It is still not clear which way we should treat errors
394
+ # like RST and resolution failures. For now, there is no additional
395
+ # delay for these errors although this may change in the future.
396
+
397
+ # EOFError means the server closed the connection on us.
398
+ rescue EOFError => e
399
+ @logger.debug("#{err_header} server #{@server} closed connection")
400
+ @http = nil
401
+
402
+ # if we have waited long enough - raise an exception...
403
+ if raise_on_eof_exception?
404
+ exception = get_param(:exception) || RuntimeError
405
+ @logger.warn("#{err_header} raising #{exception} due to permanent EOF being received from #{@server}, error age: #{Time.now.to_i - eof_time.to_i}")
406
+ raise exception.new("Permanent EOF is being received from #{@server}.")
407
+ else
408
+ # ... else just sleep a bit before new retry
409
+ sleep(add_eof)
410
+ # We will be retrying the request, so reset the file pointer
411
+ reset_fileptr_offset(request, mypos)
412
+ end
413
+ rescue Exception => e # See comment at bottom for the list of errors seen...
414
+ @http = nil
415
+ # if ctrl+c is pressed - we have to reraise exception to terminate proggy
416
+ if e.is_a?(Interrupt) && !( e.is_a?(Errno::ETIMEDOUT) || e.is_a?(Timeout::Error))
417
+ @logger.debug( "#{err_header} request to server #{@server} interrupted by ctrl-c")
418
+ raise
419
+ elsif e.is_a?(ArgumentError) && e.message.include?('wrong number of arguments (5 for 4)')
420
+ # seems our net_fix patch was overriden...
421
+ exception = get_param(:exception) || RuntimeError
422
+ raise exception.new('incompatible Net::HTTP monkey-patch')
423
+ end
424
+ # oops - we got a banana: log it
425
+ error_add(e.message)
426
+ @logger.warn("#{err_header} request failure count: #{error_count}, exception: #{e.inspect}")
427
+
428
+ # We will be retrying the request, so reset the file pointer
429
+ reset_fileptr_offset(request, mypos)
430
+
431
+ end
432
+ end
433
+ end
434
+
435
+ def finish(reason = '')
436
+ if @http && @http.started?
437
+ reason = ", reason: '#{reason}'" unless reason.blank?
438
+ @logger.info("Closing #{@http.use_ssl? ? 'HTTPS' : 'HTTP'} connection to #{@http.address}:#{@http.port}#{reason}")
439
+ @http.finish
440
+ end
441
+ end
442
+
443
+ def get_ca_file
444
+ get_ca_file_from_env || get_param(:ca_file) || get_system_ca_file
445
+ end
446
+
447
+ def get_ca_file_from_env
448
+ ENV['RIGHT_HTTP_CA_FILE']
449
+ end
450
+
451
+ def get_system_ca_file
452
+ system_ca_file = DEBIAN_CA_FILE
453
+ if File.readable?(system_ca_file)
454
+ system_ca_file
455
+ else
456
+ nil
457
+ end
458
+ end
459
+
460
+ # Errors received during testing:
461
+ #
462
+ # #<Timeout::Error: execution expired>
463
+ # #<Errno::ETIMEDOUT: Connection timed out - connect(2)>
464
+ # #<SocketError: getaddrinfo: Name or service not known>
465
+ # #<SocketError: getaddrinfo: Temporary failure in name resolution>
466
+ # #<EOFError: end of file reached>
467
+ # #<Errno::ECONNRESET: Connection reset by peer>
468
+ # #<OpenSSL::SSL::SSLError: SSL_write:: bad write retry>
469
+ end
470
+
471
+ end
472
+