right_http_connection 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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