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.
- data/History.txt +4 -0
- data/Manifest.txt +9 -0
- data/README.txt +47 -0
- data/Rakefile +41 -0
- data/lib/benchmark_fix.rb +39 -0
- data/lib/gogrid_base.rb +493 -0
- data/lib/right_gogrid.rb +834 -0
- data/lib/support.rb +111 -0
- data/test/test_credentials.rb +41 -0
- data/test/test_right_gogrid.rb +103 -0
- metadata +74 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
data/README.txt
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/lib/gogrid_base.rb
ADDED
@@ -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
|