qoobaa-aws-sqs 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,36 @@
1
+ # Copyright (c) 2007-2008 RightScale Inc
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person
4
+ # obtaining a copy of this software and associated documentation files
5
+ # (the "Software"), to deal in the Software without restriction,
6
+ # including without limitation the rights to use, copy, modify, merge,
7
+ # publish, distribute, sublicense, and/or sell copies of the Software,
8
+ # and to permit persons to whom the Software is furnished to do so,
9
+ # subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
18
+ # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
19
+ # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ # A hack because there's a bug in add! in Benchmark::Tms
24
+ module Benchmark #:nodoc:
25
+ class Tms #:nodoc:
26
+ def add!(&blk)
27
+ t = Benchmark::measure(&blk)
28
+ @utime = utime + t.utime
29
+ @stime = stime + t.stime
30
+ @cutime = cutime + t.cutime
31
+ @cstime = cstime + t.cstime
32
+ @real = real + t.real
33
+ self
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,418 @@
1
+ # Copyright (c) 2007-2008 RightScale Inc
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person
4
+ # obtaining a copy of this software and associated documentation files
5
+ # (the "Software"), to deal in the Software without restriction,
6
+ # including without limitation the rights to use, copy, modify, merge,
7
+ # publish, distribute, sublicense, and/or sell copies of the Software,
8
+ # and to permit persons to whom the Software is furnished to do so,
9
+ # subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
18
+ # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
19
+ # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ # module HttpConnection #:nodoc:
24
+ # module VERSION #:nodoc:
25
+ # MAJOR = 1
26
+ # MINOR = 2
27
+ # TINY = 4
28
+
29
+ # STRING = [MAJOR, MINOR, TINY].join('.')
30
+ # end
31
+ # end
32
+
33
+ module Aws
34
+ # HttpConnection maintains a persistent HTTP connection to a remote
35
+ # server. Each instance maintains its own unique connection to the
36
+ # HTTP server. HttpConnection makes a best effort to receive a
37
+ # proper HTTP response from the server, although it does not
38
+ # guarantee that this response contains a HTTP Success code.
39
+
40
+ # On low-level errors (TCP/IP errors) HttpConnection invokes a
41
+ # reconnect and retry algorithm. Note that although each
42
+ # HttpConnection object has its own connection to the HTTP server,
43
+ # error handling is shared across all connections to a server. For
44
+ # example, if there are three connections to www.somehttpserver.com,
45
+ # a timeout error on one of those connections will cause all three
46
+ # connections to break and reconnect. A connection will not break
47
+ # and reconnect, however, unless a request becomes active on it
48
+ # within a certain amount of time after the error (as specified by
49
+ # HTTP_CONNECTION_RETRY_DELAY). An idle connection will not break
50
+ # even if other connections to the same server experience errors.
51
+
52
+ # A HttpConnection will retry a request a certain number of times
53
+ # (as defined by HTTP_CONNNECTION_RETRY_COUNT). If all the retries
54
+ # fail, an exception is thrown and all HttpConnections associated
55
+ # with a server enter a probationary period defined by
56
+ # HTTP_CONNECTION_RETRY_DELAY. If the user makes a new request
57
+ # subsequent to entering probation, the request will fail
58
+ # immediately with the same exception thrown on probation entry.
59
+ # This is so that if the HTTP server has gone down, not every
60
+ # subsequent request must wait for a connect timeout before failing.
61
+ # After the probation period expires, the internal state of the
62
+ # HttpConnection is reset and subsequent requests have the full
63
+ # number of potential reconnects and retries available to them.
64
+
65
+ class HttpConnection
66
+
67
+ # Number of times to retry the request after encountering the first error
68
+ HTTP_CONNECTION_RETRY_COUNT = 3
69
+ # Throw a Timeout::Error if a connection isn't established within this number of seconds
70
+ HTTP_CONNECTION_OPEN_TIMEOUT = 5
71
+ # Throw a Timeout::Error if no data have been read on this connnection within this number of seconds
72
+ HTTP_CONNECTION_READ_TIMEOUT = 120
73
+ # Length of the post-error probationary period during which all requests will fail
74
+ HTTP_CONNECTION_RETRY_DELAY = 15
75
+
76
+ #--------------------
77
+ # class methods
78
+ #--------------------
79
+ #
80
+ @@params = {}
81
+ @@params[:http_connection_retry_count] = HTTP_CONNECTION_RETRY_COUNT
82
+ @@params[:http_connection_open_timeout] = HTTP_CONNECTION_OPEN_TIMEOUT
83
+ @@params[:http_connection_read_timeout] = HTTP_CONNECTION_READ_TIMEOUT
84
+ @@params[:http_connection_retry_delay] = HTTP_CONNECTION_RETRY_DELAY
85
+
86
+ # Query the global (class-level) parameters:
87
+ #
88
+ # :user_agent => 'www.HostName.com' # String to report as HTTP User agent
89
+ # :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.
90
+ # :logger => Logger object # If omitted, HttpConnection logs to STDOUT
91
+ # :exception => Exception to raise # The type of exception to raise
92
+ # # if a request repeatedly fails. RuntimeError is raised if this parameter is omitted.
93
+ # :http_connection_retry_count # by default == Aws::HttpConnection::HTTP_CONNECTION_RETRY_COUNT
94
+ # :http_connection_open_timeout # by default == Aws::HttpConnection::HTTP_CONNECTION_OPEN_TIMEOUT
95
+ # :http_connection_read_timeout # by default == Aws::HttpConnection::HTTP_CONNECTION_READ_TIMEOUT
96
+ # :http_connection_retry_delay # by default == Aws::HttpConnection::HTTP_CONNECTION_RETRY_DELAY
97
+ def self.params
98
+ @@params
99
+ end
100
+
101
+ # Set the global (class-level) parameters
102
+ def self.params=(params)
103
+ @@params = params
104
+ end
105
+
106
+ #------------------
107
+ # instance methods
108
+ #------------------
109
+ attr_accessor :http
110
+ attr_accessor :server
111
+ attr_accessor :params # see @@params
112
+ attr_accessor :logger
113
+
114
+ # Params hash:
115
+ # :user_agent => 'www.HostName.com' # String to report as HTTP User agent
116
+ # :ca_file => 'path_to_file' # A path of a CA certification file in PEM format. The file can contain several CA certificates.
117
+ # :logger => Logger object # If omitted, HttpConnection logs to STDOUT
118
+ # :exception => Exception to raise # The type of exception to raise if a request repeatedly fails. RuntimeError is raised if this parameter is omitted.
119
+ # :http_connection_retry_count # by default == Aws::HttpConnection.params[:http_connection_retry_count]
120
+ # :http_connection_open_timeout # by default == Aws::HttpConnection.params[:http_connection_open_timeout]
121
+ # :http_connection_read_timeout # by default == Aws::HttpConnection.params[:http_connection_read_timeout]
122
+ # :http_connection_retry_delay # by default == Aws::HttpConnection.params[:http_connection_retry_delay]
123
+ #
124
+ def initialize(params={})
125
+ @params = params
126
+ @params[:http_connection_retry_count] ||= @@params[:http_connection_retry_count]
127
+ @params[:http_connection_open_timeout] ||= @@params[:http_connection_open_timeout]
128
+ @params[:http_connection_read_timeout] ||= @@params[:http_connection_read_timeout]
129
+ @params[:http_connection_retry_delay] ||= @@params[:http_connection_retry_delay]
130
+ @http = nil
131
+ @server = nil
132
+ @logger = get_param(:logger) ||
133
+ (RAILS_DEFAULT_LOGGER if defined?(RAILS_DEFAULT_LOGGER)) ||
134
+ Logger.new(STDOUT)
135
+ end
136
+
137
+ def get_param(name)
138
+ @params[name] || @@params[name]
139
+ end
140
+
141
+ # Query for the maximum size (in bytes) of a single read from the underlying
142
+ # socket. For bulk transfer, especially over fast links, this is value is
143
+ # critical to performance.
144
+ def socket_read_size?
145
+ Net::BufferedIO.socket_read_size?
146
+ end
147
+
148
+ # Set the maximum size (in bytes) of a single read from the underlying
149
+ # socket. For bulk transfer, especially over fast links, this is value is
150
+ # critical to performance.
151
+ def socket_read_size=(newsize)
152
+ Net::BufferedIO.socket_read_size=(newsize)
153
+ end
154
+
155
+ # Query for the maximum size (in bytes) of a single read from local data
156
+ # sources like files. This is important, for example, in a streaming PUT of a
157
+ # large buffer.
158
+ def local_read_size?
159
+ Net::HTTPGenericRequest.local_read_size?
160
+ end
161
+
162
+ # Set the maximum size (in bytes) of a single read from local data
163
+ # sources like files. This can be used to tune the performance of, for example, a streaming PUT of a
164
+ # large buffer.
165
+ def local_read_size=(newsize)
166
+ Net::HTTPGenericRequest.local_read_size=(newsize)
167
+ end
168
+
169
+ private
170
+ #--------------
171
+ # Retry state - Keep track of errors on a per-server basis
172
+ #--------------
173
+ @@state = {} # retry state indexed by server: consecutive error count, error time, and error
174
+ @@eof = {}
175
+
176
+ # number of consecutive errors seen for server, 0 all is ok
177
+ def error_count
178
+ @@state[@server] ? @@state[@server][:count] : 0
179
+ end
180
+
181
+ # time of last error for server, nil if all is ok
182
+ def error_time
183
+ @@state[@server] && @@state[@server][:time]
184
+ end
185
+
186
+ # message for last error for server, "" if all is ok
187
+ def error_message
188
+ @@state[@server] ? @@state[@server][:message] : ""
189
+ end
190
+
191
+ # add an error for a server
192
+ def error_add(message)
193
+ @@state[@server] = { :count => error_count+1, :time => Time.now, :message => message }
194
+ end
195
+
196
+ # reset the error state for a server (i.e. a request succeeded)
197
+ def error_reset
198
+ @@state.delete(@server)
199
+ end
200
+
201
+ # Error message stuff...
202
+ def banana_message
203
+ return "#{@server} temporarily unavailable: (#{error_message})"
204
+ end
205
+
206
+ def err_header
207
+ return "#{self.class.name} :"
208
+ end
209
+
210
+ # Adds new EOF timestamp.
211
+ # Returns the number of seconds to wait before new conection retry:
212
+ # 0.5, 1, 2, 4, 8
213
+ def add_eof
214
+ (@@eof[@server] ||= []).unshift Time.now
215
+ 0.25 * 2 ** @@eof[@server].size
216
+ end
217
+
218
+ # Returns first EOF timestamp or nul if have no EOFs being tracked.
219
+ def eof_time
220
+ @@eof[@server] && @@eof[@server].last
221
+ end
222
+
223
+ # Returns true if we are receiving EOFs during last @params[:http_connection_retry_delay] seconds
224
+ # and there were no successful response from server
225
+ def raise_on_eof_exception?
226
+ @@eof[@server].blank? ? false : ( (Time.now.to_i-@params[:http_connection_retry_delay]) > @@eof[@server].last.to_i )
227
+ end
228
+
229
+ # Reset a list of EOFs for this server.
230
+ # This is being called when we have got an successful response from server.
231
+ def eof_reset
232
+ @@eof.delete(@server)
233
+ end
234
+
235
+ # Detects if an object is 'streamable' - can we read from it, and can we know the size?
236
+ def setup_streaming(request)
237
+ if(request.body && request.body.respond_to?(:read))
238
+ body = request.body
239
+ request.content_length = body.respond_to?(:lstat) ? body.lstat.size : body.size
240
+ request.body_stream = request.body
241
+ true
242
+ end
243
+ end
244
+
245
+ def get_fileptr_offset(request_params)
246
+ request_params[:request].body.pos
247
+ rescue Exception => e
248
+ # Probably caught this because the body doesn't support the pos() method, like if it is a socket.
249
+ # Just return 0 and get on with life.
250
+ 0
251
+ end
252
+
253
+ def reset_fileptr_offset(request, offset = 0)
254
+ if(request.body_stream && request.body_stream.respond_to?(:pos))
255
+ begin
256
+ request.body_stream.pos = offset
257
+ rescue Exception => e
258
+ @logger.warn("Failed file pointer reset; aborting HTTP retries." +
259
+ " -- #{err_header} #{e.inspect}")
260
+ raise e
261
+ end
262
+ end
263
+ end
264
+
265
+ # Start a fresh connection. The object closes any existing connection and
266
+ # opens a new one.
267
+ def start(request_params)
268
+ # close the previous if exists
269
+ finish
270
+ # create new connection
271
+ @server = request_params[:server]
272
+ @port = request_params[:port]
273
+ @protocol = request_params[:protocol]
274
+
275
+ @logger.info("Opening new #{@protocol.upcase} connection to #@server:#@port")
276
+ @http = Net::HTTP.new(@server, @port)
277
+ @http.open_timeout = @params[:http_connection_open_timeout]
278
+ @http.read_timeout = @params[:http_connection_read_timeout]
279
+
280
+ if @protocol == 'https'
281
+ verifyCallbackProc = Proc.new{ |ok, x509_store_ctx|
282
+ code = x509_store_ctx.error
283
+ msg = x509_store_ctx.error_string
284
+ #debugger
285
+ @logger.warn("##### #{@server} certificate verify failed: #{msg}") unless code == 0
286
+ true
287
+ }
288
+ @http.use_ssl = true
289
+ ca_file = get_param(:ca_file)
290
+ if ca_file
291
+ @http.verify_mode = OpenSSL::SSL::VERIFY_PEER
292
+ @http.verify_callback = verifyCallbackProc
293
+ @http.ca_file = ca_file
294
+ end
295
+ end
296
+ # open connection
297
+ @http.start
298
+ end
299
+
300
+ public
301
+
302
+ # Send HTTP request to server
303
+
304
+ # request_params hash:
305
+ # :server => 'www.HostName.com' # Hostname or IP address of HTTP server
306
+ # :port => '80' # Port of HTTP server
307
+ # :protocol => 'https' # http and https are supported on any port
308
+ # :request => 'requeststring' # Fully-formed HTTP request to make
309
+
310
+ # Raises RuntimeError, Interrupt, and params[:exception] (if specified in new).
311
+
312
+ def request(request_params, &block)
313
+ # We save the offset here so that if we need to retry, we can return the file pointer to its initial position
314
+ mypos = get_fileptr_offset(request_params)
315
+ loop do
316
+ # if we are inside a delay between retries: no requests this time!
317
+ if error_count > @params[:http_connection_retry_count] &&
318
+ error_time + @params[:http_connection_retry_delay] > Time.now
319
+ # store the message (otherwise it will be lost after error_reset and
320
+ # we will raise an exception with an empty text)
321
+ banana_message_text = banana_message
322
+ @logger.warn("#{err_header} re-raising same error: #{banana_message_text} " +
323
+ "-- error count: #{error_count}, error age: #{Time.now.to_i - error_time.to_i}")
324
+ exception = get_param(:exception) || RuntimeError
325
+ raise exception.new(banana_message_text)
326
+ end
327
+
328
+ # try to connect server(if connection does not exist) and get response data
329
+ begin
330
+ request_params[:protocol] ||= (request_params[:port] == 443 ? 'https' : 'http')
331
+
332
+ request = request_params[:request]
333
+ request['User-Agent'] = get_param(:user_agent) || ''
334
+
335
+ # (re)open connection to server if none exists or params has changed
336
+ unless @http &&
337
+ @http.started? &&
338
+ @server == request_params[:server] &&
339
+ @port == request_params[:port] &&
340
+ @protocol == request_params[:protocol]
341
+ start(request_params)
342
+ end
343
+
344
+ # Detect if the body is a streamable object like a file or socket. If so, stream that
345
+ # bad boy.
346
+ setup_streaming(request)
347
+ response = @http.request(request, &block)
348
+
349
+ error_reset
350
+ eof_reset
351
+ return response
352
+
353
+ # We treat EOF errors and the timeout/network errors differently. Both
354
+ # are tracked in different statistics blocks. Note below that EOF
355
+ # errors will sleep for a certain (exponentially increasing) period.
356
+ # Other errors don't sleep because there is already an inherent delay
357
+ # in them; connect and read timeouts (for example) have already
358
+ # 'slept'. It is still not clear which way we should treat errors
359
+ # like RST and resolution failures. For now, there is no additional
360
+ # delay for these errors although this may change in the future.
361
+
362
+ # EOFError means the server closed the connection on us.
363
+ rescue EOFError => e
364
+ @logger.debug("#{err_header} server #{@server} closed connection")
365
+ @http = nil
366
+
367
+ # if we have waited long enough - raise an exception...
368
+ if raise_on_eof_exception?
369
+ exception = get_param(:exception) || RuntimeError
370
+ @logger.warn("#{err_header} raising #{exception} due to permanent EOF being received from #{@server}, error age: #{Time.now.to_i - eof_time.to_i}")
371
+ raise exception.new("Permanent EOF is being received from #{@server}.")
372
+ else
373
+ # ... else just sleep a bit before new retry
374
+ sleep(add_eof)
375
+ # We will be retrying the request, so reset the file pointer
376
+ reset_fileptr_offset(request, mypos)
377
+ end
378
+ rescue Exception => e # See comment at bottom for the list of errors seen...
379
+ @http = nil
380
+ # if ctrl+c is pressed - we have to reraise exception to terminate proggy
381
+ if e.is_a?(Interrupt) && !( e.is_a?(Errno::ETIMEDOUT) || e.is_a?(Timeout::Error))
382
+ @logger.debug( "#{err_header} request to server #{@server} interrupted by ctrl-c")
383
+ raise
384
+ elsif e.is_a?(ArgumentError) && e.message.include?('wrong number of arguments (5 for 4)')
385
+ # seems our net_fix patch was overriden...
386
+ exception = get_param(:exception) || RuntimeError
387
+ raise exception.new('incompatible Net::HTTP monkey-patch')
388
+ end
389
+ # oops - we got a banana: log it
390
+ error_add(e.message)
391
+ @logger.warn("#{err_header} request failure count: #{error_count}, exception: #{e.inspect}")
392
+
393
+ # We will be retrying the request, so reset the file pointer
394
+ reset_fileptr_offset(request, mypos)
395
+
396
+ end
397
+ end
398
+ end
399
+
400
+ def finish(reason = '')
401
+ if @http && @http.started?
402
+ reason = ", reason: '#{reason}'" unless reason.blank?
403
+ @logger.info("Closing #{@http.use_ssl? ? 'HTTPS' : 'HTTP'} connection to #{@http.address}:#{@http.port}#{reason}")
404
+ @http.finish
405
+ end
406
+ end
407
+
408
+ # Errors received during testing:
409
+ #
410
+ # #<Timeout::Error: execution expired>
411
+ # #<Errno::ETIMEDOUT: Connection timed out - connect(2)>
412
+ # #<SocketError: getaddrinfo: Name or service not known>
413
+ # #<SocketError: getaddrinfo: Temporary failure in name resolution>
414
+ # #<EOFError: end of file reached>
415
+ # #<Errno::ECONNRESET: Connection reset by peer>
416
+ # #<OpenSSL::SSL::SSLError: SSL_write:: bad write retry>
417
+ end
418
+ end