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.
- data/LICENSE +20 -0
- data/README.rdoc +82 -0
- data/Rakefile +113 -0
- data/lib/right_agent.rb +59 -0
- data/lib/right_agent/actor.rb +182 -0
- data/lib/right_agent/actor_registry.rb +76 -0
- data/lib/right_agent/actors/agent_manager.rb +232 -0
- data/lib/right_agent/agent.rb +1149 -0
- data/lib/right_agent/agent_config.rb +480 -0
- data/lib/right_agent/agent_identity.rb +210 -0
- data/lib/right_agent/agent_tag_manager.rb +237 -0
- data/lib/right_agent/audit_formatter.rb +107 -0
- data/lib/right_agent/clients.rb +31 -0
- data/lib/right_agent/clients/api_client.rb +383 -0
- data/lib/right_agent/clients/auth_client.rb +247 -0
- data/lib/right_agent/clients/balanced_http_client.rb +369 -0
- data/lib/right_agent/clients/base_retry_client.rb +495 -0
- data/lib/right_agent/clients/right_http_client.rb +279 -0
- data/lib/right_agent/clients/router_client.rb +493 -0
- data/lib/right_agent/command.rb +30 -0
- data/lib/right_agent/command/agent_manager_commands.rb +150 -0
- data/lib/right_agent/command/command_client.rb +136 -0
- data/lib/right_agent/command/command_constants.rb +33 -0
- data/lib/right_agent/command/command_io.rb +126 -0
- data/lib/right_agent/command/command_parser.rb +87 -0
- data/lib/right_agent/command/command_runner.rb +118 -0
- data/lib/right_agent/command/command_serializer.rb +63 -0
- data/lib/right_agent/connectivity_checker.rb +179 -0
- data/lib/right_agent/console.rb +65 -0
- data/lib/right_agent/core_payload_types.rb +44 -0
- data/lib/right_agent/core_payload_types/cookbook.rb +61 -0
- data/lib/right_agent/core_payload_types/cookbook_position.rb +46 -0
- data/lib/right_agent/core_payload_types/cookbook_repository.rb +116 -0
- data/lib/right_agent/core_payload_types/cookbook_sequence.rb +70 -0
- data/lib/right_agent/core_payload_types/dev_repositories.rb +100 -0
- data/lib/right_agent/core_payload_types/dev_repository.rb +76 -0
- data/lib/right_agent/core_payload_types/event_categories.rb +38 -0
- data/lib/right_agent/core_payload_types/executable_bundle.rb +130 -0
- data/lib/right_agent/core_payload_types/login_policy.rb +72 -0
- data/lib/right_agent/core_payload_types/login_user.rb +79 -0
- data/lib/right_agent/core_payload_types/planned_volume.rb +94 -0
- data/lib/right_agent/core_payload_types/recipe_instantiation.rb +73 -0
- data/lib/right_agent/core_payload_types/repositories_bundle.rb +50 -0
- data/lib/right_agent/core_payload_types/right_script_attachment.rb +95 -0
- data/lib/right_agent/core_payload_types/right_script_instantiation.rb +94 -0
- data/lib/right_agent/core_payload_types/runlist_policy.rb +44 -0
- data/lib/right_agent/core_payload_types/secure_document.rb +66 -0
- data/lib/right_agent/core_payload_types/secure_document_location.rb +63 -0
- data/lib/right_agent/core_payload_types/software_repository_instantiation.rb +61 -0
- data/lib/right_agent/daemonize.rb +35 -0
- data/lib/right_agent/dispatched_cache.rb +109 -0
- data/lib/right_agent/dispatcher.rb +272 -0
- data/lib/right_agent/enrollment_result.rb +221 -0
- data/lib/right_agent/exceptions.rb +87 -0
- data/lib/right_agent/history.rb +145 -0
- data/lib/right_agent/log.rb +460 -0
- data/lib/right_agent/minimal.rb +46 -0
- data/lib/right_agent/monkey_patches.rb +30 -0
- data/lib/right_agent/monkey_patches/ruby_patch.rb +55 -0
- data/lib/right_agent/monkey_patches/ruby_patch/array_patch.rb +29 -0
- data/lib/right_agent/monkey_patches/ruby_patch/darwin_patch.rb +24 -0
- data/lib/right_agent/monkey_patches/ruby_patch/linux_patch.rb +24 -0
- data/lib/right_agent/monkey_patches/ruby_patch/linux_patch/file_patch.rb +30 -0
- data/lib/right_agent/monkey_patches/ruby_patch/object_patch.rb +49 -0
- data/lib/right_agent/monkey_patches/ruby_patch/windows_patch.rb +32 -0
- data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/file_patch.rb +60 -0
- data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/process_patch.rb +63 -0
- data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/stdio_patch.rb +27 -0
- data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/time_patch.rb +55 -0
- data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/win32ole_patch.rb +34 -0
- data/lib/right_agent/multiplexer.rb +102 -0
- data/lib/right_agent/offline_handler.rb +270 -0
- data/lib/right_agent/operation_result.rb +300 -0
- data/lib/right_agent/packets.rb +673 -0
- data/lib/right_agent/payload_formatter.rb +104 -0
- data/lib/right_agent/pending_requests.rb +128 -0
- data/lib/right_agent/pid_file.rb +159 -0
- data/lib/right_agent/platform.rb +770 -0
- data/lib/right_agent/platform/unix/darwin/platform.rb +102 -0
- data/lib/right_agent/platform/unix/linux/platform.rb +305 -0
- data/lib/right_agent/platform/unix/platform.rb +226 -0
- data/lib/right_agent/platform/windows/mingw/platform.rb +447 -0
- data/lib/right_agent/platform/windows/mswin/platform.rb +236 -0
- data/lib/right_agent/platform/windows/platform.rb +1808 -0
- data/lib/right_agent/protocol_version_mixin.rb +69 -0
- data/lib/right_agent/retryable_request.rb +195 -0
- data/lib/right_agent/scripts/agent_controller.rb +543 -0
- data/lib/right_agent/scripts/agent_deployer.rb +400 -0
- data/lib/right_agent/scripts/common_parser.rb +160 -0
- data/lib/right_agent/scripts/log_level_manager.rb +192 -0
- data/lib/right_agent/scripts/stats_manager.rb +268 -0
- data/lib/right_agent/scripts/usage.rb +58 -0
- data/lib/right_agent/secure_identity.rb +92 -0
- data/lib/right_agent/security.rb +32 -0
- data/lib/right_agent/security/cached_certificate_store_proxy.rb +77 -0
- data/lib/right_agent/security/certificate.rb +102 -0
- data/lib/right_agent/security/certificate_cache.rb +89 -0
- data/lib/right_agent/security/distinguished_name.rb +56 -0
- data/lib/right_agent/security/encrypted_document.rb +83 -0
- data/lib/right_agent/security/rsa_key_pair.rb +76 -0
- data/lib/right_agent/security/signature.rb +86 -0
- data/lib/right_agent/security/static_certificate_store.rb +85 -0
- data/lib/right_agent/sender.rb +792 -0
- data/lib/right_agent/serialize.rb +29 -0
- data/lib/right_agent/serialize/message_pack.rb +107 -0
- data/lib/right_agent/serialize/secure_serializer.rb +151 -0
- data/lib/right_agent/serialize/secure_serializer_initializer.rb +47 -0
- data/lib/right_agent/serialize/serializable.rb +151 -0
- data/lib/right_agent/serialize/serializer.rb +159 -0
- data/lib/right_agent/subprocess.rb +38 -0
- data/lib/right_agent/tracer.rb +124 -0
- data/right_agent.gemspec +101 -0
- data/spec/actor_registry_spec.rb +80 -0
- data/spec/actor_spec.rb +162 -0
- data/spec/agent_config_spec.rb +235 -0
- data/spec/agent_identity_spec.rb +78 -0
- data/spec/agent_spec.rb +734 -0
- data/spec/agent_tag_manager_spec.rb +319 -0
- data/spec/clients/api_client_spec.rb +423 -0
- data/spec/clients/auth_client_spec.rb +272 -0
- data/spec/clients/balanced_http_client_spec.rb +576 -0
- data/spec/clients/base_retry_client_spec.rb +635 -0
- data/spec/clients/router_client_spec.rb +594 -0
- data/spec/clients/spec_helper.rb +111 -0
- data/spec/command/agent_manager_commands_spec.rb +51 -0
- data/spec/command/command_io_spec.rb +93 -0
- data/spec/command/command_parser_spec.rb +79 -0
- data/spec/command/command_runner_spec.rb +107 -0
- data/spec/command/command_serializer_spec.rb +51 -0
- data/spec/connectivity_checker_spec.rb +83 -0
- data/spec/core_payload_types/dev_repositories_spec.rb +64 -0
- data/spec/core_payload_types/dev_repository_spec.rb +33 -0
- data/spec/core_payload_types/executable_bundle_spec.rb +67 -0
- data/spec/core_payload_types/login_user_spec.rb +102 -0
- data/spec/core_payload_types/recipe_instantiation_spec.rb +81 -0
- data/spec/core_payload_types/right_script_attachment_spec.rb +65 -0
- data/spec/core_payload_types/right_script_instantiation_spec.rb +79 -0
- data/spec/core_payload_types/spec_helper.rb +23 -0
- data/spec/dispatched_cache_spec.rb +136 -0
- data/spec/dispatcher_spec.rb +324 -0
- data/spec/enrollment_result_spec.rb +53 -0
- data/spec/history_spec.rb +246 -0
- data/spec/log_spec.rb +192 -0
- data/spec/monkey_patches/eventmachine_spec.rb +62 -0
- data/spec/multiplexer_spec.rb +48 -0
- data/spec/offline_handler_spec.rb +340 -0
- data/spec/operation_result_spec.rb +208 -0
- data/spec/packets_spec.rb +461 -0
- data/spec/pending_requests_spec.rb +136 -0
- data/spec/platform/spec_helper.rb +216 -0
- data/spec/platform/unix/darwin/platform_spec.rb +181 -0
- data/spec/platform/unix/linux/platform_spec.rb +540 -0
- data/spec/platform/unix/spec_helper.rb +149 -0
- data/spec/platform/windows/mingw/platform_spec.rb +222 -0
- data/spec/platform/windows/mswin/platform_spec.rb +259 -0
- data/spec/platform/windows/spec_helper.rb +720 -0
- data/spec/retryable_request_spec.rb +306 -0
- data/spec/secure_identity_spec.rb +50 -0
- data/spec/security/cached_certificate_store_proxy_spec.rb +62 -0
- data/spec/security/certificate_cache_spec.rb +71 -0
- data/spec/security/certificate_spec.rb +49 -0
- data/spec/security/distinguished_name_spec.rb +46 -0
- data/spec/security/encrypted_document_spec.rb +55 -0
- data/spec/security/rsa_key_pair_spec.rb +55 -0
- data/spec/security/signature_spec.rb +66 -0
- data/spec/security/static_certificate_store_spec.rb +58 -0
- data/spec/sender_spec.rb +1045 -0
- data/spec/serialize/message_pack_spec.rb +131 -0
- data/spec/serialize/secure_serializer_spec.rb +132 -0
- data/spec/serialize/serializable_spec.rb +90 -0
- data/spec/serialize/serializer_spec.rb +197 -0
- data/spec/spec.opts +2 -0
- data/spec/spec.win32.opts +1 -0
- data/spec/spec_helper.rb +130 -0
- data/spec/tracer_spec.rb +114 -0
- 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
|