right_http_connection 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ +++ 0.0.1 2007-05-15
2
+
3
+ + 1 major enhancement:
4
+ + Initial release
5
+
6
+ +++ 0.1.2 2007-6-27
7
+
8
+ No major changes.
9
+
10
+ +++ 0.1.3 2007-7-09
11
+
12
+ No change.
13
+
14
+ +++ 0.1.4 2007-8-10
15
+
16
+ ------------------------------------------------------------------------
17
+ r1442 | todd | 2007-08-07 15:45:24 -0700 (Tue, 07 Aug 2007) | 654 lines
18
+
19
+ (#373) Add support in right_http_connection for bailing out to a block while
20
+ reading the HTTP response (to support GET streaming...)
21
+
22
+ ------------------------------------------------------------------------
23
+ r1411 | todd | 2007-08-03 15:14:45 -0700 (Fri, 03 Aug 2007) | 3 lines
24
+
25
+ (#373) Stream uploads (PUTs) if the source is a file, stream, or anything
26
+ read()-able
27
+
28
+ +++ 1.1.0 2007-08-15
29
+ Initial public release
30
+
@@ -0,0 +1,7 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ lib/right_http_connection.rb
6
+ lib/right_http_connection/version.rb
7
+ setup.rb
@@ -0,0 +1,48 @@
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/PROBLEMS:
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
+ None.
22
+
23
+ == INSTALL:
24
+
25
+ sudo gem install
26
+
27
+ == LICENSE:
28
+
29
+ Copyright (c) 2007 RightScale, Inc.
30
+
31
+ Permission is hereby granted, free of charge, to any person obtaining
32
+ a copy of this software and associated documentation files (the
33
+ 'Software'), to deal in the Software without restriction, including
34
+ without limitation the rights to use, copy, modify, merge, publish,
35
+ distribute, sublicense, and/or sell copies of the Software, and to
36
+ permit persons to whom the Software is furnished to do so, subject to
37
+ the following conditions:
38
+
39
+ The above copyright notice and this permission notice shall be
40
+ included in all copies or substantial portions of the Software.
41
+
42
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
43
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
44
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
45
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
46
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
47
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
48
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,90 @@
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', 'version')
13
+
14
+ AUTHOR = 'RightScale' # can also be an array of Authors
15
+ EMAIL = "support@rightscale.com"
16
+ DESCRIPTION = "RightScale's robust HTTP/S connection module"
17
+ GEM_NAME = 'right_http_connection' # what ppl will type to install your gem
18
+ RUBYFORGE_PROJECT = 'rightaws' # 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
+ class Hoe
33
+ def extra_deps
34
+ @extra_deps.reject { |x| Array(x).first == 'hoe' }
35
+ end
36
+ end
37
+
38
+ # Generate all the Rake tasks
39
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
40
+ hoe = Hoe.new(GEM_NAME, VERS) do |p|
41
+ p.author = AUTHOR
42
+ p.description = DESCRIPTION
43
+ p.email = EMAIL
44
+ p.summary = DESCRIPTION
45
+ p.url = HOMEPATH
46
+ p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
47
+ p.test_globs = ["test/**/test_*.rb"]
48
+ p.clean_globs = CLEAN #An array of file patterns to delete on clean.
49
+ p.remote_rdoc_dir = "/right_http_gem_doc"
50
+
51
+ # == Optional
52
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
53
+ #p.extra_deps = [] # An array of rubygem dependencies [name, version], e.g. [ ['active_support', '>= 1.3.1'] ]
54
+ #p.spec_extras = {} # A hash of extra values to set in the gemspec.
55
+ end
56
+
57
+
58
+ desc 'Generate website files'
59
+ task :website_generate do
60
+ Dir['website/**/*.txt'].each do |txt|
61
+ sh %{ ruby scripts/txt2html #{txt} > #{txt.gsub(/txt$/,'html')} }
62
+ end
63
+ end
64
+
65
+ desc 'Upload website files to rubyforge'
66
+ task :website_upload do
67
+ config = YAML.load(File.read(File.expand_path("~/.rubyforge/user-config.yml")))
68
+ host = "#{config["username"]}@rubyforge.org"
69
+ remote_dir = "/var/www/gforge-projects/#{RUBYFORGE_PROJECT}/"
70
+ # remote_dir = "/var/www/gforge-projects/#{RUBYFORGE_PROJECT}/#{GEM_NAME}"
71
+ local_dir = 'website'
72
+ sh %{rsync -av #{local_dir}/ #{host}:#{remote_dir}}
73
+ end
74
+
75
+ desc 'Generate and upload website files'
76
+ task :website => [:website_generate, :website_upload]
77
+
78
+ desc 'Release the website and new gem version'
79
+ task :deploy => [:check_version, :website, :release]
80
+
81
+ task :check_version do
82
+ unless ENV['VERSION']
83
+ puts 'Must pass a VERSION=x.y.z release version'
84
+ exit
85
+ end
86
+ unless ENV['VERSION'] == VERS
87
+ puts "Please update your version.rb to match the release version, currently #{VERS}"
88
+ exit
89
+ end
90
+ end
@@ -0,0 +1,306 @@
1
+ #
2
+ # Copyright (c) 2007 RightScale Inc, all rights reserved
3
+ #
4
+
5
+ require "net/https"
6
+ require "uri"
7
+ require "time"
8
+ require "logger"
9
+
10
+
11
+ module Rightscale
12
+
13
+ =begin rdoc
14
+ HttpConnection maintains a persistent HTTP connection to a remote
15
+ server. Each instance maintains its own unique connection to the
16
+ HTTP server. HttpConnection makes a best effort to receive a proper
17
+ HTTP response from the server, although it does not guarantee that
18
+ this response contains a HTTP Success code.
19
+
20
+ On low-level errors (TCP/IP errors) HttpConnection invokes a reconnect
21
+ and retry algorithm. Note that although each HttpConnection object
22
+ has its own connection to the HTTP server, error handling is shared
23
+ across all connections to a server. For example, if there are three
24
+ connections to www.somehttpserver.com, a timeout error on one of those
25
+ connections will cause all three connections to break and reconnect.
26
+ A connection will not break and reconnect, however, unless a request
27
+ becomes active on it within a certain amount of time after the error
28
+ (as specified by HTTP_CONNECTION_RETRY_DELAY). An idle connection will not
29
+ break even if other connections to the same server experience errors.
30
+
31
+ A HttpConnection will retry a request a certain number of times (as
32
+ defined by HTTP_CONNNECTION_RETRY_COUNT). If all the retries fail,
33
+ an exception is thrown and all HttpConnections associated with a
34
+ server enter a probationary period defined by HTTP_CONNECTION_RETRY_DELAY.
35
+ If the user makes a new request subsequent to entering probation,
36
+ the request will fail immediately with the same exception thrown
37
+ on probation entry. This is so that if the HTTP server has gone
38
+ down, not every subsequent request must wait for a connect timeout
39
+ before failing. After the probation period expires, the internal
40
+ state of the HttpConnection is reset and subsequent requests have
41
+ the full number of potential reconnects and retries available to
42
+ them.
43
+ =end
44
+
45
+ class HttpConnection
46
+
47
+ # Number of times to retry the request after encountering the first error
48
+ HTTP_CONNECTION_RETRY_COUNT = 3
49
+ # Throw a Timeout::Error if a connection isn't established within this number of seconds
50
+ HTTP_CONNECTION_OPEN_TIMEOUT = 5
51
+ # Throw a Timeout::Error if no data have been read on this connnection within this number of seconds
52
+ HTTP_CONNECTION_READ_TIMEOUT = 30
53
+ # Length of the post-error probationary period during which all requests will fail
54
+ HTTP_CONNECTION_RETRY_DELAY = 15
55
+
56
+ #--------------------
57
+ # class methods
58
+ #--------------------
59
+ # Params hash
60
+ # :user_agent => 'www.HostName.com' # String to report as HTTP User agent
61
+ # :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.
62
+ # :logger => Logger object # If omitted, HttpConnection logs to STDOUT
63
+ # :exception => Exception to raise # The type of exception to raise
64
+ # if a request repeatedly fails. RuntimeError is raised if this parameter is omitted.
65
+ @@params = {}
66
+
67
+ # Query the global (class-level) parameters
68
+ def self.params
69
+ @@params
70
+ end
71
+
72
+ # Set the global (class-level) parameters
73
+ def self.params=(params)
74
+ @@params = params
75
+ end
76
+
77
+ #------------------
78
+ # instance methods
79
+ #------------------
80
+ attr_accessor :http
81
+ attr_accessor :server
82
+ attr_accessor :params # see @@params
83
+ attr_accessor :logger
84
+
85
+ =begin rdoc
86
+ Params hash:
87
+ :user_agent => 'www.HostName.com' String to report as HTTP User agent
88
+ :ca_file => 'path_to_file' A path of a CA certification file in PEM format. The file can contain several CA certificates.
89
+ :logger => Logger object If omitted, HttpConnection logs to STDOUT
90
+ :exception => Exception to raise The type of exception to raise if a request repeatedly fails. RuntimeError is raised if this parameter is omitted.
91
+
92
+ =end
93
+ def initialize(params={})
94
+ @params = params
95
+ @http = nil
96
+ @server = nil
97
+ @logger = get_param(:logger) ||
98
+ (RAILS_DEFAULT_LOGGER if defined?(RAILS_DEFAULT_LOGGER)) ||
99
+ Logger.new(STDOUT)
100
+ end
101
+
102
+ def get_param(name)
103
+ @params[name] || @@params[name]
104
+ end
105
+
106
+ private
107
+ #--------------
108
+ # Retry state - Keep track of errors on a per-server basis
109
+ #--------------
110
+ @@state = {} # retry state indexed by server: consecutive error count, error time, and error
111
+ @@eof = {}
112
+
113
+ # number of consecutive errors seen for server, 0 all is ok
114
+ def error_count
115
+ @@state[@server] ? @@state[@server][:count] : 0
116
+ end
117
+
118
+ # time of last error for server, nil if all is ok
119
+ def error_time
120
+ @@state[@server] && @@state[@server][:time]
121
+ end
122
+
123
+ # message for last error for server, "" if all is ok
124
+ def error_message
125
+ @@state[@server] ? @@state[@server][:message] : ""
126
+ end
127
+
128
+ # add an error for a server
129
+ def error_add(message)
130
+ @@state[@server] = { :count => error_count+1, :time => Time.now, :message => message }
131
+ end
132
+
133
+ # reset the error state for a server (i.e. a request succeeded)
134
+ def error_reset
135
+ @@state.delete(@server)
136
+ end
137
+
138
+ # Error message stuff...
139
+ def banana_message
140
+ return "#{@server} temporarily unavailable: (#{error_message})"
141
+ end
142
+
143
+ def err_header
144
+ return "#{self.class.name} :"
145
+ end
146
+
147
+ # Adds new EOF timestamp.
148
+ # Returns the number of seconds to wait before new conection retry:
149
+ # 0.5, 1, 2, 4, 8
150
+ def add_eof
151
+ (@@eof[@server] ||= []).unshift Time.now
152
+ 0.25 * 2 ** @@eof[@server].size
153
+ end
154
+
155
+ # Returns first EOF timestamp or nul if have no EOFs being tracked.
156
+ def eof_time
157
+ @@eof[@server] && @@eof[@server].last
158
+ end
159
+
160
+ # Returns true if we are receiving EOFs during last HTTP_CONNECTION_RETRY_DELAY seconds
161
+ # and there were no successful response from server
162
+ def raise_on_eof_exception?
163
+ @@eof[@server].blank? ? false : ( (Time.now.to_i-HTTP_CONNECTION_RETRY_DELAY) > @@eof[@server].last.to_i )
164
+ end
165
+
166
+ # Reset a list of EOFs for this server.
167
+ # This is being called when we have got an successful response from server.
168
+ def eof_reset
169
+ @@eof.delete(@server)
170
+ end
171
+
172
+ # Start a fresh connection. The object closes any existing connection and
173
+ # opens a new one.
174
+ def start(request_params)
175
+ # close the previous if exists
176
+ @http.finish if @http && @http.started?
177
+ # create new connection
178
+ @server = request_params[:server]
179
+ @port = request_params[:port]
180
+ @protocol = request_params[:protocol] || (@port==443 ? 'https' : 'http')
181
+
182
+ @logger.info("Opening new HTTP connection to #{@server}")
183
+ @http = Net::HTTP.new(@server, @port)
184
+ @http.open_timeout = HTTP_CONNECTION_OPEN_TIMEOUT
185
+ @http.read_timeout = HTTP_CONNECTION_READ_TIMEOUT
186
+
187
+ if @protocol == 'https'
188
+ verifyCallbackProc = Proc.new{ |ok, x509_store_ctx|
189
+ code = x509_store_ctx.error
190
+ msg = x509_store_ctx.error_string
191
+ #debugger
192
+ @logger.warn("##### #{@server} certificate verify failed: #{msg}") unless code == 0
193
+ true
194
+ }
195
+ @http.use_ssl = true
196
+ ca_file = get_param(:ca_file)
197
+ if ca_file
198
+ @http.verify_mode = OpenSSL::SSL::VERIFY_PEER
199
+ @http.verify_callback = verifyCallbackProc
200
+ @http.ca_file = ca_file
201
+ end
202
+ end
203
+ # open connection
204
+ @http.start
205
+ end
206
+
207
+ public
208
+
209
+ =begin rdoc
210
+ Send HTTP request to server
211
+
212
+ request_params hash:
213
+ :server => 'www.HostName.com' Hostname or IP address of HTTP server
214
+ :port => '80' Port of HTTP server
215
+ :protocol => 'https' http and https are supported on any port
216
+ :request => 'requeststring' Fully-formed HTTP request to make
217
+
218
+ Raises RuntimeError, Interrupt, and params[:exception] (if specified in new).
219
+
220
+ =end
221
+ def request(request_params, &block)
222
+ loop do
223
+ # if we are inside a delay between retries: no requests this time!
224
+ if error_count > HTTP_CONNECTION_RETRY_COUNT \
225
+ && error_time + HTTP_CONNECTION_RETRY_DELAY > Time.now
226
+ @logger.warn("#{err_header} re-raising same error: #{banana_message} " +
227
+ "-- error count: #{error_count}, error age: #{Time.now.to_i - error_time.to_i}")
228
+ exception = get_param(:exception) || RuntimeError
229
+ raise exception.new(banana_message)
230
+ end
231
+
232
+ # try to connect server(if connection does not exist) and get response data
233
+ begin
234
+ # (re)open connection to server if none exists
235
+ # TODO TRB 8/2/07 - you also need to get a new connection if the
236
+ # server, port, or proto has changed in the request_params
237
+ start(request_params) unless @http
238
+
239
+ # get response and return it
240
+ request = request_params[:request]
241
+ request['User-Agent'] = get_param(:user_agent) || ''
242
+
243
+ # Detect if the body is a streamable object like a file or socket. If so, stream that
244
+ # bad boy.
245
+ if(request.body && request.body.respond_to?(:read))
246
+ body = request.body
247
+ request.content_length = body.respond_to?(:lstat) ? body.lstat.size : body.size
248
+ request.body_stream = request.body
249
+ end
250
+ response = @http.request(request, &block)
251
+
252
+ error_reset
253
+ eof_reset
254
+ return response
255
+
256
+ # We treat EOF errors and the timeout/network errors differently. Both
257
+ # are tracked in different statistics blocks. Note below that EOF
258
+ # errors will sleep for a certain (exponentially increasing) period.
259
+ # Other errors don't sleep because there is already an inherent delay
260
+ # in them; connect and read timeouts (for example) have already
261
+ # 'slept'. It is still not clear which way we should treat errors
262
+ # like RST and resolution failures. For now, there is no additional
263
+ # delay for these errors although this may change in the future.
264
+
265
+ # EOFError means the server closed the connection on us.
266
+ rescue EOFError => e
267
+ @logger.debug("#{err_header} server #{@server} closed connection")
268
+ @http = nil
269
+
270
+ # if we have waited long enough - raise an exception...
271
+ if raise_on_eof_exception?
272
+ exception = get_param(:exception) || RuntimeError
273
+ @logger.warn("#{err_header} raising #{exception} due to permanent EOF being received from #{@server}, error age: #{Time.now.to_i - eof_time.to_i}")
274
+ raise exception.new("Permanent EOF is being received from #{@server}.")
275
+ else
276
+ # ... else just sleep a bit before new retry
277
+ sleep(add_eof)
278
+ end
279
+
280
+ rescue Exception => e # See comment at bottom for the list of errors seen...
281
+ # if ctrl+c is pressed - we have to reraise exception to terminate proggy
282
+ if e.is_a?(Interrupt) && !( e.is_a?(Errno::ETIMEDOUT) || e.is_a?(Timeout::Error))
283
+ @logger.debug( "#{err_header} request to server #{@server} interrupted by ctrl-c")
284
+ @http = nil
285
+ raise
286
+ end
287
+ # oops - we got a banana: log it
288
+ error_add(e.message)
289
+ @logger.warn("#{err_header} request failure count: #{error_count}, exception: #{e.inspect}")
290
+ @http = nil
291
+ end
292
+ end
293
+ end
294
+
295
+ # Errors received during testing:
296
+ #
297
+ # #<Timeout::Error: execution expired>
298
+ # #<Errno::ETIMEDOUT: Connection timed out - connect(2)>
299
+ # #<SocketError: getaddrinfo: Name or service not known>
300
+ # #<SocketError: getaddrinfo: Temporary failure in name resolution>
301
+ # #<EOFError: end of file reached>
302
+ # #<Errno::ECONNRESET: Connection reset by peer>
303
+ # #<OpenSSL::SSL::SSLError: SSL_write:: bad write retry>
304
+ end
305
+
306
+ end