right_gogrid 0.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,4 @@
1
+ === 0.1.0 / 2009-02-20
2
+
3
+ - Initial (alpha) release
4
+
@@ -0,0 +1,9 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ lib/right_gogrid.rb
6
+ lib/gogrid_base.rb
7
+ lib/benchmark_fix.rb
8
+ lib/support.rb
9
+ test/test_right_gogrid.rb
@@ -0,0 +1,47 @@
1
+ = RightScale GoGrid API Ruby Gem
2
+
3
+ Published by RightScale, Inc. under the MIT License.
4
+ For information about RightScale, see http://www.rightscale.com
5
+
6
+ == DESCRIPTION:
7
+
8
+ The RightScale GoGrid gem has been designed to provide a robust interface to GoGrid's existing API.
9
+
10
+ == FEATURES/PROBLEMS:
11
+
12
+ - Full programmatic access to the GoGrid API.
13
+ - Complete error handling: all operations check for errors and report complete
14
+ error information by raising a GoGridError.
15
+ - Persistent HTTP connections with robust network-level retry layer using
16
+ RightHttpConnection). This includes socket timeouts and retries.
17
+ - Robust HTTP-level retry layer. Certain (user-adjustable) HTTP errors returned
18
+ by GoGrid are classified as temporary errors.
19
+ These errors are automaticallly retried using exponentially increasing intervals.
20
+ The number of retries is user-configurable.
21
+
22
+ == INSTALL:
23
+
24
+ sudo gem install right_gogrid
25
+
26
+ == LICENSE:
27
+
28
+ Copyright (c) 2007-2009 RightScale, Inc.
29
+
30
+ Permission is hereby granted, free of charge, to any person obtaining
31
+ a copy of this software and associated documentation files (the
32
+ 'Software'), to deal in the Software without restriction, including
33
+ without limitation the rights to use, copy, modify, merge, publish,
34
+ distribute, sublicense, and/or sell copies of the Software, and to
35
+ permit persons to whom the Software is furnished to do so, subject to
36
+ the following conditions:
37
+
38
+ The above copyright notice and this permission notice shall be
39
+ included in all copies or substantial portions of the Software.
40
+
41
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
42
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
43
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
44
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
45
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
46
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
47
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,41 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require "rake/testtask"
6
+ require 'rcov/rcovtask'
7
+ $: << File.dirname(__FILE__)
8
+ require './lib/right_gogrid.rb'
9
+
10
+ # Suppress Hoe's self-inclusion as a dependency for our Gem. This also keeps
11
+ # Rake & rubyforge out of the dependency list. Users must manually install
12
+ # these gems to run tests, etc.
13
+ # TRB 2/19/09: also do this for the extra_dev_deps array present in newer hoes.
14
+ # Older versions of RubyGems will try to install developer-dependencies as
15
+ # required runtime dependencies....
16
+ class Hoe
17
+ def extra_deps
18
+ @extra_deps.reject do |x|
19
+ Array(x).first == 'hoe'
20
+ end
21
+ end
22
+ def extra_dev_deps
23
+ @extra_dev_deps.reject do |x|
24
+ Array(x).first == 'hoe'
25
+ end
26
+ end
27
+ end
28
+
29
+ Hoe.new('right_gogrid', RightGogrid::VERSION) do |p|
30
+ p.rubyforge_name = 'rightscale'
31
+ p.author = 'RightScale, Inc.'
32
+ p.email = 'rubygems@rightscale.com'
33
+ p.summary = 'Interface classes for the GoGrid API'
34
+ p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
35
+ p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
36
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
37
+ p.remote_rdoc_dir = "/right_gogrid_gem_doc"
38
+ p.extra_deps = [['right_http_connection','>= 1.2.4']]
39
+ end
40
+
41
+ # vim: syntax=Ruby
@@ -0,0 +1,39 @@
1
+ #
2
+ # Copyright (c) 2007-2009 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #
23
+ #
24
+
25
+
26
+ # A hack because there's a bug in add! in Benchmark::Tms
27
+ module Benchmark #:nodoc:
28
+ class Tms #:nodoc:
29
+ def add!(&blk)
30
+ t = Benchmark::measure(&blk)
31
+ @utime = utime + t.utime
32
+ @stime = stime + t.stime
33
+ @cutime = cutime + t.cutime
34
+ @cstime = cstime + t.cstime
35
+ @real = real + t.real
36
+ self
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,493 @@
1
+ #
2
+ # Copyright (c) 2007-2009 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
+ module Rightscale
25
+
26
+ class GogridBenchmarkingBlock #:nodoc:
27
+ attr_accessor :parser, :service
28
+ def initialize
29
+ # Benchmark::Tms instance for service (Ec2, S3, or SQS) access benchmarking.
30
+ @service = Benchmark::Tms.new()
31
+ # Benchmark::Tms instance for parsing benchmarking.
32
+ @parser = Benchmark::Tms.new()
33
+ end
34
+ end
35
+
36
+ class GogridNoChange < RuntimeError #:nodoc:
37
+ end
38
+
39
+ class GogridJsonParser #:nodoc:
40
+ def parse(response)
41
+ json = ActiveSupport::JSON.decode(response.body)
42
+ raise GogridError.new("Unsuccessful JSON response: #{json.inspect}") unless json["status"] == "success"
43
+ json['list']
44
+ end
45
+ end
46
+
47
+ # Dummy parser - does nothing
48
+ # Returns the original response back
49
+ class GogridDummyParser # :nodoc:
50
+ def parse(response)
51
+ response
52
+ end
53
+ end
54
+
55
+ module RightGogridInterface
56
+ DEFAULT_GOGRID_URL = 'https://api.gogrid.com/api'
57
+ DEFAULT_VERSION = '1.0'
58
+ DEFAULT_FORMAT = 'json'
59
+
60
+ # If found in an error message returned by Gogrid, these phrases indicate a transient
61
+ # error. Transient errors are automatically retried with exponential back-off.
62
+ GOGRID_PROBLEMS = [ #'Forbidden',
63
+ 'internal service error',
64
+ 'is currently unavailable',
65
+ 'no response from',
66
+ 'Please try again',
67
+ 'InternalError',
68
+ 'ServiceUnavailable',
69
+ 'Unavailable',
70
+ 'This application is not currently available',
71
+ 'InsufficientInstanceCapacity'
72
+ ]
73
+ # TODO: gather more Gogrid errors here
74
+ @@gogrid_problems = GOGRID_PROBLEMS
75
+ # Returns a list of Gogrid responses which are known to be transient problems.
76
+ # We have to re-request if we get any of them, because the problem will probably disappear.
77
+ # By default this method returns the same value as the GOGRID_PROBLEMS const.
78
+ def self.gogrid_problems
79
+ @@gogrid_problems
80
+ end
81
+
82
+ @@caching = false
83
+ def self.caching
84
+ @@caching
85
+ end
86
+ def self.caching=(caching)
87
+ @@caching = caching
88
+ end
89
+
90
+ @@bench = GogridBenchmarkingBlock.new
91
+ def self.bench_parser
92
+ @@bench.parser
93
+ end
94
+ def self.bench_gogrid
95
+ @@bench.service
96
+ end
97
+
98
+ # Current Gogrid API key
99
+ attr_reader :gogrid_api_key
100
+ # Current Gogrid secret key
101
+ attr_reader :gogrid_secret
102
+ # Last HTTP request object
103
+ attr_reader :last_request
104
+ # Last HTTP response object
105
+ attr_reader :last_response
106
+ # Last Gogrid errors list (used by GogridErrorHandler)
107
+ attr_accessor :last_errors
108
+ # Logger object
109
+ attr_accessor :logger
110
+ # Initial params hash
111
+ attr_accessor :params
112
+ # RightHttpConnection instance
113
+ attr_reader :connection
114
+ # Cache
115
+ attr_reader :cache
116
+
117
+
118
+ #
119
+ # Params:
120
+ # :gogrid_url
121
+ # :logger
122
+ # :multi_thread
123
+ #
124
+ def init(gogrid_api_key, gogrid_secret, params={}) #:nodoc:
125
+ @params = params
126
+ @cache = {}
127
+ @error_handler = nil
128
+ # deny working without credentials
129
+ if gogrid_api_key.blank? || gogrid_secret.blank?
130
+ raise GogridError.new("GoGrid api and secret keys are required to operate on GoGrid API service")
131
+ end
132
+ @gogrid_api_key = gogrid_api_key
133
+ @gogrid_secret = gogrid_secret
134
+ # parse Gogrid URL
135
+ @params[:gogrid_url] ||= ENV['GOGRID_URL'] || DEFAULT_GOGRID_URL
136
+ @params[:server] = URI.parse(@params[:gogrid_url]).host
137
+ @params[:port] = URI.parse(@params[:gogrid_url]).port
138
+ @params[:service] = URI.parse(@params[:gogrid_url]).path
139
+ @params[:protocol] = URI.parse(@params[:gogrid_url]).scheme
140
+ # other params
141
+ @params[:multi_thread] ||= defined?(GOGRID_DAEMON)
142
+ @logger = @params[:logger] || (defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER) || Logger.new(STDOUT)
143
+ @logger.info "New #{self.class.name} using #{@params[:multi_thread] ? 'multi' : 'single'}-threaded mode"
144
+ end
145
+
146
+ def on_exception(options={:raise=>true, :log=>true}) # :nodoc:
147
+ raise if $!.is_a?(GogridNoChange)
148
+ GogridError::on_gogrid_exception(self, options)
149
+ end
150
+
151
+ # --------
152
+ # Helpers
153
+ # --------
154
+
155
+ # Return +true+ if this instance works in multi_thread mode and +false+ otherwise.
156
+ def multi_thread
157
+ @params[:multi_thread]
158
+ end
159
+
160
+ def cgi_escape_params(params) # :nodoc:
161
+ params.map {|k,v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join("&")
162
+ end
163
+
164
+ def signature # :nodoc:
165
+ Digest::MD5.hexdigest("#{@gogrid_api_key}#{@gogrid_secret}#{'%.0f'%Time.new.to_f}")
166
+ end
167
+
168
+ # ----------------------------------
169
+ # request generation and processing
170
+ # ----------------------------------
171
+
172
+ def do_request(path, params={}, non_unique_params={}) # :nodoc:
173
+ request_hash = generate_request(path, params, non_unique_params)
174
+ # create a dafault response parser
175
+ case request_hash[:format]
176
+ when 'json' then parser = GogridJsonParser.new
177
+ # when 'xml'
178
+ # when 'csv'
179
+ else raise "Unsupported request format: #{params['format']}"
180
+ end
181
+ # perform a request
182
+ request_info(request_hash, parser)
183
+ end
184
+
185
+ # Generate a handy request hash.
186
+ def generate_request(path, params={}, non_unique_params={}) # :nodoc:
187
+ # default request params
188
+ params = { 'format' => DEFAULT_FORMAT,
189
+ 'v' => DEFAULT_VERSION,
190
+ 'sig' => signature,
191
+ 'api_key' => @gogrid_api_key}.merge(params)
192
+ # encode key/values
193
+ normal_params = cgi_escape_params(params)
194
+ # add the non_unique params at the end if we've received some
195
+ other_params = non_unique_params.collect do |i|
196
+ k = i.keys[0];
197
+ v = i.values[0];
198
+ "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}"
199
+ end.join("&")
200
+ other_params = "&#{other_params}" unless other_params.blank?
201
+ # create url and request
202
+ request_url = "#{@params[:service]}/#{path}?#{normal_params}#{other_params}"
203
+ request = Net::HTTP::Get.new(request_url)
204
+ # prepare output hash
205
+ { :request => request,
206
+ :server => @params[:server],
207
+ :port => @params[:port],
208
+ :protocol => @params[:protocol],
209
+ :format => params['format'] }
210
+ end
211
+
212
+ # Perform a request.
213
+ # (4xx and 5xx error handling is being made through GogridErrorHandler)
214
+ def request_info(request, parser) #:nodoc:
215
+ # check single/multi threading mode
216
+ thread = @params[:multi_thread] ? Thread.current : Thread.main
217
+ # create a connection if needed
218
+ thread[:ec2_connection] ||= Rightscale::HttpConnection.new(:exception => GogridError, :logger => @logger)
219
+ @connection = thread[:ec2_connection]
220
+ @last_request = request[:request]
221
+ @last_response = nil
222
+ # perform a request
223
+ @@bench.service.add!{ @last_response = @connection.request(request) }
224
+ # check response for success...
225
+ if @last_response.is_a?(Net::HTTPSuccess)
226
+ @error_handler = nil
227
+ result = nil
228
+ @@bench.parser.add! { result = parser.parse(@last_response) }
229
+ return result
230
+ else
231
+ @error_handler ||= GogridErrorHandler.new(self, parser, :errors_list => @@gogrid_problems)
232
+ check_result = @error_handler.check(request)
233
+ if check_result
234
+ @error_handler = nil
235
+ return check_result
236
+ end
237
+ raise GogridError.new(@last_errors, @last_response.code)
238
+ end
239
+ rescue
240
+ @error_handler = nil
241
+ raise
242
+ end
243
+
244
+ # --------
245
+ # Caching
246
+ # --------
247
+
248
+ # Perform a request.
249
+ # Skips a response parsing if caching is used.
250
+ def request_cache_or_info(method, request_hash, parser_class, use_cache=true) #:nodoc:
251
+ # We do not want to break the logic of parsing hence will use a dummy parser to process all the standard
252
+ # steps (errors checking etc). The dummy parser does nothig - just returns back the params it received.
253
+ # If the caching is enabled and hit then throw GogridNoChange.
254
+ # P.S. caching works for the whole images list only! (when the list param is blank)
255
+ response = request_info(request_hash, GogridDummyParser.new)
256
+ # check cache
257
+ cache_hits?(method.to_sym, response.body) if use_cache
258
+ result = nil
259
+ @@bench.parser.add!{ result = parser_class.new.parse(response) }
260
+ result = yield(result) if block_given?
261
+ # update parsed data
262
+ update_cache(method.to_sym, :parsed => result) if use_cache
263
+ result
264
+ end
265
+
266
+ # Returns +true+ if the describe_xxx responses are being cached
267
+ def caching?
268
+ @params.key?(:cache) ? @params[:cache] : @@caching
269
+ end
270
+
271
+ # Check if the gogrid function response hits the cache or not.
272
+ # If the cache hits:
273
+ # - raises an +GogridNoChange+ exception if +do_raise+ == +:raise+.
274
+ # - returnes parsed response from the cache if it exists or +true+ otherwise.
275
+ # If the cache miss or the caching is off then returns +false+.
276
+ def cache_hits?(function, response, do_raise=:raise) # :nodoc:
277
+ result = false
278
+ if caching?
279
+ function = function.to_sym
280
+ response_md5 = MD5.md5(response).to_s
281
+ # well, the response is new, reset cache data
282
+ unless @cache[function] && @cache[function][:response_md5] == response_md5
283
+ update_cache(function, {:response_md5 => response_md5,
284
+ :timestamp => Time.now,
285
+ :hits => 0,
286
+ :parsed => nil})
287
+ else
288
+ # aha, cache hits, update the data and throw an exception if needed
289
+ @cache[function][:hits] += 1
290
+ if do_raise == :raise
291
+ raise(GogridNoChange, "Cache hit: #{function} response has not changed since "+
292
+ "#{@cache[function][:timestamp].strftime('%Y-%m-%d %H:%M:%S')}, "+
293
+ "hits: #{@cache[function][:hits]}.")
294
+ else
295
+ result = @cache[function][:parsed] || true
296
+ end
297
+ end
298
+ end
299
+ result
300
+ end
301
+
302
+ def update_cache(function, hash) # :nodoc:
303
+ (@cache[function.to_sym] ||= {}).merge!(hash) if caching?
304
+ end
305
+ end
306
+
307
+
308
+ # Exception class to signal any GoGrid errors. All errors occuring during calls to GoGrid's
309
+ # web services raise this type of error.
310
+ # Attribute inherited by RuntimeError:
311
+ # message - the text of the error
312
+ class GogridError < RuntimeError # :nodoc:
313
+
314
+ # either an array of errors where each item is itself an array of [code, message]),
315
+ # or an error string if the error was raised manually, as in <tt>GogridError.new('err_text')</tt>
316
+ attr_reader :errors
317
+
318
+ # Response HTTP error code
319
+ attr_reader :http_code
320
+
321
+ def initialize(errors=nil, http_code=nil)
322
+ @errors = errors
323
+ @http_code = http_code
324
+ super(@errors.is_a?(Array) ? @errors.map{|code, msg| "#{code}: #{msg}"}.join("; ") : @errors.to_s)
325
+ end
326
+
327
+ # Does any of the error messages include the regexp +pattern+?
328
+ # Used to determine whether to retry request.
329
+ def include?(pattern)
330
+ if @errors.is_a?(Array)
331
+ @errors.each{ |code, msg| return true if code =~ pattern }
332
+ else
333
+ return true if @errors_str =~ pattern
334
+ end
335
+ false
336
+ end
337
+
338
+ # Generic handler for GogridErrors.
339
+ # object that caused the exception (it must provide last_request and last_response). Supported
340
+ # boolean options are:
341
+ # * <tt>:log</tt> print a message into the log using gogrid.logger to access the Logger
342
+ # * <tt>:puts</tt> do a "puts" of the error
343
+ # * <tt>:raise</tt> re-raise the error after logging
344
+ def self.on_gogrid_exception(gogrid, options={:raise=>true, :log=>true})
345
+ # Only log & notify if not user error
346
+ if !options[:raise] || system_error?($!)
347
+ error_text = "#{$!.inspect}\n#{$@}.join('\n')}"
348
+ puts error_text if options[:puts]
349
+ # Log the error
350
+ if options[:log]
351
+ request = gogrid.last_request ? gogrid.last_request.path : '-none-'
352
+ response = gogrid.last_response ? "#{gogrid.last_response.code} -- #{gogrid.last_response.message} -- #{gogrid.last_response.body}" : '-none-'
353
+ gogrid.logger.error error_text
354
+ gogrid.logger.error "Request was: #{request}"
355
+ gogrid.logger.error "Response was: #{response}"
356
+ end
357
+ end
358
+ raise if options[:raise] # re-raise an exception
359
+ return nil
360
+ end
361
+
362
+ # True if e is an system error, i.e. something that is for sure not the caller's fault.
363
+ # Used to force logging.
364
+ # TODO: Place Gogrid Errors here (present ones are AWS errors)
365
+ def self.system_error?(e)
366
+ !e.is_a?(self) || e.message =~ /InternalError|InsufficientInstanceCapacity|Unavailable/
367
+ end
368
+
369
+ end
370
+
371
+ class GogridErrorHandler # :nodoc:
372
+ # 0-100 (%)
373
+ DEFAULT_CLOSE_ON_4XX_PROBABILITY = 10
374
+
375
+ @@reiteration_start_delay = 0.2
376
+ def self.reiteration_start_delay
377
+ @@reiteration_start_delay
378
+ end
379
+ def self.reiteration_start_delay=(reiteration_start_delay)
380
+ @@reiteration_start_delay = reiteration_start_delay
381
+ end
382
+
383
+ @@reiteration_time = 5
384
+ def self.reiteration_time
385
+ @@reiteration_time
386
+ end
387
+ def self.reiteration_time=(reiteration_time)
388
+ @@reiteration_time = reiteration_time
389
+ end
390
+
391
+ @@close_on_error = true
392
+ def self.close_on_error
393
+ @@close_on_error
394
+ end
395
+ def self.close_on_error=(close_on_error)
396
+ @@close_on_error = close_on_error
397
+ end
398
+
399
+ @@close_on_4xx_probability = DEFAULT_CLOSE_ON_4XX_PROBABILITY
400
+ def self.close_on_4xx_probability
401
+ @@close_on_4xx_probability
402
+ end
403
+ def self.close_on_4xx_probability=(close_on_4xx_probability)
404
+ @@close_on_4xx_probability = close_on_4xx_probability
405
+ end
406
+
407
+ # params:
408
+ # :reiteration_time
409
+ # :errors_list
410
+ # :close_on_error = true | false
411
+ # :close_on_4xx_probability = 1-100
412
+ def initialize(gogrid, parser, params={}) #:nodoc:
413
+ @gogrid = gogrid
414
+ @parser = parser
415
+ @started_at = Time.now
416
+ @stop_at = @started_at + (params[:reiteration_time] || @@reiteration_time)
417
+ @errors_list = params[:errors_list] || []
418
+ @reiteration_delay = @@reiteration_start_delay
419
+ @retries = 0
420
+ # close current HTTP(S) connection on 5xx, errors from list and 4xx errors
421
+ @close_on_error = params[:close_on_error].nil? ? @@close_on_error : params[:close_on_error]
422
+ @close_on_4xx_probability = params[:close_on_4xx_probability] || @@close_on_4xx_probability
423
+ end
424
+
425
+ # Returns false if
426
+ def check(request) #:nodoc:
427
+ result = false
428
+ error_found = false
429
+ error_match = nil
430
+ last_errors_text = ''
431
+ response = @gogrid.last_response
432
+ # log error
433
+ request_text_data = "#{request[:server]}:#{request[:port]}#{request[:request].path}"
434
+ @gogrid.logger.warn("##### #{@gogrid.class.name} returned an error: #{response.code} #{response.message}\n#{response.body} #####")
435
+ @gogrid.logger.warn("##### #{@gogrid.class.name} request: #{request_text_data} ####")
436
+
437
+ @gogrid.last_errors = [[response.code, "#{response.message} (#{request_text_data})"]]
438
+ last_errors_text = response.message
439
+ # now - check the error
440
+ @errors_list.each do |error_to_find|
441
+ if last_errors_text[/#{error_to_find}/i]
442
+ error_found = true
443
+ error_match = error_to_find
444
+ @gogrid.logger.warn("##### Retry is needed, error pattern match: #{error_to_find} #####")
445
+ break
446
+ end
447
+ end
448
+ # check the time has gone from the first error come
449
+ if error_found
450
+ # Close the connection to the server and recreate a new one.
451
+ # It may have a chance that one server is a semi-down and reconnection
452
+ # will help us to connect to the other server
453
+ if @close_on_error
454
+ @gogrid.connection.finish "#{self.class.name}: error match to pattern '#{error_match}'"
455
+ end
456
+
457
+ if (Time.now < @stop_at)
458
+ @retries += 1
459
+
460
+ @gogrid.logger.warn("##### Retry ##{@retries} is being performed. Sleeping for #{@reiteration_delay} sec. Whole time: #{Time.now-@started_at} sec ####")
461
+ sleep @reiteration_delay
462
+ @reiteration_delay *= 2
463
+
464
+ # Always make sure that the fp is set to point to the beginning(?)
465
+ # of the File/IO. TODO: it assumes that offset is 0, which is bad.
466
+ if(request[:request].body_stream && request[:request].body_stream.respond_to?(:pos))
467
+ begin
468
+ request[:request].body_stream.pos = 0
469
+ rescue Exception => e
470
+ @logger.warn("Retry may fail due to unable to reset the file pointer" +
471
+ " -- #{self.class.name} : #{e.inspect}")
472
+ end
473
+ end
474
+ result = @gogrid.request_info(request, @parser)
475
+ else
476
+ @gogrid.logger.warn("##### Ooops, time is over... ####")
477
+ end
478
+ # aha, this is unhandled error:
479
+ elsif @close_on_error
480
+ # Is this a 5xx error ?
481
+ if @gogrid.last_response.code.to_s[/^5\d\d$/]
482
+ @gogrid.connection.finish "#{self.class.name}: code: #{@gogrid.last_response.code}: '#{@gogrid.last_response.message}'"
483
+ # Is this a 4xx error ?
484
+ elsif @gogrid.last_response.code.to_s[/^4\d\d$/] && @close_on_4xx_probability > rand(100)
485
+ @gogrid.connection.finish "#{self.class.name}: code: #{@gogrid.last_response.code}: '#{@gogrid.last_response.message}', " +
486
+ "probability: #{@close_on_4xx_probability}%"
487
+ end
488
+ end
489
+ result
490
+ end
491
+ end
492
+
493
+ end