right_agent 1.0.1 → 2.0.7
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/README.rdoc +10 -8
- data/Rakefile +31 -5
- data/lib/right_agent.rb +6 -1
- data/lib/right_agent/actor.rb +4 -20
- data/lib/right_agent/actors/agent_manager.rb +1 -1
- data/lib/right_agent/agent.rb +357 -144
- data/lib/right_agent/agent_config.rb +7 -6
- data/lib/right_agent/agent_identity.rb +13 -11
- data/lib/right_agent/agent_tag_manager.rb +60 -64
- data/{spec/results_mock.rb → lib/right_agent/clients.rb} +10 -24
- data/lib/right_agent/clients/api_client.rb +383 -0
- data/lib/right_agent/clients/auth_client.rb +247 -0
- data/lib/right_agent/clients/balanced_http_client.rb +369 -0
- data/lib/right_agent/clients/base_retry_client.rb +495 -0
- data/lib/right_agent/clients/right_http_client.rb +279 -0
- data/lib/right_agent/clients/router_client.rb +493 -0
- data/lib/right_agent/command/command_io.rb +4 -4
- data/lib/right_agent/command/command_parser.rb +2 -2
- data/lib/right_agent/command/command_runner.rb +1 -1
- data/lib/right_agent/connectivity_checker.rb +179 -0
- data/lib/right_agent/core_payload_types/secure_document_location.rb +2 -2
- data/lib/right_agent/dispatcher.rb +12 -10
- data/lib/right_agent/enrollment_result.rb +16 -12
- data/lib/right_agent/exceptions.rb +34 -20
- data/lib/right_agent/history.rb +10 -5
- data/lib/right_agent/log.rb +5 -5
- data/lib/right_agent/minimal.rb +1 -0
- data/lib/right_agent/multiplexer.rb +1 -1
- data/lib/right_agent/offline_handler.rb +270 -0
- data/lib/right_agent/packets.rb +7 -7
- data/lib/right_agent/payload_formatter.rb +1 -1
- data/lib/right_agent/pending_requests.rb +128 -0
- data/lib/right_agent/platform.rb +1 -1
- data/lib/right_agent/protocol_version_mixin.rb +69 -0
- data/lib/right_agent/{idempotent_request.rb → retryable_request.rb} +7 -7
- data/lib/right_agent/scripts/agent_controller.rb +28 -26
- data/lib/right_agent/scripts/agent_deployer.rb +37 -22
- data/lib/right_agent/scripts/common_parser.rb +10 -3
- data/lib/right_agent/secure_identity.rb +1 -1
- data/lib/right_agent/sender.rb +299 -785
- data/lib/right_agent/serialize/secure_serializer.rb +3 -1
- data/lib/right_agent/serialize/secure_serializer_initializer.rb +2 -2
- data/lib/right_agent/serialize/serializable.rb +8 -3
- data/right_agent.gemspec +49 -18
- data/spec/agent_config_spec.rb +7 -7
- data/spec/agent_identity_spec.rb +7 -4
- data/spec/agent_spec.rb +43 -7
- data/spec/agent_tag_manager_spec.rb +72 -83
- data/spec/clients/api_client_spec.rb +423 -0
- data/spec/clients/auth_client_spec.rb +272 -0
- data/spec/clients/balanced_http_client_spec.rb +576 -0
- data/spec/clients/base_retry_client_spec.rb +635 -0
- data/spec/clients/router_client_spec.rb +594 -0
- data/spec/clients/spec_helper.rb +111 -0
- data/spec/command/command_io_spec.rb +1 -1
- data/spec/command/command_parser_spec.rb +1 -1
- data/spec/connectivity_checker_spec.rb +83 -0
- data/spec/dispatcher_spec.rb +3 -2
- data/spec/enrollment_result_spec.rb +2 -2
- data/spec/history_spec.rb +51 -39
- data/spec/offline_handler_spec.rb +340 -0
- data/spec/pending_requests_spec.rb +136 -0
- data/spec/{idempotent_request_spec.rb → retryable_request_spec.rb} +73 -73
- data/spec/sender_spec.rb +835 -1052
- data/spec/serialize/secure_serializer_spec.rb +3 -2
- data/spec/spec_helper.rb +54 -1
- metadata +71 -12
@@ -0,0 +1,495 @@
|
|
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
|
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
|
+
require File.join(File.dirname(__FILE__), '..', 'core_payload_types')
|
25
|
+
|
26
|
+
module RightScale
|
27
|
+
|
28
|
+
# Abstract base client for creating RightNet and RightApi clients with retry capability
|
29
|
+
# Requests are automatically retried to overcome connectivity failures
|
30
|
+
# A status callback is provided so that the user of the client can take action
|
31
|
+
# (e.g., queue requests) when connectivity is lost
|
32
|
+
# Health checks are sent periodically to try to recover from connectivity failures
|
33
|
+
class BaseRetryClient
|
34
|
+
|
35
|
+
# Interval between reconnect attempts
|
36
|
+
DEFAULT_RECONNECT_INTERVAL = 15
|
37
|
+
|
38
|
+
# Default time to wait for HTTP connection to open
|
39
|
+
DEFAULT_OPEN_TIMEOUT = 2
|
40
|
+
|
41
|
+
# Default time to wait for response from request, which is chosen to be 5 seconds greater
|
42
|
+
# than the response timeout inside the RightNet router
|
43
|
+
DEFAULT_REQUEST_TIMEOUT = 35
|
44
|
+
|
45
|
+
# Default interval between successive retries and default maximum elapsed time until stop retrying
|
46
|
+
# These are chosen to be consistent with the retry sequencing for RightNet retryable requests
|
47
|
+
# (per :retry_interval and :retry_timeout agent deployer configuration parameters for RightNet router),
|
48
|
+
# so that if the retrying happens within the router, it will not retry here
|
49
|
+
DEFAULT_RETRY_INTERVALS = [4, 12, 36]
|
50
|
+
DEFAULT_RETRY_TIMEOUT = 25
|
51
|
+
|
52
|
+
# State of this client: :pending, :connected, :disconnected, :failed, :closing, :closed
|
53
|
+
attr_reader :state
|
54
|
+
|
55
|
+
PERMITTED_STATE_TRANSITIONS = {
|
56
|
+
:pending => [:pending, :connected, :disconnected, :failed, :closed],
|
57
|
+
:connected => [:connected, :disconnected, :failed, :closing, :closed],
|
58
|
+
:disconnected => [:connected, :disconnected, :failed, :closed],
|
59
|
+
:failed => [:failed, :closed],
|
60
|
+
:closing => [:closing, :closed],
|
61
|
+
:closed => [:closed] }
|
62
|
+
|
63
|
+
# Set configuration of this client and initialize HTTP access
|
64
|
+
#
|
65
|
+
# @param [Symbol] type of server for use in obtaining URL from auth_client, e.g., :router
|
66
|
+
# @param [AuthClient] auth_client providing authorization session for HTTP requests
|
67
|
+
#
|
68
|
+
# @option options [String] :server_name for use in reporting errors, e.g., RightNet
|
69
|
+
# @option options [String] :api_version of server for use in X-API-Version header
|
70
|
+
# @option options [Numeric] :open_timeout maximum wait for connection; defaults to DEFAULT_OPEN_TIMEOUT
|
71
|
+
# @option options [Numeric] :request_timeout maximum wait for response; defaults to DEFAULT_REQUEST_TIMEOUT
|
72
|
+
# @option options [Numeric] :retry_timeout maximum before stop retrying; defaults to DEFAULT_RETRY_TIMEOUT
|
73
|
+
# @option options [Array] :retry_intervals between successive retries; defaults to DEFAULT_RETRY_INTERVALS
|
74
|
+
# @option options [Boolean] :retry_enabled for requests that fail to connect or that return a retry result
|
75
|
+
# @option options [Numeric] :reconnect_interval for reconnect attempts after lose connectivity
|
76
|
+
# @option options [Array] :filter_params symbols or strings for names of request parameters
|
77
|
+
# whose values are to be hidden when logging; can be augmented on individual requests
|
78
|
+
# @option options [Proc] :exception_callback for unexpected exceptions
|
79
|
+
#
|
80
|
+
# @return [Boolean] whether currently connected
|
81
|
+
#
|
82
|
+
# @raise [ArgumentError] auth client does not support this client type
|
83
|
+
# @raise [ArgumentError] :api_version missing
|
84
|
+
def init(type, auth_client, options)
|
85
|
+
raise ArgumentError, "Auth client does not support server type #{type.inspect}" unless auth_client.respond_to?(type.to_s + "_url")
|
86
|
+
raise ArgumentError, ":api_version option missing" unless options[:api_version]
|
87
|
+
@type = type
|
88
|
+
@auth_client = auth_client
|
89
|
+
@http_client = nil
|
90
|
+
@status_callbacks = []
|
91
|
+
@communicated_callbacks = []
|
92
|
+
@options = options.dup
|
93
|
+
@options[:server_name] ||= type.to_s
|
94
|
+
@options[:open_timeout] ||= DEFAULT_OPEN_TIMEOUT
|
95
|
+
@options[:request_timeout] ||= DEFAULT_REQUEST_TIMEOUT
|
96
|
+
@options[:retry_timeout] ||= DEFAULT_RETRY_TIMEOUT
|
97
|
+
@options[:retry_intervals] ||= DEFAULT_RETRY_INTERVALS
|
98
|
+
@options[:reconnect_interval] ||= DEFAULT_RECONNECT_INTERVAL
|
99
|
+
reset_stats
|
100
|
+
@state = :pending
|
101
|
+
create_http_client
|
102
|
+
enable_use if check_health == :connected
|
103
|
+
state == :connected
|
104
|
+
end
|
105
|
+
|
106
|
+
# Record callback to be notified of status changes
|
107
|
+
# Multiple callbacks are supported
|
108
|
+
#
|
109
|
+
# @yield [type, status] called when status changes (optional)
|
110
|
+
# @yieldparam [Symbol] type of client reporting status change
|
111
|
+
# @yieldparam [Symbol] state of client
|
112
|
+
#
|
113
|
+
# @return [Symbol] current state
|
114
|
+
def status(&callback)
|
115
|
+
@status_callbacks << callback if callback
|
116
|
+
state
|
117
|
+
end
|
118
|
+
|
119
|
+
# Set callback for each successful communication excluding health checks
|
120
|
+
# Multiple callbacks are supported
|
121
|
+
#
|
122
|
+
# @yield [] required block executed after successful communication
|
123
|
+
#
|
124
|
+
# @return [TrueClass] always true
|
125
|
+
def communicated(&callback)
|
126
|
+
@communicated_callbacks << callback if callback
|
127
|
+
true
|
128
|
+
end
|
129
|
+
|
130
|
+
# Take any actions necessary to quiesce client interaction in preparation
|
131
|
+
# for agent termination but allow any active requests to complete
|
132
|
+
#
|
133
|
+
# @param [Symbol] scope of close action: :receive for just closing receive side
|
134
|
+
# of client, :all for closing both receive and send side; defaults to :all
|
135
|
+
#
|
136
|
+
# @return [TrueClass] always true
|
137
|
+
def close(scope = :all)
|
138
|
+
if scope == :receive && state == :connected
|
139
|
+
self.state = :closing
|
140
|
+
else
|
141
|
+
self.state = :closed
|
142
|
+
@reconnect_timer.cancel if @reconnect_timer
|
143
|
+
@reconnect_timer = nil
|
144
|
+
end
|
145
|
+
true
|
146
|
+
end
|
147
|
+
|
148
|
+
# Current statistics for this client
|
149
|
+
#
|
150
|
+
# @param [Boolean] reset the statistics after getting the current ones
|
151
|
+
#
|
152
|
+
# @return [Hash] current statistics
|
153
|
+
# [Hash, NilClass] "reconnects" Activity stats or nil if none
|
154
|
+
# [Hash, NilClass] "request failures" Activity stats or nil if none
|
155
|
+
# [Hash, NilClass] "request sent" Activity stats or nil if none
|
156
|
+
# [Float, NilClass] "response time" average number of seconds to respond to a request or nil if none
|
157
|
+
# [Hash, NilClass] "state" Activity stats or nil if none
|
158
|
+
# [Hash, NilClass] "exceptions" Exceptions stats or nil if none
|
159
|
+
def stats(reset = false)
|
160
|
+
stats = {}
|
161
|
+
@stats.each { |k, v| stats[k] = v.all }
|
162
|
+
stats["response time"] = @stats["requests sent"].avg_duration
|
163
|
+
reset_stats if reset
|
164
|
+
stats
|
165
|
+
end
|
166
|
+
|
167
|
+
protected
|
168
|
+
|
169
|
+
# Reset statistics for this client
|
170
|
+
#
|
171
|
+
# @return [TrueClass] always true
|
172
|
+
def reset_stats
|
173
|
+
@stats = {
|
174
|
+
"reconnects" => RightSupport::Stats::Activity.new,
|
175
|
+
"request failures" => RightSupport::Stats::Activity.new,
|
176
|
+
"requests sent" => RightSupport::Stats::Activity.new,
|
177
|
+
"state" => RightSupport::Stats::Activity.new,
|
178
|
+
"exceptions" => RightSupport::Stats::Exceptions.new(agent = nil, @options[:exception_callback]) }
|
179
|
+
true
|
180
|
+
end
|
181
|
+
|
182
|
+
# Update state of this client
|
183
|
+
# If state has changed, make external callbacks to notify of change
|
184
|
+
# Do not update state once set to :closed
|
185
|
+
#
|
186
|
+
# @param [Hash] value for new state
|
187
|
+
#
|
188
|
+
# @return [Symbol] updated state
|
189
|
+
#
|
190
|
+
# @raise [ArgumentError] invalid state transition
|
191
|
+
def state=(value)
|
192
|
+
if @state != :closed
|
193
|
+
unless PERMITTED_STATE_TRANSITIONS[@state].include?(value)
|
194
|
+
raise ArgumentError, "Invalid state transition: #{@state.inspect} -> #{value.inspect}"
|
195
|
+
end
|
196
|
+
|
197
|
+
case value
|
198
|
+
when :pending, :closing, :closed
|
199
|
+
@stats["state"].update(value.to_s)
|
200
|
+
@state = value
|
201
|
+
when :connected, :disconnected, :failed
|
202
|
+
if value != @state
|
203
|
+
@stats["state"].update(value.to_s)
|
204
|
+
@state = value
|
205
|
+
@status_callbacks.each do |callback|
|
206
|
+
begin
|
207
|
+
callback.call(@type, @state)
|
208
|
+
rescue StandardError => e
|
209
|
+
Log.error("Failed status callback", e)
|
210
|
+
@stats["exceptions"].track("status", e)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
reconnect if @state == :disconnected
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
@state
|
218
|
+
end
|
219
|
+
|
220
|
+
# Create HTTP client
|
221
|
+
#
|
222
|
+
# @return [TrueClass] always true
|
223
|
+
#
|
224
|
+
# @return [RightSupport::Net::BalancedHttpClient] client
|
225
|
+
def create_http_client
|
226
|
+
url = @auth_client.send(@type.to_s + "_url")
|
227
|
+
Log.info("Connecting to #{@options[:server_name]} via #{url.inspect}")
|
228
|
+
options = {
|
229
|
+
:server_name => @options[:server_name],
|
230
|
+
:open_timeout => @options[:open_timeout],
|
231
|
+
:request_timeout => @options[:request_timeout] }
|
232
|
+
options[:api_version] = @options[:api_version] if @options[:api_version]
|
233
|
+
options[:filter_params] = @options[:filter_params] if @options[:filter_params]
|
234
|
+
@http_client = RightScale::BalancedHttpClient.new(url, options)
|
235
|
+
end
|
236
|
+
|
237
|
+
# Perform any other steps needed to make this client fully usable
|
238
|
+
# once HTTP client has been created and server known to be accessible
|
239
|
+
#
|
240
|
+
# @return [TrueClass] always true
|
241
|
+
def enable_use
|
242
|
+
true
|
243
|
+
end
|
244
|
+
|
245
|
+
# Check health of RightApi directly without applying RequestBalancer
|
246
|
+
# Do not check whether HTTP client exists
|
247
|
+
#
|
248
|
+
# @return [Symbol] RightApi client state
|
249
|
+
def check_health
|
250
|
+
begin
|
251
|
+
@http_client.check_health
|
252
|
+
self.state = :connected
|
253
|
+
rescue BalancedHttpClient::NotResponding => e
|
254
|
+
Log.error("Failed #{@options[:server_name]} health check", e.nested_exception)
|
255
|
+
self.state = :disconnected
|
256
|
+
rescue Exception => e
|
257
|
+
Log.error("Failed #{@options[:server_name]} health check", e)
|
258
|
+
@stats["exceptions"].track("check health", e)
|
259
|
+
self.state = :disconnected
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
# Reconnect with server by periodically checking health
|
264
|
+
# Randomize when initially start checking to reduce server spiking
|
265
|
+
#
|
266
|
+
# @return [TrueClass] always true
|
267
|
+
def reconnect
|
268
|
+
unless @reconnecting
|
269
|
+
@reconnecting = true
|
270
|
+
@stats["reconnects"].update("initiate")
|
271
|
+
@reconnect_timer = EM::PeriodicTimer.new(rand(@options[:reconnect_interval])) do
|
272
|
+
begin
|
273
|
+
create_http_client
|
274
|
+
if check_health == :connected
|
275
|
+
enable_use
|
276
|
+
@stats["reconnects"].update("success")
|
277
|
+
@reconnect_timer.cancel if @reconnect_timer # only need 'if' for test purposes
|
278
|
+
@reconnect_timer = @reconnecting = nil
|
279
|
+
end
|
280
|
+
rescue Exception => e
|
281
|
+
Log.error("Failed #{@options[:server_name]} reconnect", e)
|
282
|
+
@stats["reconnects"].update("failure")
|
283
|
+
@stats["exceptions"].track("reconnect", e)
|
284
|
+
self.state = :disconnected
|
285
|
+
end
|
286
|
+
@reconnect_timer.interval = @options[:reconnect_interval] if @reconnect_timer
|
287
|
+
end
|
288
|
+
end
|
289
|
+
true
|
290
|
+
end
|
291
|
+
|
292
|
+
# Make request via HTTP
|
293
|
+
# Rely on underlying HTTP client to log request and response
|
294
|
+
# Retry request if response indicates to or if there are connectivity failures
|
295
|
+
#
|
296
|
+
# There are also several timeouts involved:
|
297
|
+
# - Underlying BalancedHttpClient connection open timeout (:open_timeout)
|
298
|
+
# - Underlying BalancedHttpClient request timeout (:request_timeout)
|
299
|
+
# - Retry timeout for this method and its handlers (:retry_timeout)
|
300
|
+
# and if the target server is a RightNet router:
|
301
|
+
# - Router response timeout (ideally > :retry_timeout and < :request_timeout)
|
302
|
+
# - Router retry timeout (ideally = :retry_timeout)
|
303
|
+
#
|
304
|
+
# There are several possible levels of retry involved, starting with the outermost:
|
305
|
+
# - This method will retry if the targeted server is not responding or if it receives
|
306
|
+
# a retry response, but the total elapsed time is not allowed to exceed :request_timeout
|
307
|
+
# - RequestBalancer in BalancedHttpClient will retry using other endpoints if it gets an error
|
308
|
+
# that it considers retryable, and even if a front-end balancer is in use there will
|
309
|
+
# likely be at least two such endpoints for redundancy
|
310
|
+
# and if the target server is a RightNet router:
|
311
|
+
# - The router when sending a request via AMQP will retry if it receives no response,
|
312
|
+
# but not exceeding its configured :retry_timeout; if the router's timeouts for retry
|
313
|
+
# are consistent with the ones prescribed above, there will be no retry by the
|
314
|
+
# RequestBalancer after router retries
|
315
|
+
#
|
316
|
+
# @param [Symbol] verb for HTTP REST request
|
317
|
+
# @param [String] path in URI for desired resource
|
318
|
+
# @param [Hash] params for HTTP request
|
319
|
+
# @param [String] type of request for use in logging; defaults to path
|
320
|
+
# @param [String, NilClass] request_uuid uniquely identifying this request;
|
321
|
+
# defaults to randomly generated UUID
|
322
|
+
# @param [Hash] options augmenting or overriding default options for HTTP request
|
323
|
+
#
|
324
|
+
# @return [Object, NilClass] result of request with nil meaning no result
|
325
|
+
#
|
326
|
+
# @raise [Exceptions::Unauthorized] authorization failed
|
327
|
+
# @raise [Exceptions::ConnectivityFailure] cannot connect to server, lost connection
|
328
|
+
# to it, or it is out of service or too busy to respond
|
329
|
+
# @raise [Exceptions::RetryableError] request failed but if retried may succeed
|
330
|
+
# @raise [Exceptions::Terminating] closing client and terminating service
|
331
|
+
# @raise [Exceptions::InternalServerError] internal error in server being accessed
|
332
|
+
def make_request(verb, path, params = {}, type = nil, request_uuid = nil, options = {})
|
333
|
+
raise Exceptions::Terminating if state == :closed
|
334
|
+
request_uuid ||= RightSupport::Data::UUID.generate
|
335
|
+
started_at = Time.now
|
336
|
+
attempts = 0
|
337
|
+
result = nil
|
338
|
+
@stats["requests sent"].measure(type || path, request_uuid) do
|
339
|
+
begin
|
340
|
+
attempts += 1
|
341
|
+
http_options = {
|
342
|
+
:open_timeout => @options[:open_timeout],
|
343
|
+
:request_timeout => @options[:request_timeout],
|
344
|
+
:request_uuid => request_uuid,
|
345
|
+
:headers => @auth_client.headers }
|
346
|
+
raise Exceptions::ConnectivityFailure, "#{@type} client not connected" unless [:connected, :closing].include?(state)
|
347
|
+
result = @http_client.send(verb, path, params, http_options.merge(options))
|
348
|
+
rescue StandardError => e
|
349
|
+
request_uuid = handle_exception(e, type || path, request_uuid, started_at, attempts)
|
350
|
+
request_uuid ? retry : raise
|
351
|
+
end
|
352
|
+
end
|
353
|
+
@communicated_callbacks.each { |callback| callback.call }
|
354
|
+
result
|
355
|
+
end
|
356
|
+
|
357
|
+
# Examine exception to determine whether to setup retry, raise new exception, or re-raise
|
358
|
+
#
|
359
|
+
# @param [StandardError] exception raised
|
360
|
+
# @param [String] action from request type
|
361
|
+
# @param [String] type of request for use in logging
|
362
|
+
# @param [String] request_uuid originally created for this request
|
363
|
+
# @param [Time] started_at time for request
|
364
|
+
# @param [Integer] attempts to make request
|
365
|
+
#
|
366
|
+
# @return [String, NilClass] request token to be used on retry or nil if to raise instead
|
367
|
+
#
|
368
|
+
# @raise [Exceptions::Unauthorized] authorization failed
|
369
|
+
# @raise [Exceptions::ConnectivityFailure] cannot connect to server, lost connection
|
370
|
+
# to it, or it is out of service or too busy to respond
|
371
|
+
# @raise [Exceptions::RetryableError] request failed but if retried may succeed
|
372
|
+
# @raise [Exceptions::InternalServerError] internal error in server being accessed
|
373
|
+
def handle_exception(exception, type, request_uuid, started_at, attempts)
|
374
|
+
result = request_uuid
|
375
|
+
if exception.respond_to?(:http_code)
|
376
|
+
case exception.http_code
|
377
|
+
when 301, 302 # MovedPermanently, Found
|
378
|
+
handle_redirect(exception, type, request_uuid)
|
379
|
+
when 401 # Unauthorized
|
380
|
+
raise Exceptions::Unauthorized.new(exception.http_body, exception)
|
381
|
+
when 403 # Forbidden
|
382
|
+
@auth_client.expired
|
383
|
+
raise Exceptions::RetryableError.new("Authorization expired", exception)
|
384
|
+
when 449 # RetryWith
|
385
|
+
result = handle_retry_with(exception, type, request_uuid, started_at, attempts)
|
386
|
+
when 500 # InternalServerError
|
387
|
+
raise Exceptions::InternalServerError.new(exception.http_body, @options[:server_name])
|
388
|
+
else
|
389
|
+
@stats["request failures"].update("#{type} - #{exception.http_code}")
|
390
|
+
result = nil
|
391
|
+
end
|
392
|
+
elsif exception.is_a?(BalancedHttpClient::NotResponding)
|
393
|
+
handle_not_responding(exception, type, request_uuid, started_at, attempts)
|
394
|
+
else
|
395
|
+
@stats["request failures"].update("#{type} - #{exception.class.name}")
|
396
|
+
result = nil
|
397
|
+
end
|
398
|
+
result
|
399
|
+
end
|
400
|
+
|
401
|
+
# Treat redirect response as indication that no longer accessing the correct shard
|
402
|
+
# Handle it by informing auth client so that it can re-authorize
|
403
|
+
# Do not retry, but tell client to with the expectation that re-auth will correct the situation
|
404
|
+
#
|
405
|
+
# @param [RestClient::MovedPermanently, RestClient::Found] redirect exception raised
|
406
|
+
# @param [String] type of request for use in logging
|
407
|
+
# @param [String] request_uuid originally created for this request
|
408
|
+
#
|
409
|
+
# @return [TrueClass] never returns
|
410
|
+
#
|
411
|
+
# @raise [Exceptions::RetryableError] request redirected but if retried may succeed
|
412
|
+
# @raise [Exceptions::InternalServerError] no redirect location provided
|
413
|
+
def handle_redirect(redirect, type, request_uuid)
|
414
|
+
Log.info("Received REDIRECT #{redirect} for #{type} request <#{request_uuid}>")
|
415
|
+
if redirect.respond_to?(:response) && (location = redirect.response.headers[:location]) && !location.empty?
|
416
|
+
Log.info("Requesting auth client to handle redirect to #{location.inspect}")
|
417
|
+
@stats["reconnects"].update("redirect")
|
418
|
+
@auth_client.redirect(location)
|
419
|
+
raise Exceptions::RetryableError.new(redirect.http_body, redirect)
|
420
|
+
else
|
421
|
+
raise Exceptions::InternalServerError.new("No redirect location provided", @options[:server_name])
|
422
|
+
end
|
423
|
+
true
|
424
|
+
end
|
425
|
+
|
426
|
+
# Handle retry response by retrying it once
|
427
|
+
# This indicates the request was received but a retryable error prevented
|
428
|
+
# it from being processed; the retry responsibility may be passed on
|
429
|
+
# If retrying, this function does not return until it is time to retry
|
430
|
+
#
|
431
|
+
# @param [RestClient::RetryWith] retry_result exception raised
|
432
|
+
# @param [String] type of request for use in logging
|
433
|
+
# @param [String] request_uuid originally created for this request
|
434
|
+
# @param [Time] started_at time for request
|
435
|
+
# @param [Integer] attempts to make request
|
436
|
+
#
|
437
|
+
# @return [String] request token to be used on retry
|
438
|
+
#
|
439
|
+
# @raise [Exceptions::RetryableError] request failed but if retried may succeed
|
440
|
+
def handle_retry_with(retry_result, type, request_uuid, started_at, attempts)
|
441
|
+
if @options[:retry_enabled]
|
442
|
+
interval = @options[:retry_intervals][attempts - 1]
|
443
|
+
if attempts == 1 && interval && (Time.now - started_at) < @options[:retry_timeout]
|
444
|
+
Log.error("Retrying #{type} request <#{request_uuid}> in #{interval} seconds " +
|
445
|
+
"in response to retryable error (#{retry_result.http_body})")
|
446
|
+
sleep(interval)
|
447
|
+
else
|
448
|
+
@stats["request failures"].update("#{type} - retry")
|
449
|
+
raise Exceptions::RetryableError.new(retry_result.http_body, retry_result)
|
450
|
+
end
|
451
|
+
else
|
452
|
+
@stats["request failures"].update("#{type} - retry")
|
453
|
+
raise Exceptions::RetryableError.new(retry_result.http_body, retry_result)
|
454
|
+
end
|
455
|
+
# Change request_uuid so that retried request not rejected as duplicate
|
456
|
+
"#{request_uuid}:retry"
|
457
|
+
end
|
458
|
+
|
459
|
+
# Handle not responding response by determining whether okay to retry
|
460
|
+
# If request is being retried, this function does not return until it is time to retry
|
461
|
+
#
|
462
|
+
# @param [RightScale::BalancedHttpClient::NotResponding] not_responding exception
|
463
|
+
# indicating targeted server is too busy or out of service
|
464
|
+
# @param [String] type of request for use in logging
|
465
|
+
# @param [String] request_uuid originally created for this request
|
466
|
+
# @param [Time] started_at time for request
|
467
|
+
# @param [Integer] attempts to make request
|
468
|
+
#
|
469
|
+
# @return [TrueClass] always true
|
470
|
+
#
|
471
|
+
# @raise [Exceptions::ConnectivityFailure] cannot connect to server, lost connection
|
472
|
+
# to it, or it is out of service or too busy to respond
|
473
|
+
def handle_not_responding(not_responding, type, request_uuid, started_at, attempts)
|
474
|
+
if @options[:retry_enabled]
|
475
|
+
interval = @options[:retry_intervals][attempts - 1]
|
476
|
+
if interval && (Time.now - started_at) < @options[:retry_timeout]
|
477
|
+
Log.error("Retrying #{type} request <#{request_uuid}> in #{interval} seconds " +
|
478
|
+
"in response to routing failure (#{BalancedHttpClient.exception_text(not_responding)})")
|
479
|
+
sleep(interval)
|
480
|
+
else
|
481
|
+
@stats["request failures"].update("#{type} - no result")
|
482
|
+
self.state = :disconnected
|
483
|
+
raise Exceptions::ConnectivityFailure.new(not_responding.message + " after #{attempts} attempts")
|
484
|
+
end
|
485
|
+
else
|
486
|
+
@stats["request failures"].update("#{type} - no result")
|
487
|
+
self.state = :disconnected
|
488
|
+
raise Exceptions::ConnectivityFailure.new(not_responding.message)
|
489
|
+
end
|
490
|
+
true
|
491
|
+
end
|
492
|
+
|
493
|
+
end # BaseRetryClient
|
494
|
+
|
495
|
+
end # RightScale
|