right_agent 2.0.7-x86-mingw32

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.
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