right_agent 2.0.7-x86-mingw32

Sign up to get free protection for your applications and to get access to all the features.
Files changed (176) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +82 -0
  3. data/Rakefile +113 -0
  4. data/lib/right_agent.rb +59 -0
  5. data/lib/right_agent/actor.rb +182 -0
  6. data/lib/right_agent/actor_registry.rb +76 -0
  7. data/lib/right_agent/actors/agent_manager.rb +232 -0
  8. data/lib/right_agent/agent.rb +1149 -0
  9. data/lib/right_agent/agent_config.rb +480 -0
  10. data/lib/right_agent/agent_identity.rb +210 -0
  11. data/lib/right_agent/agent_tag_manager.rb +237 -0
  12. data/lib/right_agent/audit_formatter.rb +107 -0
  13. data/lib/right_agent/clients.rb +31 -0
  14. data/lib/right_agent/clients/api_client.rb +383 -0
  15. data/lib/right_agent/clients/auth_client.rb +247 -0
  16. data/lib/right_agent/clients/balanced_http_client.rb +369 -0
  17. data/lib/right_agent/clients/base_retry_client.rb +495 -0
  18. data/lib/right_agent/clients/right_http_client.rb +279 -0
  19. data/lib/right_agent/clients/router_client.rb +493 -0
  20. data/lib/right_agent/command.rb +30 -0
  21. data/lib/right_agent/command/agent_manager_commands.rb +150 -0
  22. data/lib/right_agent/command/command_client.rb +136 -0
  23. data/lib/right_agent/command/command_constants.rb +33 -0
  24. data/lib/right_agent/command/command_io.rb +126 -0
  25. data/lib/right_agent/command/command_parser.rb +87 -0
  26. data/lib/right_agent/command/command_runner.rb +118 -0
  27. data/lib/right_agent/command/command_serializer.rb +63 -0
  28. data/lib/right_agent/connectivity_checker.rb +179 -0
  29. data/lib/right_agent/console.rb +65 -0
  30. data/lib/right_agent/core_payload_types.rb +44 -0
  31. data/lib/right_agent/core_payload_types/cookbook.rb +61 -0
  32. data/lib/right_agent/core_payload_types/cookbook_position.rb +46 -0
  33. data/lib/right_agent/core_payload_types/cookbook_repository.rb +116 -0
  34. data/lib/right_agent/core_payload_types/cookbook_sequence.rb +70 -0
  35. data/lib/right_agent/core_payload_types/dev_repositories.rb +100 -0
  36. data/lib/right_agent/core_payload_types/dev_repository.rb +76 -0
  37. data/lib/right_agent/core_payload_types/event_categories.rb +38 -0
  38. data/lib/right_agent/core_payload_types/executable_bundle.rb +130 -0
  39. data/lib/right_agent/core_payload_types/login_policy.rb +72 -0
  40. data/lib/right_agent/core_payload_types/login_user.rb +79 -0
  41. data/lib/right_agent/core_payload_types/planned_volume.rb +94 -0
  42. data/lib/right_agent/core_payload_types/recipe_instantiation.rb +73 -0
  43. data/lib/right_agent/core_payload_types/repositories_bundle.rb +50 -0
  44. data/lib/right_agent/core_payload_types/right_script_attachment.rb +95 -0
  45. data/lib/right_agent/core_payload_types/right_script_instantiation.rb +94 -0
  46. data/lib/right_agent/core_payload_types/runlist_policy.rb +44 -0
  47. data/lib/right_agent/core_payload_types/secure_document.rb +66 -0
  48. data/lib/right_agent/core_payload_types/secure_document_location.rb +63 -0
  49. data/lib/right_agent/core_payload_types/software_repository_instantiation.rb +61 -0
  50. data/lib/right_agent/daemonize.rb +35 -0
  51. data/lib/right_agent/dispatched_cache.rb +109 -0
  52. data/lib/right_agent/dispatcher.rb +272 -0
  53. data/lib/right_agent/enrollment_result.rb +221 -0
  54. data/lib/right_agent/exceptions.rb +87 -0
  55. data/lib/right_agent/history.rb +145 -0
  56. data/lib/right_agent/log.rb +460 -0
  57. data/lib/right_agent/minimal.rb +46 -0
  58. data/lib/right_agent/monkey_patches.rb +30 -0
  59. data/lib/right_agent/monkey_patches/ruby_patch.rb +55 -0
  60. data/lib/right_agent/monkey_patches/ruby_patch/array_patch.rb +29 -0
  61. data/lib/right_agent/monkey_patches/ruby_patch/darwin_patch.rb +24 -0
  62. data/lib/right_agent/monkey_patches/ruby_patch/linux_patch.rb +24 -0
  63. data/lib/right_agent/monkey_patches/ruby_patch/linux_patch/file_patch.rb +30 -0
  64. data/lib/right_agent/monkey_patches/ruby_patch/object_patch.rb +49 -0
  65. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch.rb +32 -0
  66. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/file_patch.rb +60 -0
  67. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/process_patch.rb +63 -0
  68. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/stdio_patch.rb +27 -0
  69. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/time_patch.rb +55 -0
  70. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/win32ole_patch.rb +34 -0
  71. data/lib/right_agent/multiplexer.rb +102 -0
  72. data/lib/right_agent/offline_handler.rb +270 -0
  73. data/lib/right_agent/operation_result.rb +300 -0
  74. data/lib/right_agent/packets.rb +673 -0
  75. data/lib/right_agent/payload_formatter.rb +104 -0
  76. data/lib/right_agent/pending_requests.rb +128 -0
  77. data/lib/right_agent/pid_file.rb +159 -0
  78. data/lib/right_agent/platform.rb +770 -0
  79. data/lib/right_agent/platform/unix/darwin/platform.rb +102 -0
  80. data/lib/right_agent/platform/unix/linux/platform.rb +305 -0
  81. data/lib/right_agent/platform/unix/platform.rb +226 -0
  82. data/lib/right_agent/platform/windows/mingw/platform.rb +447 -0
  83. data/lib/right_agent/platform/windows/mswin/platform.rb +236 -0
  84. data/lib/right_agent/platform/windows/platform.rb +1808 -0
  85. data/lib/right_agent/protocol_version_mixin.rb +69 -0
  86. data/lib/right_agent/retryable_request.rb +195 -0
  87. data/lib/right_agent/scripts/agent_controller.rb +543 -0
  88. data/lib/right_agent/scripts/agent_deployer.rb +400 -0
  89. data/lib/right_agent/scripts/common_parser.rb +160 -0
  90. data/lib/right_agent/scripts/log_level_manager.rb +192 -0
  91. data/lib/right_agent/scripts/stats_manager.rb +268 -0
  92. data/lib/right_agent/scripts/usage.rb +58 -0
  93. data/lib/right_agent/secure_identity.rb +92 -0
  94. data/lib/right_agent/security.rb +32 -0
  95. data/lib/right_agent/security/cached_certificate_store_proxy.rb +77 -0
  96. data/lib/right_agent/security/certificate.rb +102 -0
  97. data/lib/right_agent/security/certificate_cache.rb +89 -0
  98. data/lib/right_agent/security/distinguished_name.rb +56 -0
  99. data/lib/right_agent/security/encrypted_document.rb +83 -0
  100. data/lib/right_agent/security/rsa_key_pair.rb +76 -0
  101. data/lib/right_agent/security/signature.rb +86 -0
  102. data/lib/right_agent/security/static_certificate_store.rb +85 -0
  103. data/lib/right_agent/sender.rb +792 -0
  104. data/lib/right_agent/serialize.rb +29 -0
  105. data/lib/right_agent/serialize/message_pack.rb +107 -0
  106. data/lib/right_agent/serialize/secure_serializer.rb +151 -0
  107. data/lib/right_agent/serialize/secure_serializer_initializer.rb +47 -0
  108. data/lib/right_agent/serialize/serializable.rb +151 -0
  109. data/lib/right_agent/serialize/serializer.rb +159 -0
  110. data/lib/right_agent/subprocess.rb +38 -0
  111. data/lib/right_agent/tracer.rb +124 -0
  112. data/right_agent.gemspec +101 -0
  113. data/spec/actor_registry_spec.rb +80 -0
  114. data/spec/actor_spec.rb +162 -0
  115. data/spec/agent_config_spec.rb +235 -0
  116. data/spec/agent_identity_spec.rb +78 -0
  117. data/spec/agent_spec.rb +734 -0
  118. data/spec/agent_tag_manager_spec.rb +319 -0
  119. data/spec/clients/api_client_spec.rb +423 -0
  120. data/spec/clients/auth_client_spec.rb +272 -0
  121. data/spec/clients/balanced_http_client_spec.rb +576 -0
  122. data/spec/clients/base_retry_client_spec.rb +635 -0
  123. data/spec/clients/router_client_spec.rb +594 -0
  124. data/spec/clients/spec_helper.rb +111 -0
  125. data/spec/command/agent_manager_commands_spec.rb +51 -0
  126. data/spec/command/command_io_spec.rb +93 -0
  127. data/spec/command/command_parser_spec.rb +79 -0
  128. data/spec/command/command_runner_spec.rb +107 -0
  129. data/spec/command/command_serializer_spec.rb +51 -0
  130. data/spec/connectivity_checker_spec.rb +83 -0
  131. data/spec/core_payload_types/dev_repositories_spec.rb +64 -0
  132. data/spec/core_payload_types/dev_repository_spec.rb +33 -0
  133. data/spec/core_payload_types/executable_bundle_spec.rb +67 -0
  134. data/spec/core_payload_types/login_user_spec.rb +102 -0
  135. data/spec/core_payload_types/recipe_instantiation_spec.rb +81 -0
  136. data/spec/core_payload_types/right_script_attachment_spec.rb +65 -0
  137. data/spec/core_payload_types/right_script_instantiation_spec.rb +79 -0
  138. data/spec/core_payload_types/spec_helper.rb +23 -0
  139. data/spec/dispatched_cache_spec.rb +136 -0
  140. data/spec/dispatcher_spec.rb +324 -0
  141. data/spec/enrollment_result_spec.rb +53 -0
  142. data/spec/history_spec.rb +246 -0
  143. data/spec/log_spec.rb +192 -0
  144. data/spec/monkey_patches/eventmachine_spec.rb +62 -0
  145. data/spec/multiplexer_spec.rb +48 -0
  146. data/spec/offline_handler_spec.rb +340 -0
  147. data/spec/operation_result_spec.rb +208 -0
  148. data/spec/packets_spec.rb +461 -0
  149. data/spec/pending_requests_spec.rb +136 -0
  150. data/spec/platform/spec_helper.rb +216 -0
  151. data/spec/platform/unix/darwin/platform_spec.rb +181 -0
  152. data/spec/platform/unix/linux/platform_spec.rb +540 -0
  153. data/spec/platform/unix/spec_helper.rb +149 -0
  154. data/spec/platform/windows/mingw/platform_spec.rb +222 -0
  155. data/spec/platform/windows/mswin/platform_spec.rb +259 -0
  156. data/spec/platform/windows/spec_helper.rb +720 -0
  157. data/spec/retryable_request_spec.rb +306 -0
  158. data/spec/secure_identity_spec.rb +50 -0
  159. data/spec/security/cached_certificate_store_proxy_spec.rb +62 -0
  160. data/spec/security/certificate_cache_spec.rb +71 -0
  161. data/spec/security/certificate_spec.rb +49 -0
  162. data/spec/security/distinguished_name_spec.rb +46 -0
  163. data/spec/security/encrypted_document_spec.rb +55 -0
  164. data/spec/security/rsa_key_pair_spec.rb +55 -0
  165. data/spec/security/signature_spec.rb +66 -0
  166. data/spec/security/static_certificate_store_spec.rb +58 -0
  167. data/spec/sender_spec.rb +1045 -0
  168. data/spec/serialize/message_pack_spec.rb +131 -0
  169. data/spec/serialize/secure_serializer_spec.rb +132 -0
  170. data/spec/serialize/serializable_spec.rb +90 -0
  171. data/spec/serialize/serializer_spec.rb +197 -0
  172. data/spec/spec.opts +2 -0
  173. data/spec/spec.win32.opts +1 -0
  174. data/spec/spec_helper.rb +130 -0
  175. data/spec/tracer_spec.rb +114 -0
  176. metadata +447 -0
