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,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ test/*
5
+ LICENSE
@@ -0,0 +1,6 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+ test/testcredentials.rb
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Jakub Kuźma
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ 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 BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,7 @@
1
+ = AWS SQS
2
+
3
+ SQS library extracted from RightScale AWS.
4
+
5
+ == Copyright
6
+
7
+ Copyright (c) 2009 Jakub Kuźma. See LICENSE for details.
@@ -0,0 +1,58 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'rubygems'
4
+ require 'rake'
5
+
6
+ begin
7
+ require 'jeweler'
8
+ Jeweler::Tasks.new do |gem|
9
+ gem.name = "aws-sqs"
10
+ gem.summary = %Q{AWS SQS}
11
+ gem.email = "qoobaa@gmail.com"
12
+ gem.homepage = "http://github.com/qoobaa/aws-sqs"
13
+ gem.authors = ["Jakub Kuźma"]
14
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
+ end
16
+
17
+ rescue LoadError
18
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
19
+ end
20
+
21
+ require 'rake/testtask'
22
+ Rake::TestTask.new(:test) do |test|
23
+ test.libs << 'lib' << 'test'
24
+ test.pattern = 'test/**/*_test.rb'
25
+ test.verbose = true
26
+ end
27
+
28
+ begin
29
+ require 'rcov/rcovtask'
30
+ Rcov::RcovTask.new do |test|
31
+ test.libs << 'test'
32
+ test.pattern = 'test/**/*_test.rb'
33
+ test.verbose = true
34
+ end
35
+ rescue LoadError
36
+ task :rcov do
37
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
38
+ end
39
+ end
40
+
41
+
42
+ task :default => :test
43
+
44
+ require 'rake/rdoctask'
45
+ Rake::RDocTask.new do |rdoc|
46
+ if File.exist?('VERSION.yml')
47
+ config = YAML.load(File.read('VERSION.yml'))
48
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
49
+ else
50
+ version = ""
51
+ end
52
+
53
+ rdoc.rdoc_dir = 'rdoc'
54
+ rdoc.title = "aws #{version}"
55
+ rdoc.rdoc_files.include('README*')
56
+ rdoc.rdoc_files.include('lib/**/*.rb')
57
+ end
58
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.1
@@ -0,0 +1,51 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{aws-sqs}
5
+ s.version = "0.1.1"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Jakub Kuźma"]
9
+ s.date = %q{2009-06-02}
10
+ s.email = %q{qoobaa@gmail.com}
11
+ s.extra_rdoc_files = [
12
+ "LICENSE",
13
+ "README.rdoc"
14
+ ]
15
+ s.files = [
16
+ ".document",
17
+ ".gitignore",
18
+ "LICENSE",
19
+ "README.rdoc",
20
+ "Rakefile",
21
+ "VERSION",
22
+ "aws-sqs.gemspec",
23
+ "lib/awsbase/awsbase.rb",
24
+ "lib/awsbase/benchmark_fix.rb",
25
+ "lib/awsbase/http_connection.rb",
26
+ "lib/awsbase/support.rb",
27
+ "lib/sqs/sqs.rb",
28
+ "lib/sqs/sqs_interface.rb",
29
+ "test/sqs_test.rb",
30
+ "test/test_helper.rb"
31
+ ]
32
+ s.homepage = %q{http://github.com/qoobaa/aws-sqs}
33
+ s.rdoc_options = ["--charset=UTF-8"]
34
+ s.require_paths = ["lib"]
35
+ s.rubygems_version = %q{1.3.4}
36
+ s.summary = %q{AWS SQS}
37
+ s.test_files = [
38
+ "test/sqs_test.rb",
39
+ "test/test_helper.rb"
40
+ ]
41
+
42
+ if s.respond_to? :specification_version then
43
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
44
+ s.specification_version = 3
45
+
46
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
47
+ else
48
+ end
49
+ else
50
+ end
51
+ end
@@ -0,0 +1,801 @@
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
+ # Test
25
+ module Aws
26
+ require 'digest/md5'
27
+ require 'pp'
28
+
29
+ class AwsUtils #:nodoc:
30
+ @@digest1 = OpenSSL::Digest::Digest.new("sha1")
31
+ @@digest256 = nil
32
+ if OpenSSL::OPENSSL_VERSION_NUMBER > 0x00908000
33
+ @@digest256 = OpenSSL::Digest::Digest.new("sha256") rescue nil # Some installation may not support sha256
34
+ end
35
+
36
+ def self.sign(aws_secret_access_key, auth_string)
37
+ Base64.encode64(OpenSSL::HMAC.digest(@@digest1, aws_secret_access_key, auth_string)).strip
38
+ end
39
+
40
+ # Escape a string accordingly Amazon rulles
41
+ # http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?REST_RESTAuth.html
42
+ def self.amz_escape(param)
43
+ param.to_s.gsub(/([^a-zA-Z0-9._~-]+)/n) do
44
+ '%' + $1.unpack('H2' * $1.size).join('%').upcase
45
+ end
46
+ end
47
+
48
+ # Set a timestamp and a signature version
49
+ def self.fix_service_params(service_hash, signature)
50
+ service_hash["Timestamp"] ||= Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.000Z") unless service_hash["Expires"]
51
+ service_hash["SignatureVersion"] = signature
52
+ service_hash
53
+ end
54
+
55
+ # Signature Version 0
56
+ # A deprecated guy (should work till septemper 2009)
57
+ def self.sign_request_v0(aws_secret_access_key, service_hash)
58
+ fix_service_params(service_hash, '0')
59
+ string_to_sign = "#{service_hash['Action']}#{service_hash['Timestamp'] || service_hash['Expires']}"
60
+ service_hash['Signature'] = AwsUtils::sign(aws_secret_access_key, string_to_sign)
61
+ service_hash.to_a.collect{|key,val| "#{amz_escape(key)}=#{amz_escape(val.to_s)}" }.join("&")
62
+ end
63
+
64
+ # Signature Version 1
65
+ # Another deprecated guy (should work till septemper 2009)
66
+ def self.sign_request_v1(aws_secret_access_key, service_hash)
67
+ fix_service_params(service_hash, '1')
68
+ string_to_sign = service_hash.sort{|a,b| (a[0].to_s.downcase)<=>(b[0].to_s.downcase)}.to_s
69
+ service_hash['Signature'] = AwsUtils::sign(aws_secret_access_key, string_to_sign)
70
+ service_hash.to_a.collect{|key,val| "#{amz_escape(key)}=#{amz_escape(val.to_s)}" }.join("&")
71
+ end
72
+
73
+ # Signature Version 2
74
+ # EC2, SQS and SDB requests must be signed by this guy.
75
+ # See: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?REST_RESTAuth.html
76
+ # http://developer.amazonwebservices.com/connect/entry.jspa?externalID=1928
77
+ def self.sign_request_v2(aws_secret_access_key, service_hash, http_verb, host, uri)
78
+ fix_service_params(service_hash, '2')
79
+ # select a signing method (make an old openssl working with sha1)
80
+ # make 'HmacSHA256' to be a default one
81
+ service_hash['SignatureMethod'] = 'HmacSHA256' unless ['HmacSHA256', 'HmacSHA1'].include?(service_hash['SignatureMethod'])
82
+ service_hash['SignatureMethod'] = 'HmacSHA1' unless @@digest256
83
+ # select a digest
84
+ digest = (service_hash['SignatureMethod'] == 'HmacSHA256' ? @@digest256 : @@digest1)
85
+ # form string to sign
86
+ canonical_string = service_hash.keys.sort.map do |key|
87
+ "#{amz_escape(key)}=#{amz_escape(service_hash[key])}"
88
+ end.join('&')
89
+ string_to_sign = "#{http_verb.to_s.upcase}\n#{host.downcase}\n#{uri}\n#{canonical_string}"
90
+ # sign the string
91
+ signature = amz_escape(Base64.encode64(OpenSSL::HMAC.digest(digest, aws_secret_access_key, string_to_sign)).strip)
92
+ "#{canonical_string}&Signature=#{signature}"
93
+ end
94
+
95
+ # From Amazon's SQS Dev Guide, a brief description of how to escape:
96
+ # "URL encode the computed signature and other query parameters as specified in
97
+ # RFC1738, section 2.2. In addition, because the + character is interpreted as a blank space
98
+ # by Sun Java classes that perform URL decoding, make sure to encode the + character
99
+ # although it is not required by RFC1738."
100
+ # Avoid using CGI::escape to escape URIs.
101
+ # CGI::escape will escape characters in the protocol, host, and port
102
+ # sections of the URI. Only target chars in the query
103
+ # string should be escaped.
104
+ def self.URLencode(raw)
105
+ e = URI.escape(raw)
106
+ e.gsub(/\+/, "%2b")
107
+ end
108
+
109
+ def self.allow_only(allowed_keys, params)
110
+ bogus_args = []
111
+ params.keys.each {|p| bogus_args.push(p) unless allowed_keys.include?(p) }
112
+ raise AwsError.new("The following arguments were given but are not legal for the function call #{caller_method}: #{bogus_args.inspect}") if bogus_args.length > 0
113
+ end
114
+
115
+ def self.mandatory_arguments(required_args, params)
116
+ rargs = required_args.dup
117
+ params.keys.each {|p| rargs.delete(p)}
118
+ raise AwsError.new("The following mandatory arguments were not provided to #{caller_method}: #{rargs.inspect}") if rargs.length > 0
119
+ end
120
+
121
+ def self.caller_method
122
+ caller[1]=~/`(.*?)'/
123
+ $1
124
+ end
125
+ end
126
+
127
+ class AwsBenchmarkingBlock #:nodoc:
128
+ attr_accessor :xml, :service
129
+ def initialize
130
+ # Benchmark::Tms instance for service (Ec2, S3, or SQS) access benchmarking.
131
+ @service = Benchmark::Tms.new()
132
+ # Benchmark::Tms instance for XML parsing benchmarking.
133
+ @xml = Benchmark::Tms.new()
134
+ end
135
+ end
136
+
137
+ class AwsNoChange < RuntimeError
138
+ end
139
+
140
+ class AwsBase
141
+ # Amazon HTTP Error handling
142
+
143
+ # Text, if found in an error message returned by AWS, indicates that this may be a transient
144
+ # error. Transient errors are automatically retried with exponential back-off.
145
+ AMAZON_PROBLEMS = [ 'internal service error',
146
+ 'is currently unavailable',
147
+ 'no response from',
148
+ 'Please try again',
149
+ 'InternalError',
150
+ 'ServiceUnavailable', #from SQS docs
151
+ 'Unavailable',
152
+ 'This application is not currently available',
153
+ 'InsufficientInstanceCapacity'
154
+ ]
155
+ @@amazon_problems = AMAZON_PROBLEMS
156
+ # Returns a list of Amazon service responses which are known to be transient problems.
157
+ # We have to re-request if we get any of them, because the problem will probably disappear.
158
+ # By default this method returns the same value as the AMAZON_PROBLEMS const.
159
+ def self.amazon_problems
160
+ @@amazon_problems
161
+ end
162
+
163
+ # Sets the list of Amazon side problems. Use in conjunction with the
164
+ # getter to append problems.
165
+ def self.amazon_problems=(problems_list)
166
+ @@amazon_problems = problems_list
167
+ end
168
+ end
169
+
170
+ module AwsBaseInterface
171
+ DEFAULT_SIGNATURE_VERSION = '2'
172
+
173
+ @@caching = false
174
+ def self.caching
175
+ @@caching
176
+ end
177
+ def self.caching=(caching)
178
+ @@caching = caching
179
+ end
180
+
181
+ # Current aws_access_key_id
182
+ attr_reader :aws_access_key_id
183
+ # Last HTTP request object
184
+ attr_reader :last_request
185
+ # Last HTTP response object
186
+ attr_reader :last_response
187
+ # Last AWS errors list (used by AWSErrorHandler)
188
+ attr_accessor :last_errors
189
+ # Last AWS request id (used by AWSErrorHandler)
190
+ attr_accessor :last_request_id
191
+ # Logger object
192
+ attr_accessor :logger
193
+ # Initial params hash
194
+ attr_accessor :params
195
+ # HttpConnection instance
196
+ attr_reader :connection
197
+ # Cache
198
+ attr_reader :cache
199
+ # Signature version (all services except s3)
200
+ attr_reader :signature_version
201
+
202
+ def init(service_info, aws_access_key_id, aws_secret_access_key, params={}) #:nodoc:
203
+ @params = params
204
+ raise AwsError.new("AWS access keys are required to operate on #{service_info[:name]}") \
205
+ if aws_access_key_id.blank? || aws_secret_access_key.blank?
206
+ @aws_access_key_id = aws_access_key_id
207
+ @aws_secret_access_key = aws_secret_access_key
208
+ # if the endpoint was explicitly defined - then use it
209
+ if @params[:endpoint_url]
210
+ @params[:server] = URI.parse(@params[:endpoint_url]).host
211
+ @params[:port] = URI.parse(@params[:endpoint_url]).port
212
+ @params[:service] = URI.parse(@params[:endpoint_url]).path
213
+ @params[:protocol] = URI.parse(@params[:endpoint_url]).scheme
214
+ @params[:region] = nil
215
+ else
216
+ @params[:server] ||= service_info[:default_host]
217
+ @params[:server] = "#{@params[:region]}.#{@params[:server]}" if @params[:region]
218
+ @params[:port] ||= service_info[:default_port]
219
+ @params[:service] ||= service_info[:default_service]
220
+ @params[:protocol] ||= service_info[:default_protocol]
221
+ end
222
+ if !@params[:multi_thread].nil? && @params[:connection_mode].nil? # user defined this
223
+ @params[:connection_mode] = @params[:multi_thread] ? :per_thread : :single
224
+ end
225
+ # @params[:multi_thread] ||= defined?(AWS_DAEMON)
226
+ @params[:connection_mode] ||= :default
227
+ @params[:connection_mode] = :per_request if @params[:connection_mode] == :default
228
+ @logger = @params[:logger]
229
+ @logger = RAILS_DEFAULT_LOGGER if !@logger && defined?(RAILS_DEFAULT_LOGGER)
230
+ @logger = Logger.new(STDOUT) if !@logger
231
+ @logger.info "New #{self.class.name} using #{@params[:connection_mode].to_s}-connection mode"
232
+ @error_handler = nil
233
+ @cache = {}
234
+ @signature_version = (params[:signature_version] || DEFAULT_SIGNATURE_VERSION).to_s
235
+ end
236
+
237
+ def signed_service_params(aws_secret_access_key, service_hash, http_verb=nil, host=nil, service=nil )
238
+ case signature_version.to_s
239
+ when '0'
240
+ AwsUtils::sign_request_v0(aws_secret_access_key, service_hash)
241
+ when '1'
242
+ AwsUtils::sign_request_v1(aws_secret_access_key, service_hash)
243
+ when '2'
244
+ AwsUtils::sign_request_v2(aws_secret_access_key, service_hash, http_verb, host, service)
245
+ else
246
+ raise AwsError.new("Unknown signature version (#{signature_version.to_s}) requested")
247
+ end
248
+ end
249
+
250
+ # Returns +true+ if the describe_xxx responses are being cached
251
+ def caching?
252
+ @params.key?(:cache) ? @params[:cache] : @@caching
253
+ end
254
+
255
+ # Check if the aws function response hits the cache or not.
256
+ # If the cache hits:
257
+ # - raises an +AwsNoChange+ exception if +do_raise+ == +:raise+.
258
+ # - returnes parsed response from the cache if it exists or +true+ otherwise.
259
+ # If the cache miss or the caching is off then returns +false+.
260
+ def cache_hits?(function, response, do_raise=:raise)
261
+ result = false
262
+ if caching?
263
+ function = function.to_sym
264
+ # get rid of requestId (this bad boy was added for API 2008-08-08+ and it is uniq for every response)
265
+ response = response.sub(%r{<requestId>.+?</requestId>}, '')
266
+ response_md5 =Digest::MD5.hexdigest(response).to_s
267
+ # check for changes
268
+ unless @cache[function] && @cache[function][:response_md5] == response_md5
269
+ # well, the response is new, reset cache data
270
+ update_cache(function, {:response_md5 => response_md5,
271
+ :timestamp => Time.now,
272
+ :hits => 0,
273
+ :parsed => nil})
274
+ else
275
+ # aha, cache hits, update the data and throw an exception if needed
276
+ @cache[function][:hits] += 1
277
+ if do_raise == :raise
278
+ raise(AwsNoChange, "Cache hit: #{function} response has not changed since "+
279
+ "#{@cache[function][:timestamp].strftime('%Y-%m-%d %H:%M:%S')}, "+
280
+ "hits: #{@cache[function][:hits]}.")
281
+ else
282
+ result = @cache[function][:parsed] || true
283
+ end
284
+ end
285
+ end
286
+ result
287
+ end
288
+
289
+ def update_cache(function, hash)
290
+ (@cache[function.to_sym] ||= {}).merge!(hash) if caching?
291
+ end
292
+
293
+ def on_exception(options={:raise=>true, :log=>true}) # :nodoc:
294
+ raise if $!.is_a?(AwsNoChange)
295
+ AwsError::on_aws_exception(self, options)
296
+ end
297
+
298
+ # Return +true+ if this instance works in multi_thread mode and +false+ otherwise.
299
+ def multi_thread
300
+ @params[:multi_thread]
301
+ end
302
+
303
+ def request_info_impl(connection, benchblock, request, parser, &block) #:nodoc:
304
+ @connection = connection
305
+ @last_request = request[:request]
306
+ @last_response = nil
307
+ response=nil
308
+ blockexception = nil
309
+
310
+ if(block != nil)
311
+ # TRB 9/17/07 Careful - because we are passing in blocks, we get a situation where
312
+ # an exception may get thrown in the block body (which is high-level
313
+ # code either here or in the application) but gets caught in the
314
+ # low-level code of HttpConnection. The solution is not to let any
315
+ # exception escape the block that we pass to HttpConnection::request.
316
+ # Exceptions can originate from code directly in the block, or from user
317
+ # code called in the other block which is passed to response.read_body.
318
+ benchblock.service.add! do
319
+ responsehdr = @connection.request(request) do |response|
320
+ #########
321
+ begin
322
+ @last_response = response
323
+ if response.is_a?(Net::HTTPSuccess)
324
+ @error_handler = nil
325
+ response.read_body(&block)
326
+ else
327
+ @error_handler = AWSErrorHandler.new(self, parser, :errors_list => self.class.amazon_problems) unless @error_handler
328
+ check_result = @error_handler.check(request)
329
+ if check_result
330
+ @error_handler = nil
331
+ return check_result
332
+ end
333
+ raise AwsError.new(@last_errors, @last_response.code, @last_request_id)
334
+ end
335
+ rescue Exception => e
336
+ blockexception = e
337
+ end
338
+ end
339
+ #########
340
+
341
+ #OK, now we are out of the block passed to the lower level
342
+ if(blockexception)
343
+ raise blockexception
344
+ end
345
+ benchblock.xml.add! do
346
+ parser.parse(responsehdr)
347
+ end
348
+ return parser.result
349
+ end
350
+ else
351
+ benchblock.service.add!{ response = @connection.request(request) }
352
+ # check response for errors...
353
+ @last_response = response
354
+ if response.is_a?(Net::HTTPSuccess)
355
+ @error_handler = nil
356
+ benchblock.xml.add! { parser.parse(response) }
357
+ return parser.result
358
+ else
359
+ @error_handler = AWSErrorHandler.new(self, parser, :errors_list => self.class.amazon_problems) unless @error_handler
360
+ check_result = @error_handler.check(request)
361
+ if check_result
362
+ @error_handler = nil
363
+ return check_result
364
+ end
365
+ raise AwsError.new(@last_errors, @last_response.code, @last_request_id)
366
+ end
367
+ end
368
+ rescue
369
+ @error_handler = nil
370
+ raise
371
+ end
372
+
373
+ def request_cache_or_info(method, link, parser_class, benchblock, use_cache=true) #:nodoc:
374
+ # We do not want to break the logic of parsing hence will use a dummy parser to process all the standard
375
+ # steps (errors checking etc). The dummy parser does nothig - just returns back the params it received.
376
+ # If the caching is enabled and hit then throw AwsNoChange.
377
+ # P.S. caching works for the whole images list only! (when the list param is blank)
378
+ # check cache
379
+ response, params = request_info(link, DummyParser.new)
380
+ cache_hits?(method.to_sym, response.body) if use_cache
381
+ parser = parser_class.new(:logger => @logger)
382
+ benchblock.xml.add!{ parser.parse(response, params) }
383
+ result = block_given? ? yield(parser) : parser.result
384
+ # update parsed data
385
+ update_cache(method.to_sym, :parsed => result) if use_cache
386
+ result
387
+ end
388
+
389
+ # Returns Amazons request ID for the latest request
390
+ def last_request_id
391
+ @last_response && @last_response.body.to_s[%r{<requestId>(.+?)</requestId>}] && $1
392
+ end
393
+
394
+ end
395
+
396
+
397
+ # Exception class to signal any Amazon errors. All errors occuring during calls to Amazon's
398
+ # web services raise this type of error.
399
+ # Attribute inherited by RuntimeError:
400
+ # message - the text of the error, generally as returned by AWS in its XML response.
401
+ class AwsError < RuntimeError
402
+
403
+ # either an array of errors where each item is itself an array of [code, message]),
404
+ # or an error string if the error was raised manually, as in <tt>AwsError.new('err_text')</tt>
405
+ attr_reader :errors
406
+
407
+ # Request id (if exists)
408
+ attr_reader :request_id
409
+
410
+ # Response HTTP error code
411
+ attr_reader :http_code
412
+
413
+ def initialize(errors=nil, http_code=nil, request_id=nil)
414
+ @errors = errors
415
+ @request_id = request_id
416
+ @http_code = http_code
417
+ super(@errors.is_a?(Array) ? @errors.map{|code, msg| "#{code}: #{msg}"}.join("; ") : @errors.to_s)
418
+ end
419
+
420
+ # Does any of the error messages include the regexp +pattern+?
421
+ # Used to determine whether to retry request.
422
+ def include?(pattern)
423
+ if @errors.is_a?(Array)
424
+ @errors.each{ |code, msg| return true if code =~ pattern }
425
+ else
426
+ return true if @errors_str =~ pattern
427
+ end
428
+ false
429
+ end
430
+
431
+ # Generic handler for AwsErrors. +aws+ is the Aws::S3, Aws::EC2, or Aws::SQS
432
+ # object that caused the exception (it must provide last_request and last_response). Supported
433
+ # boolean options are:
434
+ # * <tt>:log</tt> print a message into the log using aws.logger to access the Logger
435
+ # * <tt>:puts</tt> do a "puts" of the error
436
+ # * <tt>:raise</tt> re-raise the error after logging
437
+ def self.on_aws_exception(aws, options={:raise=>true, :log=>true})
438
+ # Only log & notify if not user error
439
+ if !options[:raise] || system_error?($!)
440
+ error_text = "#{$!.inspect}\n#{$@}.join('\n')}"
441
+ puts error_text if options[:puts]
442
+ # Log the error
443
+ if options[:log]
444
+ request = aws.last_request ? aws.last_request.path : '-none-'
445
+ response = aws.last_response ? "#{aws.last_response.code} -- #{aws.last_response.message} -- #{aws.last_response.body}" : '-none-'
446
+ aws.logger.error error_text
447
+ aws.logger.error "Request was: #{request}"
448
+ aws.logger.error "Response was: #{response}"
449
+ end
450
+ end
451
+ raise if options[:raise] # re-raise an exception
452
+ return nil
453
+ end
454
+
455
+ # True if e is an AWS system error, i.e. something that is for sure not the caller's fault.
456
+ # Used to force logging.
457
+ def self.system_error?(e)
458
+ !e.is_a?(self) || e.message =~ /InternalError|InsufficientInstanceCapacity|Unavailable/
459
+ end
460
+
461
+ end
462
+
463
+
464
+ class AWSErrorHandler
465
+ # 0-100 (%)
466
+ DEFAULT_CLOSE_ON_4XX_PROBABILITY = 10
467
+
468
+ @@reiteration_start_delay = 0.2
469
+ def self.reiteration_start_delay
470
+ @@reiteration_start_delay
471
+ end
472
+ def self.reiteration_start_delay=(reiteration_start_delay)
473
+ @@reiteration_start_delay = reiteration_start_delay
474
+ end
475
+
476
+ @@reiteration_time = 5
477
+ def self.reiteration_time
478
+ @@reiteration_time
479
+ end
480
+ def self.reiteration_time=(reiteration_time)
481
+ @@reiteration_time = reiteration_time
482
+ end
483
+
484
+ @@close_on_error = true
485
+ def self.close_on_error
486
+ @@close_on_error
487
+ end
488
+ def self.close_on_error=(close_on_error)
489
+ @@close_on_error = close_on_error
490
+ end
491
+
492
+ @@close_on_4xx_probability = DEFAULT_CLOSE_ON_4XX_PROBABILITY
493
+ def self.close_on_4xx_probability
494
+ @@close_on_4xx_probability
495
+ end
496
+ def self.close_on_4xx_probability=(close_on_4xx_probability)
497
+ @@close_on_4xx_probability = close_on_4xx_probability
498
+ end
499
+
500
+ # params:
501
+ # :reiteration_time
502
+ # :errors_list
503
+ # :close_on_error = true | false
504
+ # :close_on_4xx_probability = 1-100
505
+ def initialize(aws, parser, params={}) #:nodoc:
506
+ @aws = aws # Link to Ec2 | Sqs | S3 instance
507
+ @parser = parser # parser to parse Amazon response
508
+ @started_at = Time.now
509
+ @stop_at = @started_at + (params[:reiteration_time] || @@reiteration_time)
510
+ @errors_list = params[:errors_list] || []
511
+ @reiteration_delay = @@reiteration_start_delay
512
+ @retries = 0
513
+ # close current HTTP(S) connection on 5xx, errors from list and 4xx errors
514
+ @close_on_error = params[:close_on_error].nil? ? @@close_on_error : params[:close_on_error]
515
+ @close_on_4xx_probability = params[:close_on_4xx_probability] || @@close_on_4xx_probability
516
+ end
517
+
518
+ # Returns false if
519
+ def check(request) #:nodoc:
520
+ result = false
521
+ error_found = false
522
+ redirect_detected= false
523
+ error_match = nil
524
+ last_errors_text = ''
525
+ response = @aws.last_response
526
+ # log error
527
+ request_text_data = "#{request[:server]}:#{request[:port]}#{request[:request].path}"
528
+ # is this a redirect?
529
+ # yes!
530
+ if response.is_a?(Net::HTTPRedirection)
531
+ redirect_detected = true
532
+ else
533
+ # no, it's an error ...
534
+ @aws.logger.warn("##### #{@aws.class.name} returned an error: #{response.code} #{response.message}\n#{response.body} #####")
535
+ @aws.logger.warn("##### #{@aws.class.name} request: #{request_text_data} ####")
536
+ end
537
+ # Check response body: if it is an Amazon XML document or not:
538
+ if redirect_detected || (response.body && response.body[/<\?xml/]) # ... it is a xml document
539
+ @aws.class.bench_xml.add! do
540
+ error_parser = ErrorResponseParser.new
541
+ error_parser.parse(response)
542
+ @aws.last_errors = error_parser.errors
543
+ @aws.last_request_id = error_parser.requestID
544
+ last_errors_text = @aws.last_errors.flatten.join("\n")
545
+ # on redirect :
546
+ if redirect_detected
547
+ location = response['location']
548
+ # ... log information and ...
549
+ @aws.logger.info("##### #{@aws.class.name} redirect requested: #{response.code} #{response.message} #####")
550
+ @aws.logger.info("##### New location: #{location} #####")
551
+ # ... fix the connection data
552
+ request[:server] = URI.parse(location).host
553
+ request[:protocol] = URI.parse(location).scheme
554
+ request[:port] = URI.parse(location).port
555
+ end
556
+ end
557
+ else # ... it is not a xml document(probably just a html page?)
558
+ @aws.last_errors = [[response.code, "#{response.message} (#{request_text_data})"]]
559
+ @aws.last_request_id = '-undefined-'
560
+ last_errors_text = response.message
561
+ end
562
+ # now - check the error
563
+ unless redirect_detected
564
+ @errors_list.each do |error_to_find|
565
+ if last_errors_text[/#{error_to_find}/i]
566
+ error_found = true
567
+ error_match = error_to_find
568
+ @aws.logger.warn("##### Retry is needed, error pattern match: #{error_to_find} #####")
569
+ break
570
+ end
571
+ end
572
+ end
573
+ # check the time has gone from the first error come
574
+ if redirect_detected || error_found
575
+ # Close the connection to the server and recreate a new one.
576
+ # It may have a chance that one server is a semi-down and reconnection
577
+ # will help us to connect to the other server
578
+ if !redirect_detected && @close_on_error
579
+ @aws.connection.finish "#{self.class.name}: error match to pattern '#{error_match}'"
580
+ end
581
+
582
+ if (Time.now < @stop_at)
583
+ @retries += 1
584
+ unless redirect_detected
585
+ @aws.logger.warn("##### Retry ##{@retries} is being performed. Sleeping for #{@reiteration_delay} sec. Whole time: #{Time.now-@started_at} sec ####")
586
+ sleep @reiteration_delay
587
+ @reiteration_delay *= 2
588
+
589
+ # Always make sure that the fp is set to point to the beginning(?)
590
+ # of the File/IO. TODO: it assumes that offset is 0, which is bad.
591
+ if(request[:request].body_stream && request[:request].body_stream.respond_to?(:pos))
592
+ begin
593
+ request[:request].body_stream.pos = 0
594
+ rescue Exception => e
595
+ @logger.warn("Retry may fail due to unable to reset the file pointer" +
596
+ " -- #{self.class.name} : #{e.inspect}")
597
+ end
598
+ end
599
+ else
600
+ @aws.logger.info("##### Retry ##{@retries} is being performed due to a redirect. ####")
601
+ end
602
+ result = @aws.request_info(request, @parser)
603
+ else
604
+ @aws.logger.warn("##### Ooops, time is over... ####")
605
+ end
606
+ # aha, this is unhandled error:
607
+ elsif @close_on_error
608
+ # Is this a 5xx error ?
609
+ if @aws.last_response.code.to_s[/^5\d\d$/]
610
+ @aws.connection.finish "#{self.class.name}: code: #{@aws.last_response.code}: '#{@aws.last_response.message}'"
611
+ # Is this a 4xx error ?
612
+ elsif @aws.last_response.code.to_s[/^4\d\d$/] && @close_on_4xx_probability > rand(100)
613
+ @aws.connection.finish "#{self.class.name}: code: #{@aws.last_response.code}: '#{@aws.last_response.message}', " +
614
+ "probability: #{@close_on_4xx_probability}%"
615
+ end
616
+ end
617
+ result
618
+ end
619
+ end
620
+
621
+
622
+ #-----------------------------------------------------------------
623
+
624
+ class SaxParserCallback #:nodoc:
625
+ def self.include_callback
626
+ include XML::SaxParser::Callbacks
627
+ end
628
+ def initialize(right_aws_parser)
629
+ @right_aws_parser = right_aws_parser
630
+ end
631
+ def on_start_element(name, attr_hash)
632
+ @right_aws_parser.tag_start(name, attr_hash)
633
+ end
634
+ def on_characters(chars)
635
+ @right_aws_parser.text(chars)
636
+ end
637
+ def on_end_element(name)
638
+ @right_aws_parser.tag_end(name)
639
+ end
640
+ def on_start_document; end
641
+ def on_comment(msg); end
642
+ def on_processing_instruction(target, data); end
643
+ def on_cdata_block(cdata); end
644
+ def on_end_document; end
645
+ end
646
+
647
+ class AwsParser #:nodoc:
648
+ # default parsing library
649
+ DEFAULT_XML_LIBRARY = 'rexml'
650
+ # a list of supported parsers
651
+ @@supported_xml_libs = [DEFAULT_XML_LIBRARY, 'libxml']
652
+
653
+ @@xml_lib = DEFAULT_XML_LIBRARY # xml library name: 'rexml' | 'libxml'
654
+ def self.xml_lib
655
+ @@xml_lib
656
+ end
657
+ def self.xml_lib=(new_lib_name)
658
+ @@xml_lib = new_lib_name
659
+ end
660
+
661
+ attr_accessor :result
662
+ attr_reader :xmlpath
663
+ attr_accessor :xml_lib
664
+
665
+ def initialize(params={})
666
+ @xmlpath = ''
667
+ @result = false
668
+ @text = ''
669
+ @xml_lib = params[:xml_lib] || @@xml_lib
670
+ @logger = params[:logger]
671
+ reset
672
+ end
673
+ def tag_start(name, attributes)
674
+ @text = ''
675
+ tagstart(name, attributes)
676
+ @xmlpath += @xmlpath.empty? ? name : "/#{name}"
677
+ end
678
+ def tag_end(name)
679
+ if @xmlpath =~ /^(.*?)\/?#{name}$/
680
+ @xmlpath = $1
681
+ end
682
+ tagend(name)
683
+ end
684
+ def text(text)
685
+ @text += text
686
+ tagtext(text)
687
+ end
688
+ # Parser method.
689
+ # Params:
690
+ # xml_text - xml message text(String) or Net:HTTPxxx instance (response)
691
+ # params[:xml_lib] - library name: 'rexml' | 'libxml'
692
+ def parse(xml_text, params={})
693
+ # Get response body
694
+ xml_text = xml_text.body unless xml_text.is_a?(String)
695
+ @xml_lib = params[:xml_lib] || @xml_lib
696
+ # check that we had no problems with this library otherwise use default
697
+ @xml_lib = DEFAULT_XML_LIBRARY unless @@supported_xml_libs.include?(@xml_lib)
698
+ # load xml library
699
+ if @xml_lib=='libxml' && !defined?(XML::SaxParser)
700
+ begin
701
+ require 'xml/libxml'
702
+ # is it new ? - Setup SaxParserCallback
703
+ if XML::Parser::VERSION >= '0.5.1.0'
704
+ SaxParserCallback.include_callback
705
+ end
706
+ rescue LoadError => e
707
+ @@supported_xml_libs.delete(@xml_lib)
708
+ @xml_lib = DEFAULT_XML_LIBRARY
709
+ if @logger
710
+ @logger.error e.inspect
711
+ @logger.error e.backtrace
712
+ @logger.info "Can not load 'libxml' library. '#{DEFAULT_XML_LIBRARY}' is used for parsing."
713
+ end
714
+ end
715
+ end
716
+ # Parse the xml text
717
+ case @xml_lib
718
+ when 'libxml'
719
+ xml = XML::SaxParser.new
720
+ xml.string = xml_text
721
+ # check libxml-ruby version
722
+ if XML::Parser::VERSION >= '0.5.1.0'
723
+ xml.callbacks = SaxParserCallback.new(self)
724
+ else
725
+ xml.on_start_element{|name, attr_hash| self.tag_start(name, attr_hash)}
726
+ xml.on_characters{ |text| self.text(text)}
727
+ xml.on_end_element{ |name| self.tag_end(name)}
728
+ end
729
+ xml.parse
730
+ else
731
+ REXML::Document.parse_stream(xml_text, self)
732
+ end
733
+ end
734
+ # Parser must have a lots of methods
735
+ # (see /usr/lib/ruby/1.8/rexml/parsers/streamparser.rb)
736
+ # We dont need most of them in AwsParser and method_missing helps us
737
+ # to skip their definition
738
+ def method_missing(method, *params)
739
+ # if the method is one of known - just skip it ...
740
+ return if [:comment, :attlistdecl, :notationdecl, :elementdecl,
741
+ :entitydecl, :cdata, :xmldecl, :attlistdecl, :instruction,
742
+ :doctype].include?(method)
743
+ # ... else - call super to raise an exception
744
+ super(method, params)
745
+ end
746
+ # the functions to be overriden by children (if nessesery)
747
+ def reset ; end
748
+ def tagstart(name, attributes); end
749
+ def tagend(name) ; end
750
+ def tagtext(text) ; end
751
+ end
752
+
753
+ #-----------------------------------------------------------------
754
+ # PARSERS: Errors
755
+ #-----------------------------------------------------------------
756
+
757
+ #<Error>
758
+ # <Code>TemporaryRedirect</Code>
759
+ # <Message>Please re-send this request to the specified temporary endpoint. Continue to use the original request endpoint for future requests.</Message>
760
+ # <RequestId>FD8D5026D1C5ABA3</RequestId>
761
+ # <Endpoint>bucket-for-k.s3-external-3.amazonaws.com</Endpoint>
762
+ # <HostId>ItJy8xPFPli1fq/JR3DzQd3iDvFCRqi1LTRmunEdM1Uf6ZtW2r2kfGPWhRE1vtaU</HostId>
763
+ # <Bucket>bucket-for-k</Bucket>
764
+ #</Error>
765
+
766
+ class ErrorResponseParser < AwsParser #:nodoc:
767
+ attr_accessor :errors # array of hashes: error/message
768
+ attr_accessor :requestID
769
+ # attr_accessor :endpoint, :host_id, :bucket
770
+ def tagend(name)
771
+ case name
772
+ when 'RequestID' ; @requestID = @text
773
+ when 'Code' ; @code = @text
774
+ when 'Message' ; @message = @text
775
+ # when 'Endpoint' ; @endpoint = @text
776
+ # when 'HostId' ; @host_id = @text
777
+ # when 'Bucket' ; @bucket = @text
778
+ when 'Error' ; @errors << [ @code, @message ]
779
+ end
780
+ end
781
+ def reset
782
+ @errors = []
783
+ end
784
+ end
785
+
786
+ # Dummy parser - does nothing
787
+ # Returns the original params back
788
+ class DummyParser # :nodoc:
789
+ attr_accessor :result
790
+ def parse(response, params={})
791
+ @result = [response, params]
792
+ end
793
+ end
794
+
795
+ class Http2xxParser < AwsParser # :nodoc:
796
+ def parse(response)
797
+ @result = response.is_a?(Net::HTTPSuccess)
798
+ end
799
+ end
800
+ end
801
+