right_cloud_api_base 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.
- checksums.yaml +7 -0
- data/HISTORY +2 -0
- data/LICENSE +19 -0
- data/README.md +14 -0
- data/Rakefile +37 -0
- data/lib/base/api_manager.rb +707 -0
- data/lib/base/helpers/cloud_api_logger.rb +214 -0
- data/lib/base/helpers/http_headers.rb +239 -0
- data/lib/base/helpers/http_parent.rb +103 -0
- data/lib/base/helpers/http_request.rb +173 -0
- data/lib/base/helpers/http_response.rb +122 -0
- data/lib/base/helpers/net_http_patch.rb +31 -0
- data/lib/base/helpers/query_api_patterns.rb +862 -0
- data/lib/base/helpers/support.rb +270 -0
- data/lib/base/helpers/support.xml.rb +306 -0
- data/lib/base/helpers/utils.rb +380 -0
- data/lib/base/manager.rb +122 -0
- data/lib/base/parsers/json.rb +38 -0
- data/lib/base/parsers/plain.rb +36 -0
- data/lib/base/parsers/rexml.rb +83 -0
- data/lib/base/parsers/sax.rb +200 -0
- data/lib/base/routines/cache_validator.rb +184 -0
- data/lib/base/routines/connection_proxies/net_http_persistent_proxy.rb +194 -0
- data/lib/base/routines/connection_proxies/right_http_connection_proxy.rb +224 -0
- data/lib/base/routines/connection_proxy.rb +66 -0
- data/lib/base/routines/request_analyzer.rb +122 -0
- data/lib/base/routines/request_generator.rb +48 -0
- data/lib/base/routines/request_initializer.rb +52 -0
- data/lib/base/routines/response_analyzer.rb +152 -0
- data/lib/base/routines/response_parser.rb +79 -0
- data/lib/base/routines/result_wrapper.rb +75 -0
- data/lib/base/routines/retry_manager.rb +106 -0
- data/lib/base/routines/routine.rb +98 -0
- data/lib/right_cloud_api_base.rb +72 -0
- data/lib/right_cloud_api_base_version.rb +37 -0
- data/right_cloud_api_base.gemspec +63 -0
- data/spec/helpers/query_api_pattern_spec.rb +312 -0
- data/spec/helpers/support_spec.rb +211 -0
- data/spec/helpers/support_xml_spec.rb +207 -0
- data/spec/helpers/utils_spec.rb +179 -0
- data/spec/routines/connection_proxies/test_net_http_persistent_proxy_spec.rb +143 -0
- data/spec/routines/test_cache_validator_spec.rb +152 -0
- data/spec/routines/test_connection_proxy_spec.rb +44 -0
- data/spec/routines/test_request_analyzer_spec.rb +106 -0
- data/spec/routines/test_response_analyzer_spec.rb +132 -0
- data/spec/routines/test_response_parser_spec.rb +228 -0
- data/spec/routines/test_result_wrapper_spec.rb +63 -0
- data/spec/routines/test_retry_manager_spec.rb +84 -0
- data/spec/spec_helper.rb +15 -0
- metadata +215 -0
@@ -0,0 +1,224 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2013 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 NONINFRINGEMENT.
|
18
|
+
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
19
|
+
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
20
|
+
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
21
|
+
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
module RightScale
|
25
|
+
module CloudApi
|
26
|
+
class ConnectionProxy
|
27
|
+
|
28
|
+
class RightHttpConnectionProxy
|
29
|
+
@@storage = {}
|
30
|
+
|
31
|
+
def self.storage
|
32
|
+
@@storage
|
33
|
+
end
|
34
|
+
|
35
|
+
# Remove dead threads/fibers from the storage
|
36
|
+
def self.clean_storage
|
37
|
+
Utils::remove_dead_fibers_and_threads_from_storage(storage)
|
38
|
+
end
|
39
|
+
|
40
|
+
class Error < CloudApi::Error
|
41
|
+
end
|
42
|
+
|
43
|
+
# Performs an HTTP request.
|
44
|
+
#
|
45
|
+
# @param [Hash] data The API request +data+ storage.
|
46
|
+
# See {RightScale::CloudApi::ApiManager.initialize_api_request_options} code for its explanation.
|
47
|
+
#
|
48
|
+
def request(data)
|
49
|
+
require "right_http_connection"
|
50
|
+
|
51
|
+
@data = data
|
52
|
+
@data[:response] = {}
|
53
|
+
# Create a connection
|
54
|
+
@uri = @data[:connection][:uri].dup
|
55
|
+
|
56
|
+
# Create/Get RightHttpConnection instance
|
57
|
+
remote_endpoint = current_endpoint
|
58
|
+
right_http_connection = current_connection
|
59
|
+
|
60
|
+
# Register a callback to close current connection
|
61
|
+
@data[:callbacks][:close_current_connection] = Proc::new{|reason| close_connection(remote_endpoint, reason); log "Current connection closed: #{reason}" }
|
62
|
+
|
63
|
+
# Create a real HTTP request
|
64
|
+
fake = @data[:request][:instance]
|
65
|
+
http_request = "Net::HTTP::#{fake.verb._camelize}"._constantize::new(fake.path)
|
66
|
+
if fake.is_io?
|
67
|
+
http_request.body_stream = fake.body
|
68
|
+
else
|
69
|
+
http_request.body = fake.body
|
70
|
+
end
|
71
|
+
fake.headers.each{|header, value| http_request[header] = value }
|
72
|
+
fake.raw = http_request
|
73
|
+
|
74
|
+
# Set all the options are suported by RightHttpConnection (if they are)
|
75
|
+
http_connection_data = {
|
76
|
+
:server => @uri.host,
|
77
|
+
:port => @uri.port,
|
78
|
+
:protocol => @uri.scheme,
|
79
|
+
:request => http_request,
|
80
|
+
:exception => ConnectionError
|
81
|
+
}
|
82
|
+
|
83
|
+
# Set all required options
|
84
|
+
http_connection_data[:logger] = @data[:options][:cloud_api_logger].logger
|
85
|
+
http_connection_data[:user_agent] = @data[:options][:connection_user_agent] if @data[:options].has_key?(:connection_user_agent)
|
86
|
+
http_connection_data[:ca_file] = @data[:options][:connection_ca_file] if @data[:options].has_key?(:connection_ca_file)
|
87
|
+
http_connection_data[:http_connection_retry_count] = @data[:options][:connection_retry_count] if @data[:options].has_key?(:connection_retry_count)
|
88
|
+
http_connection_data[:http_connection_read_timeout] = @data[:options][:connection_read_timeout] if @data[:options].has_key?(:connection_read_timeout)
|
89
|
+
http_connection_data[:http_connection_open_timeout] = @data[:options][:connection_open_timeout] if @data[:options].has_key?(:connection_open_timeout)
|
90
|
+
http_connection_data[:http_connection_retry_delay] = @data[:options][:connection_retry_delay] if @data[:options].has_key?(:connection_retry_delay)
|
91
|
+
http_connection_data[:raise_on_timeout] = @data[:options][:abort_on_timeout] if @data[:options][:abort_on_timeout]
|
92
|
+
http_connection_data[:cert] = @data[:credentials][:cert] if @data[:credentials].has_key?(:cert)
|
93
|
+
http_connection_data[:key] = @data[:credentials][:key] if @data[:credentials].has_key?(:key)
|
94
|
+
|
95
|
+
#log "HttpConnection request: #{http_connection_data.inspect}"
|
96
|
+
|
97
|
+
# Make a request:
|
98
|
+
block = @data[:vars][:system][:block]
|
99
|
+
if block
|
100
|
+
# If block is given - pass there all the chunks of a response and stop
|
101
|
+
# (dont do any parsing, analysing etc)
|
102
|
+
#
|
103
|
+
# TRB 9/17/07 Careful - because we are passing in blocks, we get a situation where
|
104
|
+
# an exception may get thrown in the block body (which is high-level
|
105
|
+
# code either here or in the application) but gets caught in the
|
106
|
+
# low-level code of HttpConnection. The solution is not to let any
|
107
|
+
# exception escape the block that we pass to HttpConnection::request.
|
108
|
+
# Exceptions can originate from code directly in the block, or from user
|
109
|
+
# code called in the other block which is passed to response.read_body.
|
110
|
+
#
|
111
|
+
# TODO: the suggested fix for RightHttpConnection if to catch
|
112
|
+
# Interrupt and SystemCallError instead of Exception in line 402
|
113
|
+
response = nil
|
114
|
+
begin
|
115
|
+
block_exception = nil
|
116
|
+
# Response.body will be a Net::ReadAdapter instance here - it cant be read as a string.
|
117
|
+
# WEB: On its own, Net::HTTP causes response.body to be a Net::ReadAdapter when you make a request with a block
|
118
|
+
# that calls read_body on the response.
|
119
|
+
response = right_http_connection.request(http_connection_data) do |res|
|
120
|
+
begin
|
121
|
+
# Update temp response
|
122
|
+
@data[:response][:instance] = HTTPResponse::new( res.code,
|
123
|
+
nil,
|
124
|
+
res.to_hash,
|
125
|
+
res )
|
126
|
+
res.read_body(&block) if res.is_a?(Net::HTTPSuccess)
|
127
|
+
rescue Exception => e
|
128
|
+
block_exception = e
|
129
|
+
break
|
130
|
+
end
|
131
|
+
end
|
132
|
+
raise block_exception if block_exception
|
133
|
+
rescue Exception => e
|
134
|
+
right_http_connection.finish(e.message)
|
135
|
+
raise e
|
136
|
+
end
|
137
|
+
else
|
138
|
+
# Things are simple if there is no any block
|
139
|
+
response = right_http_connection.request(http_connection_data)
|
140
|
+
end
|
141
|
+
|
142
|
+
@data[:response][:instance] = HTTPResponse::new( response.code,
|
143
|
+
response.body,
|
144
|
+
response.to_hash,
|
145
|
+
response )
|
146
|
+
|
147
|
+
# # HACK: KD
|
148
|
+
# #
|
149
|
+
# # When one uploads a file with pos > 0 and 'content-length' != File.size - pos
|
150
|
+
# # then the next request through this connection fails with 400 or 505...
|
151
|
+
# # It seems that it expects the file to be read until EOF.
|
152
|
+
# #
|
153
|
+
# # KIlling the current connection seems to help but it is not good...
|
154
|
+
# #
|
155
|
+
# if @data[:request][:instance].body_stream #&& !@data[:request][:instance].body_stream.eof
|
156
|
+
# pp @data[:request][:instance].body_stream.pos
|
157
|
+
# log "closing current connection because of an issue when an IO object is not read until EOF"
|
158
|
+
# @connection.finish
|
159
|
+
# end
|
160
|
+
end
|
161
|
+
|
162
|
+
def log(message)
|
163
|
+
@data[:options][:cloud_api_logger].log(message, :connection_proxy)
|
164
|
+
end
|
165
|
+
|
166
|
+
#----------------------------
|
167
|
+
# HTTP Connections handling
|
168
|
+
#----------------------------
|
169
|
+
|
170
|
+
def storage # :nodoc:
|
171
|
+
@@storage[Utils::current_thread_and_fiber] ||= {}
|
172
|
+
end
|
173
|
+
|
174
|
+
def current_endpoint # :nodoc:
|
175
|
+
"#{@uri.scheme}://#{@uri.host}:#{@uri.port}"
|
176
|
+
end
|
177
|
+
|
178
|
+
def close_connection(endpoint, reason='') # :nodoc:
|
179
|
+
return nil unless storage[endpoint]
|
180
|
+
|
181
|
+
log "Destroying RightHttpConnection to #{endpoint}, reason: #{reason}"
|
182
|
+
storage[endpoint][:connection].finish(reason)
|
183
|
+
rescue => e
|
184
|
+
log "Exception in close_connection: #{e.class.name}: #{e.message}"
|
185
|
+
ensure
|
186
|
+
storage.delete(endpoint) if endpoint
|
187
|
+
end
|
188
|
+
|
189
|
+
INACTIVE_LIFETIME_LIMIT = 900 # seconds
|
190
|
+
|
191
|
+
# Delete out-of-dated connections for current Thread/Fiber
|
192
|
+
def clean_outdated_connections
|
193
|
+
life_time_scratch = Time::now - INACTIVE_LIFETIME_LIMIT
|
194
|
+
storage.each do |endpoint, connection|
|
195
|
+
if connection[:last_used_at] < life_time_scratch
|
196
|
+
close_connection(endpoint, 'out-of-date')
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Expire the connection if it has expired.
|
202
|
+
def current_connection # :nodoc:
|
203
|
+
# Remove dead threads/fibers from the storage
|
204
|
+
self.class::clean_storage
|
205
|
+
# Delete out-of-dated connections
|
206
|
+
clean_outdated_connections
|
207
|
+
# Get current_connection
|
208
|
+
endpoint = current_endpoint
|
209
|
+
unless storage[endpoint]
|
210
|
+
storage[endpoint] = {}
|
211
|
+
storage[endpoint][:connection] = Rightscale::HttpConnection.new( :exception => CloudError,
|
212
|
+
:logger => @data[:options][:cloud_api_logger].logger )
|
213
|
+
log "Creating RightHttpConection to #{endpoint.inspect}"
|
214
|
+
else
|
215
|
+
log "Reusing RightHttpConection to #{endpoint.inspect}"
|
216
|
+
end
|
217
|
+
storage[endpoint][:last_used_at] = Time::now
|
218
|
+
storage[endpoint][:connection]
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2013 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 NONINFRINGEMENT.
|
18
|
+
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
19
|
+
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
20
|
+
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
21
|
+
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
module RightScale
|
25
|
+
module CloudApi
|
26
|
+
|
27
|
+
# This routine generifies all HTTP requests so that the main code does not need to worry about
|
28
|
+
# the underlaying libraries (right_http_connection or persistent_connection).
|
29
|
+
#
|
30
|
+
class ConnectionProxy < Routine
|
31
|
+
class Error < CloudApi::Error
|
32
|
+
end
|
33
|
+
|
34
|
+
# Main entry point.
|
35
|
+
#
|
36
|
+
# Performs an HTTP request.
|
37
|
+
#
|
38
|
+
def process
|
39
|
+
unless @connection_proxy
|
40
|
+
# Try to use a user defined connection proxy. The options are:
|
41
|
+
# - RightScale::CloudApi::ConnectionProxy::RightHttpConnectionProxy,
|
42
|
+
# - RightScale::CloudApi::ConnectionProxy::NetHttpPersistentProxy
|
43
|
+
connection_proxy_class = data[:options][:connection_proxy]
|
44
|
+
unless connection_proxy_class
|
45
|
+
# If it is not defined then load right_http_connection gem and use it.
|
46
|
+
# connection_proxy_class = ConnectionProxy::RightHttpConnectionProxy
|
47
|
+
connection_proxy_class = RightScale::CloudApi::ConnectionProxy::NetHttpPersistentProxy
|
48
|
+
end
|
49
|
+
@connection_proxy = connection_proxy_class.new
|
50
|
+
end
|
51
|
+
|
52
|
+
# Register a call back to close current connection
|
53
|
+
data[:callbacks][:close_current_connection] = Proc::new do |reason|
|
54
|
+
@connection_proxy.close_connection(nil, reason)
|
55
|
+
cloud_api_logger.log("Current connection closed: #{reason}", :connection_proxy)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Make a request.
|
59
|
+
with_timer('HTTP request', :connection_proxy) do
|
60
|
+
@connection_proxy.request(data)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2013 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 NONINFRINGEMENT.
|
18
|
+
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
19
|
+
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
20
|
+
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
21
|
+
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
module RightScale
|
25
|
+
module CloudApi
|
26
|
+
|
27
|
+
# The routine provides *error_pattern* method that is used to define request and response patterns.
|
28
|
+
#
|
29
|
+
# The patterns allows one to control the request processing flow: you can enable retries for
|
30
|
+
# certain API calls or can re-open a connection on HTTP failure.
|
31
|
+
#
|
32
|
+
# The supported actions are:
|
33
|
+
#
|
34
|
+
# Request option:
|
35
|
+
# - :abort_on_timeout - If there was a low level timeout then it should not retry a request
|
36
|
+
# but should fail. Lets say one made a call to launch some instance and the remote cloud
|
37
|
+
# launched them but timed out to respond back. The default behavior for the gem is to make
|
38
|
+
# a retry if a timeout received but in this particular case it will launch the instances
|
39
|
+
# again. So the solution here is to make the system to fail after the first unsuccessfull
|
40
|
+
# request.
|
41
|
+
#
|
42
|
+
# Response options:
|
43
|
+
# - :retry - Make a retry if the failed response matches to the pattern.
|
44
|
+
# - :abort - Do not make a retry and fail if the response matches to the pattern (opposite to :retry).
|
45
|
+
# - :disconnect_and_abort - Close a connection and fail.
|
46
|
+
# - :reconnect_and_retry - Reestablish a connection and make a retry.
|
47
|
+
#
|
48
|
+
class RequestAnalyzer < Routine
|
49
|
+
|
50
|
+
class Error < CloudApi::Error
|
51
|
+
end
|
52
|
+
|
53
|
+
REQUEST_ACTIONS = [ :abort_on_timeout ]
|
54
|
+
REQUEST_KEYS = [ :verb, :verb!, :path, :path!, :request, :request!, :if ]
|
55
|
+
|
56
|
+
RESPONSE_ACTIONS = [ :disconnect_and_abort, :abort, :reconnect_and_retry, :retry ]
|
57
|
+
RESPONSE_KEYS = [ :verb, :verb!, :path, :path!, :request, :request!, :code, :code!, :response, :response!, :if ]
|
58
|
+
|
59
|
+
ALL_ACTIONS = REQUEST_ACTIONS + RESPONSE_ACTIONS
|
60
|
+
|
61
|
+
module ClassMethods
|
62
|
+
|
63
|
+
def self.extended(base)
|
64
|
+
unless base.respond_to?(:options) && base.options.is_a?(Hash)
|
65
|
+
fail Error::new("RequestAnalyzer routine assumes class being extended responds to :options and returns a hash")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Adds a new error pattern.
|
70
|
+
# Patterns are analyzed in order of their definition. If one pattern hits the rest are not analyzed.
|
71
|
+
#
|
72
|
+
# @param [Symbol] action The requested action.
|
73
|
+
# @param [Hash] error_pattern The requested pattern (see {file:lib/base/helper/utils.rb self.pattern_matches?}).
|
74
|
+
#
|
75
|
+
# @xample:
|
76
|
+
# error_pattern :abort_on_timeout, :path => /Action=(Run|Create)/
|
77
|
+
# error_pattern :retry, :response => /InternalError|Internal Server Error|internal service error/i
|
78
|
+
# error_pattern :disconnect_and_abort, :code => /5..|403|408/
|
79
|
+
# error_pattern :disconnect_and_abort, :code => /4../, :if => Proc.new{ |opts| rand(100) < 10 }
|
80
|
+
#
|
81
|
+
# @raise [RightScale::CloudApi::RequestAnalyzer::Error] If error_pattern is not a Hash instance.
|
82
|
+
# @raise [RightScale::CloudApi::RequestAnalyzer::Error] If action is not supported.
|
83
|
+
# @raise [RightScale::CloudApi::RequestAnalyzer::Error] If pattern keys are weird.
|
84
|
+
#
|
85
|
+
def error_pattern(action, error_pattern)
|
86
|
+
action = action.to_sym
|
87
|
+
fail Error::new("Patterns are not set for action #{action.inspect}") if !error_pattern.is_a?(Hash) || error_pattern._blank?
|
88
|
+
fail Error::new("Unsupported action #{action.inspect} for error pattern #{error_pattern.inspect}") unless ALL_ACTIONS.include?(action)
|
89
|
+
unsupported_keys = REQUEST_ACTIONS.include?(action) ? error_pattern.keys - REQUEST_KEYS : error_pattern.keys - RESPONSE_KEYS
|
90
|
+
fail Error::new("Unsupported keys #{unsupported_keys.inspect} for #{action.inspect} in error pattern #{error_pattern.inspect}") unless unsupported_keys._blank?
|
91
|
+
(options[:error_patterns] ||= []) << error_pattern.merge(:action => action)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# The main entry point.
|
96
|
+
#
|
97
|
+
def process
|
98
|
+
# Get a list of accessible error patterns
|
99
|
+
error_patterns = data[:options][:error_patterns] || []
|
100
|
+
opts = { :request => data[:request][:instance],
|
101
|
+
:response => nil,
|
102
|
+
:verb => data[:request][:verb],
|
103
|
+
:params => data[:request][:orig_params].dup}
|
104
|
+
# Walk through all the error patterns and find the first that matches.
|
105
|
+
# RequestAnalyser accepts only REQUEST_ACTIONS (actually "abort_on_timeout" only)
|
106
|
+
request_error_patterns = error_patterns.select{|e| REQUEST_ACTIONS.include?(e[:action])}
|
107
|
+
request_error_patterns.each do |pattern|
|
108
|
+
# If we see any pattern that matches our current state
|
109
|
+
if Utils::pattern_matches?(pattern, opts)
|
110
|
+
# then set a flag to disable retries
|
111
|
+
data[:options][:abort_on_timeout] = true
|
112
|
+
cloud_api_logger.log("Request matches to error pattern: #{pattern.inspect}" , :request_analyzer)
|
113
|
+
break
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
|
@@ -0,0 +1,48 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2013 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 NONINFRINGEMENT.
|
18
|
+
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
19
|
+
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
20
|
+
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
21
|
+
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
module RightScale
|
25
|
+
module CloudApi
|
26
|
+
|
27
|
+
# The routine generates a new HTTP request.
|
28
|
+
#
|
29
|
+
class RequestGenerator < Routine
|
30
|
+
|
31
|
+
# Generates an HTTP request instance.
|
32
|
+
#
|
33
|
+
# The request instance must be compatible to what ConnectionProxy is being used expects.
|
34
|
+
#
|
35
|
+
def process
|
36
|
+
request = HTTPRequest::new( data[:request][:verb],
|
37
|
+
data[:request][:path],
|
38
|
+
data[:request][:body],
|
39
|
+
data[:request][:headers] )
|
40
|
+
cloud_api_logger.log("Request generated: #{request.to_s}" , :request_generator)
|
41
|
+
cloud_api_logger.log("Request headers: #{request.headers_info}" , :request_generator)
|
42
|
+
cloud_api_logger.log("Request body: #{request.body_info}\n", :request_generator_body) unless (request.body.to_s.size == 0)
|
43
|
+
data[:request][:instance] = request
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2013 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 NONINFRINGEMENT.
|
18
|
+
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
19
|
+
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
20
|
+
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
21
|
+
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
module RightScale
|
25
|
+
module CloudApi
|
26
|
+
|
27
|
+
# The routine adds a random token to all the GET requests if the corresponding option is enabled.
|
28
|
+
#
|
29
|
+
class RequestInitializer < Routine
|
30
|
+
|
31
|
+
# Initializes things we may need to initialize.
|
32
|
+
#
|
33
|
+
# Sometimes we may need to add a random token for every request so that remote cloud. This may
|
34
|
+
# be needed when the cloud caches responses for similar requests. Lets say you listed instances
|
35
|
+
# then created one and then listed them again. S-me clouds (rackspace) may start to report the
|
36
|
+
# new seconds after it was created because of the caching they do.
|
37
|
+
#
|
38
|
+
# But if we mix something random onto every request then 2 consecutive list instances calls will
|
39
|
+
# look like they are different and the cloud wont return the cached data.
|
40
|
+
#
|
41
|
+
def process
|
42
|
+
# Add a random thing to every get request
|
43
|
+
if data[:request][:verb] == :get && !data[:options][:random_token]._blank?
|
44
|
+
random_token_name = 'rsrcarandomtoken'
|
45
|
+
random_token_name = data[:options][:random_token].to_s if [String, Symbol].include?(data[:options][:random_token].class)
|
46
|
+
data[:request][:params][random_token_name] = Utils::generate_token
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2013 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 NONINFRINGEMENT.
|
18
|
+
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
19
|
+
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
20
|
+
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
21
|
+
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
module RightScale
|
25
|
+
module CloudApi
|
26
|
+
|
27
|
+
# The routine analyzes HTTP responses and in the case of HTTP error it takes actions defined
|
28
|
+
# through *error_pattern* definitions.
|
29
|
+
#
|
30
|
+
class ResponseAnalyzer < Routine
|
31
|
+
|
32
|
+
class Error < CloudApi::Error
|
33
|
+
end
|
34
|
+
|
35
|
+
# Analyzes an HTTP response.
|
36
|
+
#
|
37
|
+
# In the case of 4xx, 5xx HTTP errors the method parses the response body to get the
|
38
|
+
# error message. Then it tries to find an error pattern that would match to the response.
|
39
|
+
# If the pattern found it takes the action (:retry, :abort, :disconnect_and_abort or
|
40
|
+
# :reconnect_and_retry) acordingly to the error patern. If the pattern not fount it just
|
41
|
+
# fails with RightScale::CloudApi::CloudError.
|
42
|
+
#
|
43
|
+
# In the case of 2xx code the method does nothing.
|
44
|
+
#
|
45
|
+
# In the case of any other unexpected HTTP code it fails with RightScale::CloudApi::CloudError.
|
46
|
+
#
|
47
|
+
# @example
|
48
|
+
# error_pattern :abort_on_timeout, :path => /Action=(Run|Create)/
|
49
|
+
# error_pattern :retry, :response => /InternalError|Internal Server Error|internal service error/i
|
50
|
+
# error_pattern :disconnect_and_abort, :code => /5..|403|408/
|
51
|
+
# error_pattern :reconnect_and_retry, :code => /4../, :if => Proc.new{ |opts| rand(100) < 10 }
|
52
|
+
#
|
53
|
+
def process
|
54
|
+
# Extract the current response and log it.
|
55
|
+
response = data[:response][:instance]
|
56
|
+
unless response.nil?
|
57
|
+
cloud_api_logger.log("Response received: #{response.to_s}", :response_analyzer)
|
58
|
+
cloud_api_logger.log("Response headers: #{response.headers_info}", :response_analyzer)
|
59
|
+
log_method = (response.is_error? || response.is_redirect?) ? :response_analyzer_body_error : :response_analyzer_body
|
60
|
+
cloud_api_logger.log("Response body: #{response.body_info}", log_method)
|
61
|
+
end
|
62
|
+
|
63
|
+
code = data[:response][:instance].code
|
64
|
+
body = data[:response][:instance].body
|
65
|
+
close_current_connection_proc = data[:callbacks][:close_current_connection]
|
66
|
+
|
67
|
+
# Analyze the response code.
|
68
|
+
case code
|
69
|
+
when /^(5..|4..)/
|
70
|
+
# Try to parse the received error message.
|
71
|
+
error_message = if data[:options][:response_error_parser]
|
72
|
+
parser = data[:options][:response_error_parser]
|
73
|
+
with_timer("Error parsing with #{parser}") do
|
74
|
+
parser::parse(data[:response][:instance], data[:options])
|
75
|
+
end
|
76
|
+
else
|
77
|
+
"#{code}: #{body.to_s}"
|
78
|
+
end
|
79
|
+
# Get the list of patterns.
|
80
|
+
error_patterns = data[:options][:error_patterns] || []
|
81
|
+
opts = { :request => data[:request][:instance],
|
82
|
+
:response => data[:response][:instance],
|
83
|
+
:verb => data[:request][:verb],
|
84
|
+
:params => data[:request][:orig_params].dup }
|
85
|
+
# Walk through all the patterns and find the first that matches.
|
86
|
+
error_patterns.each do |pattern|
|
87
|
+
if Utils::pattern_matches?(pattern, opts)
|
88
|
+
cloud_api_logger.log("Error code: #{code}, pattern match: #{pattern.inspect}", :response_analyzer, :warn)
|
89
|
+
# Take the required action.
|
90
|
+
case pattern[:action]
|
91
|
+
when :disconnect_and_abort
|
92
|
+
close_current_connection_proc && close_current_connection_proc.call("Error code: #{code}")
|
93
|
+
fail(HttpError::new(code, error_message))
|
94
|
+
when :reconnect_and_retry
|
95
|
+
close_current_connection_proc && close_current_connection_proc.call("Error code: #{code}")
|
96
|
+
@data[:vars][:retry][:http] = { :code => code, :message => error_message }
|
97
|
+
fail(RetryAttempt::new)
|
98
|
+
when :abort
|
99
|
+
fail(HttpError::new(code, error_message))
|
100
|
+
when :retry
|
101
|
+
invoke_callback_method(data[:options][:before_retry_callback],
|
102
|
+
:routine => self,
|
103
|
+
:pattern => pattern,
|
104
|
+
:opts => opts)
|
105
|
+
@data[:vars][:retry][:http] = { :code => code, :message => error_message }
|
106
|
+
fail(RetryAttempt::new)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
# The default behavior: this guy hits when there is no any matching pattern
|
111
|
+
fail(HttpError::new(code, error_message))
|
112
|
+
when /^3..$/
|
113
|
+
# In the case of redirect: update a request URI and retry
|
114
|
+
location = Array(data[:response][:instance].headers['location']).first
|
115
|
+
# ----- AMAZON HACK BEGIN ----------------------------------------------------------
|
116
|
+
# Amazon sometimes hide a location host into a response body.
|
117
|
+
if location._blank? && body && body[/<Endpoint>(.*?)<\/Endpoint>/] && $1
|
118
|
+
data[:connection][:uri].host = $1
|
119
|
+
location = data[:connection][:uri].to_s
|
120
|
+
end
|
121
|
+
# ----- AMAZON HACK END ------------------------------------------------------------
|
122
|
+
# Replace URI and retry if the location was successfully set
|
123
|
+
unless location._blank?
|
124
|
+
data[:connection][:uri] = ::URI.parse(location)
|
125
|
+
old_request = data[:request].delete(:instance)
|
126
|
+
data[:request].delete(:path)
|
127
|
+
cloud_api_logger.log("Redirect detected: #{location.inspect}", :response_analyzer)
|
128
|
+
invoke_callback_method(data[:options][:before_redirect_callback],
|
129
|
+
:routine => self,
|
130
|
+
:old_request => old_request,
|
131
|
+
:location => location)
|
132
|
+
raise(RetryAttempt::new)
|
133
|
+
else
|
134
|
+
# ----- OPENSTACK BEGIN ----------------------------------------------------------
|
135
|
+
# some OS services like Glance returns a list of supported api versions with status 300
|
136
|
+
# if there is at least one href in the body we need to further analize it in the OS manager
|
137
|
+
return true if body && body[/href/]
|
138
|
+
# ----- OPENSTACK END ----------------------------------------------------------
|
139
|
+
raise HttpError::new(code, "Cannot parse a redirect location")
|
140
|
+
end
|
141
|
+
when /^2../
|
142
|
+
# There is nothing to do on 2xx code
|
143
|
+
return true
|
144
|
+
else
|
145
|
+
fail(Error::new("Unexpected response code: #{code.inspect}"))
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|