right_agent 1.0.1 → 2.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. data/README.rdoc +10 -8
  2. data/Rakefile +31 -5
  3. data/lib/right_agent.rb +6 -1
  4. data/lib/right_agent/actor.rb +4 -20
  5. data/lib/right_agent/actors/agent_manager.rb +1 -1
  6. data/lib/right_agent/agent.rb +357 -144
  7. data/lib/right_agent/agent_config.rb +7 -6
  8. data/lib/right_agent/agent_identity.rb +13 -11
  9. data/lib/right_agent/agent_tag_manager.rb +60 -64
  10. data/{spec/results_mock.rb → lib/right_agent/clients.rb} +10 -24
  11. data/lib/right_agent/clients/api_client.rb +383 -0
  12. data/lib/right_agent/clients/auth_client.rb +247 -0
  13. data/lib/right_agent/clients/balanced_http_client.rb +369 -0
  14. data/lib/right_agent/clients/base_retry_client.rb +495 -0
  15. data/lib/right_agent/clients/right_http_client.rb +279 -0
  16. data/lib/right_agent/clients/router_client.rb +493 -0
  17. data/lib/right_agent/command/command_io.rb +4 -4
  18. data/lib/right_agent/command/command_parser.rb +2 -2
  19. data/lib/right_agent/command/command_runner.rb +1 -1
  20. data/lib/right_agent/connectivity_checker.rb +179 -0
  21. data/lib/right_agent/core_payload_types/secure_document_location.rb +2 -2
  22. data/lib/right_agent/dispatcher.rb +12 -10
  23. data/lib/right_agent/enrollment_result.rb +16 -12
  24. data/lib/right_agent/exceptions.rb +34 -20
  25. data/lib/right_agent/history.rb +10 -5
  26. data/lib/right_agent/log.rb +5 -5
  27. data/lib/right_agent/minimal.rb +1 -0
  28. data/lib/right_agent/multiplexer.rb +1 -1
  29. data/lib/right_agent/offline_handler.rb +270 -0
  30. data/lib/right_agent/packets.rb +7 -7
  31. data/lib/right_agent/payload_formatter.rb +1 -1
  32. data/lib/right_agent/pending_requests.rb +128 -0
  33. data/lib/right_agent/platform.rb +1 -1
  34. data/lib/right_agent/protocol_version_mixin.rb +69 -0
  35. data/lib/right_agent/{idempotent_request.rb → retryable_request.rb} +7 -7
  36. data/lib/right_agent/scripts/agent_controller.rb +28 -26
  37. data/lib/right_agent/scripts/agent_deployer.rb +37 -22
  38. data/lib/right_agent/scripts/common_parser.rb +10 -3
  39. data/lib/right_agent/secure_identity.rb +1 -1
  40. data/lib/right_agent/sender.rb +299 -785
  41. data/lib/right_agent/serialize/secure_serializer.rb +3 -1
  42. data/lib/right_agent/serialize/secure_serializer_initializer.rb +2 -2
  43. data/lib/right_agent/serialize/serializable.rb +8 -3
  44. data/right_agent.gemspec +49 -18
  45. data/spec/agent_config_spec.rb +7 -7
  46. data/spec/agent_identity_spec.rb +7 -4
  47. data/spec/agent_spec.rb +43 -7
  48. data/spec/agent_tag_manager_spec.rb +72 -83
  49. data/spec/clients/api_client_spec.rb +423 -0
  50. data/spec/clients/auth_client_spec.rb +272 -0
  51. data/spec/clients/balanced_http_client_spec.rb +576 -0
  52. data/spec/clients/base_retry_client_spec.rb +635 -0
  53. data/spec/clients/router_client_spec.rb +594 -0
  54. data/spec/clients/spec_helper.rb +111 -0
  55. data/spec/command/command_io_spec.rb +1 -1
  56. data/spec/command/command_parser_spec.rb +1 -1
  57. data/spec/connectivity_checker_spec.rb +83 -0
  58. data/spec/dispatcher_spec.rb +3 -2
  59. data/spec/enrollment_result_spec.rb +2 -2
  60. data/spec/history_spec.rb +51 -39
  61. data/spec/offline_handler_spec.rb +340 -0
  62. data/spec/pending_requests_spec.rb +136 -0
  63. data/spec/{idempotent_request_spec.rb → retryable_request_spec.rb} +73 -73
  64. data/spec/sender_spec.rb +835 -1052
  65. data/spec/serialize/secure_serializer_spec.rb +3 -2
  66. data/spec/spec_helper.rb +54 -1
  67. 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