@@ -0,0 +1,369 @@
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 'restclient'
25
+
26
+ module RightScale
27
+
28
+ # HTTP REST client for request balanced access to RightScale servers
29
+ # It is intended for use by instance agents and by infrastructure servers
30
+ # and therefore supports both session cookie and global session-based authentication
31
+ class BalancedHttpClient
32
+
33
+ # When server not responding and retry is recommended
34
+ class NotResponding < Exceptions::NestedException; end
35
+
36
+ # HTTP status codes for which a retry is warranted, which is limited to when server
37
+ # is not accessible for some reason (502, 503) or server response indicates that
38
+ # the request could not be routed for some retryable reason (504)
39
+ RETRY_STATUS_CODES = [502, 503, 504]
40
+
41
+ # Default time for HTTP connection to open
42
+ DEFAULT_OPEN_TIMEOUT = 2
43
+
44
+ # Default time to wait for health check response
45
+ HEALTH_CHECK_TIMEOUT = 5
46
+
47
+ # Default time to wait for response from request
48
+ DEFAULT_REQUEST_TIMEOUT = 30
49
+
50
+ # Default health check path
51
+ DEFAULT_HEALTH_CHECK_PATH = "/health-check"
52
+
53
+ # Text used for filtered parameter value
54
+ FILTERED_PARAM_VALUE = "<hidden>"
55
+
56
+ # Environment variables to examine for proxy settings, in order
57
+ PROXY_ENVIRONMENT_VARIABLES = ['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy', 'ALL_PROXY']
58
+
59
+ # Create client for making HTTP REST requests
60
+ #
61
+ # @param [Array, String] urls of server being accessed as array or comma-separated string
62
+ #
63
+ # @option options [String] :api_version for X-API-Version header
64
+ # @option options [String] :server_name of server for use in exceptions; defaults to host name
65
+ # @option options [String] :health_check_path in URI for health check resource;
66
+ # defaults to DEFAULT_HEALTH_CHECK_PATH
67
+ # @option options [Array] :filter_params symbols or strings for names of request parameters
68
+ # whose values are to be hidden when logging; can be augmented on individual requests
69
+ def initialize(urls, options = {})
70
+ @urls = split(urls)
71
+ @api_version = options[:api_version]
72
+ @server_name = options[:server_name]
73
+ @filter_params = (options[:filter_params] || []).map { |p| p.to_s }
74
+
75
+ # Create health check proc for use by request balancer
76
+ # Strip user and password from host name since health-check does not require authorization
77
+ @health_check_proc = Proc.new do |host|
78
+ uri = URI.parse(host)
79
+ uri.user = uri.password = nil
80
+ uri.path = uri.path + (options[:health_check_path] || DEFAULT_HEALTH_CHECK_PATH)
81
+ check_options = {
82
+ :open_timeout => DEFAULT_OPEN_TIMEOUT,
83
+ :timeout => HEALTH_CHECK_TIMEOUT }
84
+ check_options[:headers] = {"X-API-Version" => @api_version} if @api_version
85
+ RightSupport::Net::HTTPClient.new.get(uri.to_s, check_options)
86
+ end
87
+
88
+ # Initialize use of proxy if defined
89
+ if (proxy_var = PROXY_ENVIRONMENT_VARIABLES.detect { |v| ENV.has_key?(v) })
90
+ proxy = ENV[proxy_var].match(/^[[:alpha:]]+:\/\//) ? URI.parse(ENV[proxy_var]) : URI.parse("http://" + ENV[proxy_var])
91
+ RestClient.proxy = proxy.to_s if proxy
92
+ end
93
+
94
+ # Initialize request balancer
95
+ balancer_options = {
96
+ :policy => RightSupport::Net::LB::HealthCheck,
97
+ :health_check => @health_check_proc }
98
+ @balancer = RightSupport::Net::RequestBalancer.new(@urls, balancer_options)
99
+ end
100
+
101
+ def get(*args)
102
+ request(:get, *args)
103
+ end
104
+
105
+ def post(*args)
106
+ request(:post, *args)
107
+ end
108
+
109
+ def put(*args)
110
+ request(:put, *args)
111
+ end
112
+
113
+ def delete(*args)
114
+ request(:delete, *args)
115
+ end
116
+
117
+ def check_health(host = nil)
118
+ begin
119
+ @health_check_proc.call(host || @urls.first)
120
+ rescue StandardError => e
121
+ if e.respond_to?(:http_code) && RETRY_STATUS_CODES.include?(e.http_code)
122
+ raise NotResponding.new("#{@server_name || host} not responding", e)
123
+ else
124
+ raise
125
+ end
126
+ end
127
+ end
128
+
129
+ protected
130
+
131
+ # Make request via request balancer
132
+ # Encode request parameters and response using JSON
133
+ # Apply configured authorization scheme
134
+ # Log request/response with filtered parameters included for failure or debug mode
135
+ #
136
+ # @param [Symbol] verb for HTTP REST request
137
+ # @param [String] path in URI for desired resource
138
+ # @param [Hash] params for HTTP request
139
+ #
140
+ # @option options [Numeric] :open_timeout maximum wait for connection; defaults to DEFAULT_OPEN_TIMEOUT
141
+ # @option options [Numeric] :request_timeout maximum wait for response; defaults to DEFAULT_REQUEST_TIMEOUT
142
+ # @option options [String] :request_uuid uniquely identifying request; defaults to random generated UUID
143
+ # @option options [Array] :filter_params symbols or strings for names of request
144
+ # parameters whose values are to be hidden when logging in addition to the ones
145
+ # provided during object initialization
146
+ # @option options [Hash] :headers to be added to request
147
+ # @option options [Symbol] :log_level to use when logging information about the request other than errors
148
+ #
149
+ # @return [Object] result returned by receiver of request
150
+ #
151
+ # @raise [NotResponding] server not responding, recommend retry
152
+ def request(verb, path, params = {}, options = {})
153
+ result = nil
154
+ host_picked = nil
155
+ started_at = Time.now
156
+ filter = @filter_params + (options[:filter_params] || []).map { |p| p.to_s }
157
+ request_uuid = options[:request_uuid] || RightSupport::Data::UUID.generate
158
+ log_level = options[:log_level] || :info
159
+
160
+ Log.send(log_level, "Requesting #{verb.to_s.upcase} <#{request_uuid}> " + log_text(path, params, filter))
161
+
162
+ begin
163
+ request_options = {
164
+ :open_timeout => options[:open_timeout] || DEFAULT_OPEN_TIMEOUT,
165
+ :timeout => options[:request_timeout] || DEFAULT_REQUEST_TIMEOUT,
166
+ :headers => {
167
+ "X-Request-Lineage-Uuid" => request_uuid,
168
+ :accept => "application/json" } }
169
+ request_options[:headers]["X-API-Version"] = @api_version if @api_version
170
+ request_options[:headers].merge!(options[:headers]) if options[:headers]
171
+ request_options[:headers]["X-DEBUG"] = true if Log.level == :debug
172
+
173
+ if [:get, :delete].include?(verb)
174
+ request_options[:query] = params if params.is_a?(Hash) && params.any?
175
+ else
176
+ request_options[:payload] = JSON.dump(params)
177
+ request_options[:headers][:content_type] = "application/json"
178
+ end
179
+
180
+ response = @balancer.request do |host|
181
+ uri = URI.parse(host)
182
+ uri.user = uri.password = nil
183
+ host_picked = uri.to_s
184
+ RightSupport::Net::HTTPClient.new.send(verb, host + path, request_options)
185
+ end
186
+ rescue RightSupport::Net::NoResult => e
187
+ handle_no_result(e, host_picked) do |e2|
188
+ report_failure(host_picked, path, params, filter, request_uuid, started_at, e2)
189
+ end
190
+ rescue Exception => e
191
+ report_failure(host_picked, path, params, filter, request_uuid, started_at, e)
192
+ raise
193
+ end
194
+
195
+ response(response, host_picked, path, request_uuid, started_at, log_level)
196
+ end
197
+
198
+ # Process HTTP response by extracting result and logging request completion
199
+ # Extract result from location header for 201 response
200
+ # JSON-decode body of other 2xx responses except for 204
201
+ #
202
+ # @param [RestClient::Response, NilClass] response received
203
+ # @param [String] host server URL where request was completed
204
+ # @param [String] path in URI for desired resource
205
+ # @param [String] request_uuid uniquely identifying request
206
+ # @param [Time] started_at time for request
207
+ # @param [Symbol] log_level to use when logging information about the request
208
+ # other than errors
209
+ #
210
+ # @return [Object] JSON-decoded response body
211
+ def response(response, host, path, request_uuid, started_at, log_level)
212
+ result = nil
213
+ code = "nil"
214
+ length = "-"
215
+
216
+ if response
217
+ code = response.code
218
+ body = response.body
219
+ headers = response.headers
220
+ if (200..207).include?(code)
221
+ if code == 201
222
+ result = headers[:location]
223
+ elsif code == 204 || body.nil? || (body.respond_to?(:empty?) && body.empty?)
224
+ result = nil
225
+ else
226
+ result = JSON.load(body)
227
+ result = nil if result.respond_to?(:empty?) && result.empty?
228
+ end
229
+ end
230
+ length = headers[:content_length] || body.size
231
+ end
232
+
233
+ duration = "%.0fms" % ((Time.now - started_at) * 1000)
234
+ completed = "Completed <#{request_uuid}> in #{duration} | #{code} [#{host}#{path}] | #{length} bytes"
235
+ completed << " | #{result.inspect}" if Log.level == :debug
236
+ Log.send(log_level, completed)
237
+
238
+ result
239
+ end
240
+
241
+ # Handle no result from balancer
242
+ # Distinguish the not responding case since it likely warrants a retry by the client
243
+ # Also try to distinguish between the targeted server not responding and that server
244
+ # gatewaying to another server that is not responding, so that the receiver of
245
+ # the resulting exception is clearer as to the source of the problem
246
+ #
247
+ # @param [RightSupport::Net::NoResult] no_result exception raised by request balancer when it
248
+ # could not deliver request
249
+ # @param [String] host server URL where request was attempted
250
+ #
251
+ # @yield [exception] required block called for reporting exception of interest
252
+ # @yieldparam [Exception] exception extracted
253
+ #
254
+ # @return [TrueClass] always true
255
+ #
256
+ # @raise [NotResponding] server not responding, recommend retry
257
+ def handle_no_result(no_result, host)
258
+ server_name = @server_name || host
259
+ e = no_result.details.values.flatten.last
260
+ if no_result.details.empty?
261
+ yield(no_result)
262
+ raise NotResponding.new("#{server_name} not responding", no_result)
263
+ elsif (e.respond_to?(:http_code) && RETRY_STATUS_CODES.include?(e.http_code))
264
+ yield(e)
265
+ if e.http_code == 504 && (e.http_body && !e.http_body.empty?)
266
+ raise NotResponding.new(e.http_body, e)
267
+ else
268
+ raise NotResponding.new("#{server_name} not responding", e)
269
+ end
270
+ else
271
+ yield(e)
272
+ raise e
273
+ end
274
+ true
275
+ end
276
+
277
+ # Report request failure to logs
278
+ # Also report it as audit entry if an instance is targeted
279
+ #
280
+ # @param [String] host server URL where request was attempted if known
281
+ # @param [String] path in URI for desired resource
282
+ # @param [Hash] params for request
283
+ # @param [Array] filter list of parameters whose value is to be hidden
284
+ # @param [String] request_uuid uniquely identifying request
285
+ # @param [Time] started_at time for request
286
+ # @param [Exception, String] exception or message that should be logged
287
+ #
288
+ # @return [TrueClass] Always return true
289
+ def report_failure(host, path, params, filter, request_uuid, started_at, exception)
290
+ status = exception.respond_to?(:http_code) ? exception.http_code : "nil"
291
+ duration = "%.0fms" % ((Time.now - started_at) * 1000)
292
+ Log.error("Failed <#{request_uuid}> in #{duration} | #{status} " + log_text(path, params, filter, host, exception))
293
+ true
294
+ end
295
+
296
+ # Generate log text describing request and failure if any
297
+ #
298
+ # @param [String] path in URI for desired resource
299
+ # @param [Hash] params for HTTP request
300
+ # @param [Array, NilClass] filter augmentation to base filter list
301
+ # @param [String] host server URL where request was attempted if known
302
+ # @param [Exception, String, NilClass] exception or failure message that should be logged
303
+ #
304
+ # @return [String] Log text
305
+ def log_text(path, params, filter, host = nil, exception = nil)
306
+ filtered_params = (exception || Log.level == :debug) ? filter(params, filter).inspect : nil
307
+ text = filtered_params ? "#{path} #{filtered_params}" : path
308
+ text = "[#{host}#{text}]" if host
309
+ text << " | #{self.class.exception_text(exception)}" if exception
310
+ text
311
+ end
312
+
313
+ # Apply parameter hiding filter
314
+ #
315
+ # @param [Hash, Object] params to be filtered
316
+ # @param [Array] filter names of params as strings (not symbols) whose value is to be hidden
317
+ #
318
+ # @return [Hash] filtered parameters
319
+ def filter(params, filter)
320
+ if filter.empty? || !params.is_a?(Hash)
321
+ params
322
+ else
323
+ filtered_params = {}
324
+ params.each { |k, p| filtered_params[k] = filter.include?(k.to_s) ? FILTERED_PARAM_VALUE : p }
325
+ filtered_params
326
+ end
327
+ end
328
+
329
+ # Split string into an array unless nil or already an array
330
+ #
331
+ # @param [String, Array, NilClass] object to be split
332
+ # @param [String, Regex] pattern on which to split; defaults to comma
333
+ #
334
+ # @return [Array] split object
335
+ def split(object, pattern = /,\s*/)
336
+ object ? (object.is_a?(Array) ? object : object.split(pattern)) : []
337
+ end
338
+
339
+ public
340
+
341
+ # Extract text of exception for logging
342
+ # For RestClient exceptions extract useful info from http_body attribute
343
+ #
344
+ # @param [Exception, String, NilClass] exception or failure message
345
+ #
346
+ # @return [String] exception text
347
+ def self.exception_text(exception)
348
+ case exception
349
+ when String
350
+ exception
351
+ when RestClient::Exception
352
+ if exception.http_body.nil? || exception.http_body.empty? || exception.http_body =~ /^<html>| html /
353
+ exception.message
354
+ else
355
+ exception.inspect
356
+ end
357
+ when RightSupport::Net::NoResult, NotResponding
358
+ "#{exception.class}: #{exception.message}"
359
+ when Exception
360
+ backtrace = exception.backtrace ? " in\n" + exception.backtrace.join("\n") : ""
361
+ "#{exception.class}: #{exception.message}" + backtrace
362
+ else
363
+ ""
364
+ end
365
+ end
366
+
367
+ end # BalancedHttpClient
368
+
369
+ end # RightScale
@@ -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