right_agent 1.0.1 → 2.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +10 -8
- data/Rakefile +31 -5
- data/lib/right_agent.rb +6 -1
- data/lib/right_agent/actor.rb +4 -20
- data/lib/right_agent/actors/agent_manager.rb +1 -1
- data/lib/right_agent/agent.rb +357 -144
- data/lib/right_agent/agent_config.rb +7 -6
- data/lib/right_agent/agent_identity.rb +13 -11
- data/lib/right_agent/agent_tag_manager.rb +60 -64
- data/{spec/results_mock.rb → lib/right_agent/clients.rb} +10 -24
- data/lib/right_agent/clients/api_client.rb +383 -0
- data/lib/right_agent/clients/auth_client.rb +247 -0
- data/lib/right_agent/clients/balanced_http_client.rb +369 -0
- data/lib/right_agent/clients/base_retry_client.rb +495 -0
- data/lib/right_agent/clients/right_http_client.rb +279 -0
- data/lib/right_agent/clients/router_client.rb +493 -0
- data/lib/right_agent/command/command_io.rb +4 -4
- data/lib/right_agent/command/command_parser.rb +2 -2
- data/lib/right_agent/command/command_runner.rb +1 -1
- data/lib/right_agent/connectivity_checker.rb +179 -0
- data/lib/right_agent/core_payload_types/secure_document_location.rb +2 -2
- data/lib/right_agent/dispatcher.rb +12 -10
- data/lib/right_agent/enrollment_result.rb +16 -12
- data/lib/right_agent/exceptions.rb +34 -20
- data/lib/right_agent/history.rb +10 -5
- data/lib/right_agent/log.rb +5 -5
- data/lib/right_agent/minimal.rb +1 -0
- data/lib/right_agent/multiplexer.rb +1 -1
- data/lib/right_agent/offline_handler.rb +270 -0
- data/lib/right_agent/packets.rb +7 -7
- data/lib/right_agent/payload_formatter.rb +1 -1
- data/lib/right_agent/pending_requests.rb +128 -0
- data/lib/right_agent/platform.rb +1 -1
- data/lib/right_agent/protocol_version_mixin.rb +69 -0
- data/lib/right_agent/{idempotent_request.rb → retryable_request.rb} +7 -7
- data/lib/right_agent/scripts/agent_controller.rb +28 -26
- data/lib/right_agent/scripts/agent_deployer.rb +37 -22
- data/lib/right_agent/scripts/common_parser.rb +10 -3
- data/lib/right_agent/secure_identity.rb +1 -1
- data/lib/right_agent/sender.rb +299 -785
- data/lib/right_agent/serialize/secure_serializer.rb +3 -1
- data/lib/right_agent/serialize/secure_serializer_initializer.rb +2 -2
- data/lib/right_agent/serialize/serializable.rb +8 -3
- data/right_agent.gemspec +49 -18
- data/spec/agent_config_spec.rb +7 -7
- data/spec/agent_identity_spec.rb +7 -4
- data/spec/agent_spec.rb +43 -7
- data/spec/agent_tag_manager_spec.rb +72 -83
- data/spec/clients/api_client_spec.rb +423 -0
- data/spec/clients/auth_client_spec.rb +272 -0
- data/spec/clients/balanced_http_client_spec.rb +576 -0
- data/spec/clients/base_retry_client_spec.rb +635 -0
- data/spec/clients/router_client_spec.rb +594 -0
- data/spec/clients/spec_helper.rb +111 -0
- data/spec/command/command_io_spec.rb +1 -1
- data/spec/command/command_parser_spec.rb +1 -1
- data/spec/connectivity_checker_spec.rb +83 -0
- data/spec/dispatcher_spec.rb +3 -2
- data/spec/enrollment_result_spec.rb +2 -2
- data/spec/history_spec.rb +51 -39
- data/spec/offline_handler_spec.rb +340 -0
- data/spec/pending_requests_spec.rb +136 -0
- data/spec/{idempotent_request_spec.rb → retryable_request_spec.rb} +73 -73
- data/spec/sender_spec.rb +835 -1052
- data/spec/serialize/secure_serializer_spec.rb +3 -2
- data/spec/spec_helper.rb +54 -1
- metadata +71 -12
data/lib/right_agent/platform.rb
CHANGED
@@ -170,7 +170,7 @@ unless defined?(RightScale::Platform)
|
|
170
170
|
end
|
171
171
|
raise "No platform dispatch target found in #{binding.class} for " +
|
172
172
|
"'#{meth.inspect}', tried " + dispatch_candidates(meth).join(', ') unless target
|
173
|
-
binding.
|
173
|
+
binding.send(target, *args)
|
174
174
|
end
|
175
175
|
|
176
176
|
# @return [Controller] Platform-specific controller object
|
@@ -0,0 +1,69 @@
|
|
1
|
+
##
|
2
|
+
# Copyright (c) 2014 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
|
+
module RightScale
|
24
|
+
|
25
|
+
# Mixin for testing for availability of features in agents based on protocol version
|
26
|
+
module ProtocolVersionMixin
|
27
|
+
|
28
|
+
# Mix this module into its own eigenclass to make the module-instance methods
|
29
|
+
# become callable as module methods
|
30
|
+
extend self
|
31
|
+
|
32
|
+
# Test whether given version of agent has the protocol version embedded in each
|
33
|
+
# packet (this is generally inferred by the version in the received packet not being
|
34
|
+
# Packet::DEFAULT_VERSION, which is true of all with version >= 12)
|
35
|
+
def can_put_version_in_packet?(version); version && version != 0 end
|
36
|
+
|
37
|
+
# Test whether given version of agent uses /mapper/query_tags rather than the
|
38
|
+
# deprecated TagQuery packet
|
39
|
+
def can_use_mapper_query_tags?(version); version && version >= 8 end
|
40
|
+
|
41
|
+
# Test whether given version of agent can handle a request that is being retried
|
42
|
+
# as indicated by a retries count in the Request packet
|
43
|
+
def can_handle_request_retries?(version); version && version >= 9 end
|
44
|
+
|
45
|
+
# Test whether given version of agent can handle serialized identity and queue name
|
46
|
+
# that does not incorporate 'nanite'
|
47
|
+
def can_handle_non_nanite_ids?(version); version && version >= 10 end
|
48
|
+
|
49
|
+
# Test whether given version of agent supports routing results to mapper response queue
|
50
|
+
# rather than to the identity queue of the mapper that routed the request
|
51
|
+
def can_route_to_response_queue?(version); version && version >= 10 end
|
52
|
+
|
53
|
+
# Test whether given version of agent can handle receipt of a result containing an
|
54
|
+
# OperationResult with MULTICAST status
|
55
|
+
def can_handle_multicast_result?(version); version && [10, 11].include?(version) end
|
56
|
+
|
57
|
+
# Test whether given version of agent can handle msgpack encoding
|
58
|
+
def can_handle_msgpack_result?(version); version && version >= 12 end
|
59
|
+
|
60
|
+
# Test whether given version of agent can handle receipt of a result containing an
|
61
|
+
# OperationResult with NON_DELIVERY status
|
62
|
+
def can_handle_non_delivery_result?(version); version && version >= 13 end
|
63
|
+
|
64
|
+
# Test whether given version of agent can handle HTTP communication mode
|
65
|
+
def can_handle_http?(version); version && version >= 23 end
|
66
|
+
|
67
|
+
end # ProtocolVersionMixin
|
68
|
+
|
69
|
+
end # RightScale
|
@@ -42,7 +42,7 @@ module RightScale
|
|
42
42
|
# applied after the new :retry_delay_count is reached, and so on until :retry_delay
|
43
43
|
# reaches :max_retry_delay which then is used as the interval until the default or
|
44
44
|
# specified :timeout is reached. The default :timeout is 4 days.
|
45
|
-
class
|
45
|
+
class RetryableRequest
|
46
46
|
|
47
47
|
include OperationResultHelper
|
48
48
|
include EM::Deferrable
|
@@ -72,7 +72,7 @@ module RightScale
|
|
72
72
|
# operation(String):: Request operation (e.g., '/booter/get_boot_bundle')
|
73
73
|
# payload(Hash):: Request payload
|
74
74
|
# options(Hash):: Request options
|
75
|
-
# :targets(Array)::
|
75
|
+
# :targets(Array):: Target agent identities from which to randomly choose one
|
76
76
|
# :retry_on_error(Boolean):: Whether request should be retried if recipient returned an error
|
77
77
|
# :retry_delay(Fixnum):: Number of seconds delay before initial retry with -1 meaning no delay,
|
78
78
|
# defaults to DEFAULT_RETRY_DELAY
|
@@ -86,8 +86,8 @@ module RightScale
|
|
86
86
|
# === Raises
|
87
87
|
# ArgumentError:: If operation or payload not specified
|
88
88
|
def initialize(operation, payload, options = {})
|
89
|
-
raise ArgumentError.new("operation is required") unless @operation = operation
|
90
|
-
raise ArgumentError.new("payload is required") unless @payload = payload
|
89
|
+
raise ArgumentError.new("operation is required") unless (@operation = operation)
|
90
|
+
raise ArgumentError.new("payload is required") unless (@payload = payload)
|
91
91
|
@retry_on_error = options[:retry_on_error] || false
|
92
92
|
@timeout = options[:timeout] || DEFAULT_TIMEOUT
|
93
93
|
@retry_delay = options[:retry_delay] || DEFAULT_RETRY_DELAY
|
@@ -105,7 +105,7 @@ module RightScale
|
|
105
105
|
# === Return
|
106
106
|
# true:: Always return true
|
107
107
|
def run
|
108
|
-
Sender.instance.
|
108
|
+
Sender.instance.send_request(@operation, @payload, retrieve_target(@targets)) { |r| handle_response(r) }
|
109
109
|
if @cancel_timer.nil? && @timeout > 0
|
110
110
|
@cancel_timer = EM::Timer.new(@timeout) do
|
111
111
|
msg = "Request #{@operation} timed out after #{@timeout} seconds"
|
@@ -187,9 +187,9 @@ module RightScale
|
|
187
187
|
end
|
188
188
|
|
189
189
|
def retrieve_target(targets)
|
190
|
-
targets[rand(0xffff) % targets.size] if targets
|
190
|
+
{:agent_id => targets[rand(0xffff) % targets.size]} if targets && targets.any?
|
191
191
|
end
|
192
192
|
|
193
|
-
end #
|
193
|
+
end # RetryableRequest
|
194
194
|
|
195
195
|
end # RightScale
|
@@ -45,31 +45,33 @@
|
|
45
45
|
# rnac [options]
|
46
46
|
#
|
47
47
|
# options:
|
48
|
-
# --start, -s AGENT
|
49
|
-
# --stop, -p AGENT
|
50
|
-
# --stop-agent ID
|
51
|
-
# --kill, -k PIDFILE
|
52
|
-
# --killall, -K
|
53
|
-
# --status, -U
|
54
|
-
# --identity, -i ID
|
55
|
-
# --token, -t TOKEN
|
56
|
-
#
|
57
|
-
# --
|
58
|
-
#
|
59
|
-
# --
|
60
|
-
#
|
61
|
-
# --
|
62
|
-
# --
|
63
|
-
# --
|
64
|
-
# --
|
65
|
-
# --
|
66
|
-
# --
|
67
|
-
# --
|
68
|
-
# --
|
69
|
-
# --
|
70
|
-
# --
|
71
|
-
# --
|
72
|
-
# --
|
48
|
+
# --start, -s AGENT Start agent named AGENT
|
49
|
+
# --stop, -p AGENT Stop agent named AGENT
|
50
|
+
# --stop-agent ID Stop agent with serialized identity ID
|
51
|
+
# --kill, -k PIDFILE Kill process with given process id file
|
52
|
+
# --killall, -K Stop all running agents
|
53
|
+
# --status, -U List running agents on local machine
|
54
|
+
# --identity, -i ID Use this as base ID to build agent's identity
|
55
|
+
# --token, -t TOKEN Use this token to build agent's identity with it plugging
|
56
|
+
# directly in unless --secure-identity is specified
|
57
|
+
# --secure-identity, -S Derive token used in agent identity from given TOKEN and ID
|
58
|
+
# --prefix, -x PREFIX Use this prefix to build agent's identity
|
59
|
+
# --type TYPE Use this agent type to build agent's' identity;
|
60
|
+
# defaults to AGENT with any trailing '_[0-9]+' removed
|
61
|
+
# --list, -l List all configured agents
|
62
|
+
# --user, -u USER Set AMQP user
|
63
|
+
# --pass, -p PASS Set AMQP password
|
64
|
+
# --vhost, -v VHOST Set AMQP vhost
|
65
|
+
# --host, -h HOST Set AMQP server hostname
|
66
|
+
# --port, -P PORT Set AMQP server port
|
67
|
+
# --cfg-dir, -c DIR Set directory containing configuration for all agents
|
68
|
+
# --pid-dir, -z DIR Set directory containing agent process id files
|
69
|
+
# --log-dir DIR Set log directory
|
70
|
+
# --log-level LVL Log level (debug, info, warning, error or fatal)
|
71
|
+
# --foreground, -f Run agent in foreground
|
72
|
+
# --interactive, -I Spawn an irb console after starting agent
|
73
|
+
# --test Use test settings
|
74
|
+
# --help Display help
|
73
75
|
|
74
76
|
require 'rubygems'
|
75
77
|
require 'optparse'
|
@@ -333,7 +335,7 @@ module RightScale
|
|
333
335
|
EM.error_handler do |e|
|
334
336
|
Log.error("EM block execution failed with exception", e, :trace)
|
335
337
|
Log.error("\n\n===== Exiting due to EM block exception =====\n\n")
|
336
|
-
# Cannot rely on EM.stop at this point, so exit to give chance for
|
338
|
+
# Cannot rely on EM.stop at this point, so exit to give chance for monitor restart
|
337
339
|
exit(1)
|
338
340
|
end
|
339
341
|
|
@@ -7,14 +7,6 @@
|
|
7
7
|
# <agent name>/config.yml
|
8
8
|
# in platform-specific RightAgent configuration directory
|
9
9
|
#
|
10
|
-
# === Examples:
|
11
|
-
# Build configuration for agent named AGENT with default options:
|
12
|
-
# rad AGENT
|
13
|
-
#
|
14
|
-
# Build configuration for agent named AGENT so it uses given AMQP settings:
|
15
|
-
# rad AGENT --user USER --pass PASSWORD --vhost VHOST --port PORT --host HOST
|
16
|
-
# rad AGENT -u USER -p PASSWORD -v VHOST -P PORT -h HOST
|
17
|
-
#
|
18
10
|
# === Usage:
|
19
11
|
# rad AGENT [options]
|
20
12
|
#
|
@@ -22,13 +14,18 @@
|
|
22
14
|
# --root-dir, -r DIR Set agent root directory (containing init, actors, and certs subdirectories)
|
23
15
|
# --cfg-dir, -c DIR Set directory where generated configuration files for all agents are stored
|
24
16
|
# --pid-dir, -z DIR Set directory containing process id file
|
25
|
-
# --identity, -i ID Use base
|
26
|
-
# --token, -t TOKEN Use token
|
27
|
-
#
|
28
|
-
# --
|
17
|
+
# --identity, -i ID Use this as base ID to build agent's identity
|
18
|
+
# --token, -t TOKEN Use this token to build agent's identity with it plugging
|
19
|
+
# directly in unless --secure-identity is specified
|
20
|
+
# --secure-identity, -S Derive token used in agent identity from given TOKEN and ID
|
21
|
+
# --prefix, -x PREFIX Use this prefix to build agent's identity
|
22
|
+
# --type TYPE Use this agent type to build agent's' identity;
|
29
23
|
# defaults to AGENT with any trailing '_[0-9]+' removed
|
30
|
-
# --
|
31
|
-
# --
|
24
|
+
# --api-url, -a URL Set URL for HTTP access to RightApi
|
25
|
+
# --account, -A ID Set identifier for account owning this agent
|
26
|
+
# --shard, -s ID Set identifier for database shard in which this agent is operating
|
27
|
+
# --mode, -m MODE Set communication mode this agent is to use: :http or :amqp
|
28
|
+
# --url URL Set agent AMQP connection URL (host, port, user, pass, vhost)
|
32
29
|
# --user, -u USER Set agent AMQP username
|
33
30
|
# --password, -p PASS Set agent AMQP password
|
34
31
|
# --vhost, -v VHOST Set agent AMQP virtual host
|
@@ -43,12 +40,12 @@
|
|
43
40
|
# --retry-interval SEC Set number of seconds before initial request retry, increases exponentially
|
44
41
|
# --check-interval SEC Set number of seconds between failed connection checks, increases exponentially
|
45
42
|
# --ping-interval SEC Set minimum number of seconds since last message receipt for the agent
|
46
|
-
# to ping the
|
47
|
-
# --reconnect-interval SEC Set number of seconds between
|
43
|
+
# to ping the RightNet router to check connectivity, 0 means disable ping
|
44
|
+
# --reconnect-interval SEC Set number of seconds between HTTP or AMQP reconnect attempts
|
48
45
|
# --grace-timeout SEC Set number of seconds before graceful termination times out
|
49
46
|
# --[no-]dup-check Set whether to check for and reject duplicate requests, .e.g., due to retries
|
50
47
|
# --options, -o KEY=VAL Set options that act as final override for any persisted configuration settings
|
51
|
-
# --monit
|
48
|
+
# --monit Generate monit configuration file
|
52
49
|
# --test Build test deployment using default test settings
|
53
50
|
# --quiet, -Q Do not produce output
|
54
51
|
# --help Display help
|
@@ -135,14 +132,10 @@ module RightScale
|
|
135
132
|
options[:pid_dir] = d
|
136
133
|
end
|
137
134
|
|
138
|
-
opts.on('
|
135
|
+
opts.on('--monit') do
|
139
136
|
options[:monit] = true
|
140
137
|
end
|
141
138
|
|
142
|
-
opts.on('-S', '--secure-identity') do
|
143
|
-
options[:secure_identity] = true
|
144
|
-
end
|
145
|
-
|
146
139
|
opts.on('--http-proxy PROXY') do |proxy|
|
147
140
|
options[:http_proxy] = proxy
|
148
141
|
end
|
@@ -187,6 +180,22 @@ module RightScale
|
|
187
180
|
options[:prefetch] = count.to_i
|
188
181
|
end
|
189
182
|
|
183
|
+
opts.on("-a", "--api-url URL") do |url|
|
184
|
+
options[:api_url] = url
|
185
|
+
end
|
186
|
+
|
187
|
+
opts.on('-A', '--account ID') do |id|
|
188
|
+
options[:account_id] = id.to_i
|
189
|
+
end
|
190
|
+
|
191
|
+
opts.on('-s', '--shard ID') do |id|
|
192
|
+
options[:shard_id] = id.to_i
|
193
|
+
end
|
194
|
+
|
195
|
+
opts.on('-m', '--mode MODE') do |mode|
|
196
|
+
options[:mode] = mode
|
197
|
+
end
|
198
|
+
|
190
199
|
opts.on('-b', '--heartbeat SEC') do |sec|
|
191
200
|
options[:heartbeat] = sec.to_i
|
192
201
|
end
|
@@ -281,6 +290,11 @@ module RightScale
|
|
281
290
|
cfg[:root_dir] = AgentConfig.root_dir
|
282
291
|
cfg[:pid_dir] = AgentConfig.pid_dir
|
283
292
|
cfg[:identity] = options[:identity] if options[:identity]
|
293
|
+
cfg[:token] = options[:token] if options[:token]
|
294
|
+
cfg[:api_url] = options[:api_url] if options[:api_url]
|
295
|
+
cfg[:account_id] = options[:account_id] if options[:account_id]
|
296
|
+
cfg[:shard_id] = options[:shard_id] if options[:shard_id]
|
297
|
+
cfg[:mode] = options[:mode] if options[:mode]
|
284
298
|
cfg[:user] = options[:user] if options[:user]
|
285
299
|
cfg[:pass] = options[:pass] if options[:pass]
|
286
300
|
cfg[:vhost] = options[:vhost] if options[:vhost]
|
@@ -344,6 +358,7 @@ check process #{agent_name}
|
|
344
358
|
true
|
345
359
|
end
|
346
360
|
|
361
|
+
|
347
362
|
# Print error on console and exit abnormally
|
348
363
|
#
|
349
364
|
# === Parameters
|
@@ -58,6 +58,10 @@ module RightScale
|
|
58
58
|
options[:token] = t
|
59
59
|
end
|
60
60
|
|
61
|
+
opts.on("-S", "--secure-identity") do
|
62
|
+
options[:secure_identity] = true
|
63
|
+
end
|
64
|
+
|
61
65
|
opts.on("-x", "--prefix PREFIX") do |p|
|
62
66
|
options[:prefix] = p
|
63
67
|
end
|
@@ -107,7 +111,7 @@ module RightScale
|
|
107
111
|
true
|
108
112
|
end
|
109
113
|
|
110
|
-
# Generate agent
|
114
|
+
# Generate agent identity from options
|
111
115
|
# Build identity from base_id, token, prefix and agent name
|
112
116
|
#
|
113
117
|
# === Parameters
|
@@ -123,8 +127,11 @@ module RightScale
|
|
123
127
|
puts "** Identity needs to be a positive integer"
|
124
128
|
exit(1)
|
125
129
|
end
|
126
|
-
token = options[:
|
127
|
-
|
130
|
+
token = if options[:secure_identity]
|
131
|
+
RightScale::SecureIdentity.derive(base_id, options[:token])
|
132
|
+
else
|
133
|
+
options[:token]
|
134
|
+
end
|
128
135
|
options[:identity] = AgentIdentity.new(options[:prefix] || 'rs', options[:agent_type], base_id, token).to_s
|
129
136
|
end
|
130
137
|
end
|
@@ -48,7 +48,7 @@ module RightScale
|
|
48
48
|
# as the name of an agent.
|
49
49
|
#
|
50
50
|
# Public tokens are generated by taking the SHA1 hash of the base ID and the
|
51
|
-
# auth token, separated by a
|
51
|
+
# auth token, separated by a delimiter. Thus a public token can always be
|
52
52
|
# deterministically derived from its inputs.
|
53
53
|
#
|
54
54
|
# === Parameters
|
data/lib/right_agent/sender.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
#
|
2
|
-
# Copyright (c) 2009-
|
2
|
+
# Copyright (c) 2009-2013 RightScale Inc
|
3
3
|
#
|
4
4
|
# Permission is hereby granted, free of charge, to any person obtaining
|
5
5
|
# a copy of this software and associated documentation files (the
|
@@ -22,541 +22,40 @@
|
|
22
22
|
|
23
23
|
module RightScale
|
24
24
|
|
25
|
-
#
|
25
|
+
# TODO require target to be hash or nil only once cleanup string values in RightLink usage
|
26
|
+
# TODO require payload to be hash or nil only once cleanup RightApi and RightLink usage
|
27
|
+
|
28
|
+
# This class allows sending requests to agents via RightNet
|
26
29
|
# It is used by Actor.request which is used by actors that need to send requests to remote agents
|
27
|
-
# If requested, it will queue requests when there are no
|
28
|
-
# All requests go through the mapper for security purposes
|
30
|
+
# If requested, it will queue requests when there are is no RightNet connection
|
29
31
|
class Sender
|
30
32
|
|
31
|
-
|
32
|
-
class TemporarilyOffline < Exception; end
|
33
|
-
|
34
|
-
# Request that is waiting for a response
|
35
|
-
class PendingRequest
|
36
|
-
|
37
|
-
# (Symbol) Kind of send request
|
38
|
-
attr_reader :kind
|
39
|
-
|
40
|
-
# (Time) Time when request message was received
|
41
|
-
attr_reader :receive_time
|
42
|
-
|
43
|
-
# (Proc) Block to be activated when response is received
|
44
|
-
attr_reader :response_handler
|
45
|
-
|
46
|
-
# (String) Token for parent request in a retry situation
|
47
|
-
attr_accessor :retry_parent
|
48
|
-
|
49
|
-
# (String) Non-delivery reason if any
|
50
|
-
attr_accessor :non_delivery
|
51
|
-
|
52
|
-
def initialize(kind, receive_time, response_handler)
|
53
|
-
@kind = kind
|
54
|
-
@receive_time = receive_time
|
55
|
-
@response_handler = response_handler
|
56
|
-
@retry_parent = nil
|
57
|
-
@non_delivery = nil
|
58
|
-
end
|
59
|
-
|
60
|
-
end # PendingRequest
|
61
|
-
|
62
|
-
# Cache for requests that are waiting for a response
|
63
|
-
# Automatically deletes push requests when get too old
|
64
|
-
# Retains non-push requests until explicitly deleted
|
65
|
-
class PendingRequests < Hash
|
66
|
-
|
67
|
-
# Kinds of send requests
|
68
|
-
REQUEST_KINDS = [:send_retryable_request, :send_persistent_request]
|
69
|
-
|
70
|
-
# Kinds of send pushes
|
71
|
-
PUSH_KINDS = [:send_push, :send_persistent_push]
|
72
|
-
|
73
|
-
# Maximum number of seconds to retain send pushes in cache
|
74
|
-
MAX_PUSH_AGE = 2 * 60
|
75
|
-
|
76
|
-
# Minimum number of seconds between push cleanups
|
77
|
-
MIN_CLEANUP_INTERVAL = 15
|
78
|
-
|
79
|
-
# Create cache
|
80
|
-
def initialize
|
81
|
-
@last_cleanup = Time.now
|
82
|
-
super
|
83
|
-
end
|
84
|
-
|
85
|
-
# Store pending request
|
86
|
-
#
|
87
|
-
# === Parameters
|
88
|
-
# token(String):: Generated message identifier
|
89
|
-
# request(PendingRequest):: Pending request
|
90
|
-
#
|
91
|
-
# === Return
|
92
|
-
# (PendingRequest):: Stored request
|
93
|
-
def []=(token, request)
|
94
|
-
now = Time.now
|
95
|
-
if (now - @last_cleanup) > MIN_CLEANUP_INTERVAL
|
96
|
-
self.reject! { |t, r| PUSH_KINDS.include?(r.kind) && (now - r.receive_time) > MAX_PUSH_AGE }
|
97
|
-
@last_cleanup = now
|
98
|
-
end
|
99
|
-
super
|
100
|
-
end
|
101
|
-
|
102
|
-
# Select cache entries of the given kinds
|
103
|
-
#
|
104
|
-
# === Parameters
|
105
|
-
# kinds(Array):: Kind of requests to be included
|
106
|
-
#
|
107
|
-
# === Return
|
108
|
-
# (Hash):: Requests of specified kind
|
109
|
-
def kind(kinds)
|
110
|
-
self.reject { |t, r| !kinds.include?(r.kind) }
|
111
|
-
end
|
112
|
-
|
113
|
-
# Get age of youngest pending request
|
114
|
-
#
|
115
|
-
# === Return
|
116
|
-
# age(Integer):: Age of youngest request
|
117
|
-
def youngest_age
|
118
|
-
now = Time.now
|
119
|
-
age = nil
|
120
|
-
self.each_value do |r|
|
121
|
-
seconds = (now - r.receive_time).to_i
|
122
|
-
age = seconds if age.nil? || seconds < age
|
123
|
-
end
|
124
|
-
age
|
125
|
-
end
|
126
|
-
|
127
|
-
# Get age of oldest pending request
|
128
|
-
#
|
129
|
-
# === Return
|
130
|
-
# age(Integer):: Age of oldest request
|
131
|
-
def oldest_age
|
132
|
-
now = Time.now
|
133
|
-
age = nil
|
134
|
-
self.each_value do |r|
|
135
|
-
seconds = (now - r.receive_time).to_i
|
136
|
-
age = seconds if age.nil? || seconds > age
|
137
|
-
end
|
138
|
-
age
|
139
|
-
end
|
140
|
-
|
141
|
-
end # PendingRequests
|
142
|
-
|
143
|
-
# Queue for storing requests while disconnected from broker and then sending
|
144
|
-
# them when successfully reconnect
|
145
|
-
class OfflineHandler
|
146
|
-
|
147
|
-
# Maximum seconds to wait before starting flushing offline queue when disabling offline mode
|
148
|
-
MAX_QUEUE_FLUSH_DELAY = 2 * 60
|
149
|
-
|
150
|
-
# Maximum number of offline queued requests before triggering restart vote
|
151
|
-
MAX_QUEUED_REQUESTS = 100
|
152
|
-
|
153
|
-
# Number of seconds that should be spent in offline mode before triggering a restart vote
|
154
|
-
RESTART_VOTE_DELAY = 15 * 60
|
155
|
-
|
156
|
-
# (Symbol) Current queue state with possible values:
|
157
|
-
# Value Description Action Next state
|
158
|
-
# :created Queue created init :initializing
|
159
|
-
# :initializing Agent still initializing start :running
|
160
|
-
# :running Queue has been started disable when offline :flushing
|
161
|
-
# :flushing Sending queued requests enable :running
|
162
|
-
# :terminating Agent terminating
|
163
|
-
attr_reader :state
|
164
|
-
|
165
|
-
# (Symbol) Current offline handling mode with possible values:
|
166
|
-
# Value Description
|
167
|
-
# :initializing Agent still initializing
|
168
|
-
# :online Agent connected to broker
|
169
|
-
# :offline Agent disconnected from broker
|
170
|
-
attr_reader :mode
|
171
|
-
|
172
|
-
# (Array) Offline queue
|
173
|
-
attr_accessor :queue
|
174
|
-
|
175
|
-
# Create offline queue
|
176
|
-
#
|
177
|
-
# === Parameters
|
178
|
-
# restart_callback(Proc):: Callback that is activated on each restart vote with votes being initiated
|
179
|
-
# by offline queue exceeding MAX_QUEUED_REQUESTS
|
180
|
-
# offline_stats(RightSupport::Stats::Activity):: Offline queue tracking statistics
|
181
|
-
def initialize(restart_callback, offline_stats)
|
182
|
-
@restart_vote = restart_callback
|
183
|
-
@restart_vote_timer = nil
|
184
|
-
@restart_vote_count = 0
|
185
|
-
@offline_stats = offline_stats
|
186
|
-
@state = :created
|
187
|
-
@mode = :initializing
|
188
|
-
@queue = []
|
189
|
-
end
|
190
|
-
|
191
|
-
# Initialize the offline queue
|
192
|
-
# All requests sent prior to running this initialization are queued
|
193
|
-
# and then are sent once this initialization has run
|
194
|
-
# All requests following this call and prior to calling start
|
195
|
-
# are prepended to the request queue
|
196
|
-
#
|
197
|
-
# === Return
|
198
|
-
# true:: Always return true
|
199
|
-
def init
|
200
|
-
@state = :initializing if @state == :created
|
201
|
-
true
|
202
|
-
end
|
203
|
-
|
204
|
-
# Switch to online mode and send all buffered messages
|
205
|
-
#
|
206
|
-
# === Return
|
207
|
-
# true:: Always return true
|
208
|
-
def start
|
209
|
-
if @state == :initializing
|
210
|
-
if @mode == :offline
|
211
|
-
@state = :running
|
212
|
-
else
|
213
|
-
@state = :flushing
|
214
|
-
flush
|
215
|
-
end
|
216
|
-
@mode = :online if @mode == :initializing
|
217
|
-
end
|
218
|
-
true
|
219
|
-
end
|
220
|
-
|
221
|
-
# Is agent currently offline?
|
222
|
-
#
|
223
|
-
# === Return
|
224
|
-
# (Boolean):: true if agent offline, otherwise false
|
225
|
-
def offline?
|
226
|
-
@mode == :offline || @state == :created
|
227
|
-
end
|
228
|
-
|
229
|
-
# In request queueing mode?
|
230
|
-
#
|
231
|
-
# === Return
|
232
|
-
# (Boolean):: true if should queue request, otherwise false
|
233
|
-
def queueing?
|
234
|
-
offline? && @state != :flushing
|
235
|
-
end
|
236
|
-
|
237
|
-
# Switch to offline mode
|
238
|
-
# In this mode requests are queued in memory rather than sent to the mapper
|
239
|
-
# Idempotent
|
240
|
-
#
|
241
|
-
# === Return
|
242
|
-
# true:: Always return true
|
243
|
-
def enable
|
244
|
-
if offline?
|
245
|
-
if @state == :flushing
|
246
|
-
# If we were in offline mode then switched back to online but are still in the
|
247
|
-
# process of flushing the in-memory queue and are now switching to offline mode
|
248
|
-
# again then stop the flushing
|
249
|
-
@state = :running
|
250
|
-
end
|
251
|
-
else
|
252
|
-
Log.info("[offline] Disconnect from broker detected, entering offline mode")
|
253
|
-
Log.info("[offline] Messages will be queued in memory until connection to broker is re-established")
|
254
|
-
@offline_stats.update
|
255
|
-
@queue ||= [] # Ensure queue is valid without losing any messages when going offline
|
256
|
-
@mode = :offline
|
257
|
-
start_timer
|
258
|
-
end
|
259
|
-
true
|
260
|
-
end
|
261
|
-
|
262
|
-
# Switch back to sending requests to mapper after in-memory queue gets flushed
|
263
|
-
# Idempotent
|
264
|
-
#
|
265
|
-
# === Return
|
266
|
-
# true:: Always return true
|
267
|
-
def disable
|
268
|
-
if offline? && @state != :created
|
269
|
-
Log.info("[offline] Connection to broker re-established")
|
270
|
-
@offline_stats.finish
|
271
|
-
cancel_timer
|
272
|
-
@state = :flushing
|
273
|
-
# Wait a bit to avoid flooding the mapper
|
274
|
-
EM.add_timer(rand(MAX_QUEUE_FLUSH_DELAY)) { flush }
|
275
|
-
end
|
276
|
-
true
|
277
|
-
end
|
278
|
-
|
279
|
-
# Queue given request in memory
|
280
|
-
#
|
281
|
-
# === Parameters
|
282
|
-
# request(Hash):: Request to be stored
|
283
|
-
#
|
284
|
-
# === Return
|
285
|
-
# true:: Always return true
|
286
|
-
def queue_request(kind, type, payload, target, callback)
|
287
|
-
request = {:kind => kind, :type => type, :payload => payload, :target => target, :callback => callback}
|
288
|
-
Log.info("[offline] Queuing request: #{request.inspect}")
|
289
|
-
vote_to_restart if (@restart_vote_count += 1) >= MAX_QUEUED_REQUESTS
|
290
|
-
if @state == :initializing
|
291
|
-
# We are in the initialization callback, requests should be put at the head of the queue
|
292
|
-
@queue.unshift(request)
|
293
|
-
else
|
294
|
-
@queue << request
|
295
|
-
end
|
296
|
-
true
|
297
|
-
end
|
298
|
-
|
299
|
-
# Prepare for agent termination
|
300
|
-
#
|
301
|
-
# === Return
|
302
|
-
# true:: Always return true
|
303
|
-
def terminate
|
304
|
-
@state = :terminating
|
305
|
-
cancel_timer
|
306
|
-
true
|
307
|
-
end
|
308
|
-
|
309
|
-
protected
|
310
|
-
|
311
|
-
# Send any requests that were queued while in offline mode
|
312
|
-
# Do this asynchronously to allow for agents to respond to requests
|
313
|
-
# Once all in-memory requests have been flushed, switch off offline mode
|
314
|
-
#
|
315
|
-
# === Return
|
316
|
-
# true:: Always return true
|
317
|
-
def flush
|
318
|
-
if @state == :flushing
|
319
|
-
Log.info("[offline] Starting to flush request queue of size #{@queue.size}") unless @mode == :initializing
|
320
|
-
unless @queue.empty?
|
321
|
-
r = @queue.shift
|
322
|
-
if r[:callback]
|
323
|
-
Sender.instance.__send__(r[:kind], r[:type], r[:payload], r[:target]) { |result| r[:callback].call(result) }
|
324
|
-
else
|
325
|
-
Sender.instance.__send__(r[:kind], r[:type], r[:payload], r[:target])
|
326
|
-
end
|
327
|
-
end
|
328
|
-
if @queue.empty?
|
329
|
-
Log.info("[offline] Request queue flushed, resuming normal operations") unless @mode == :initializing
|
330
|
-
@mode = :online
|
331
|
-
@state = :running
|
332
|
-
else
|
333
|
-
EM.next_tick { flush }
|
334
|
-
end
|
335
|
-
end
|
336
|
-
true
|
337
|
-
end
|
338
|
-
|
339
|
-
# Vote for restart and reset trigger
|
340
|
-
#
|
341
|
-
# === Parameters
|
342
|
-
# timer_trigger(Boolean):: true if vote was triggered by timer, false if it
|
343
|
-
# was triggered by number of messages in in-memory queue
|
344
|
-
#
|
345
|
-
# === Return
|
346
|
-
# true:: Always return true
|
347
|
-
def vote_to_restart(timer_trigger = false)
|
348
|
-
if @restart_vote
|
349
|
-
@restart_vote.call
|
350
|
-
if timer_trigger
|
351
|
-
start_timer
|
352
|
-
else
|
353
|
-
@restart_vote_count = 0
|
354
|
-
end
|
355
|
-
end
|
356
|
-
true
|
357
|
-
end
|
358
|
-
|
359
|
-
# Start restart vote timer
|
360
|
-
#
|
361
|
-
# === Return
|
362
|
-
# true:: Always return true
|
363
|
-
def start_timer
|
364
|
-
if @restart_vote && @state != :terminating
|
365
|
-
@restart_vote_timer ||= EM::Timer.new(RESTART_VOTE_DELAY) { vote_to_restart(timer_trigger = true) }
|
366
|
-
end
|
367
|
-
true
|
368
|
-
end
|
369
|
-
|
370
|
-
# Cancel restart vote timer
|
371
|
-
#
|
372
|
-
# === Return
|
373
|
-
# true:: Always return true
|
374
|
-
def cancel_timer
|
375
|
-
if @restart_vote_timer
|
376
|
-
@restart_vote_timer.cancel
|
377
|
-
@restart_vote_timer = nil
|
378
|
-
@restart_vote_count = 0
|
379
|
-
end
|
380
|
-
true
|
381
|
-
end
|
382
|
-
|
383
|
-
end # OfflineHandler
|
384
|
-
|
385
|
-
# Broker connectivity checker
|
386
|
-
# Checks connectivity when requested
|
387
|
-
class ConnectivityChecker
|
388
|
-
|
389
|
-
# Minimum number of seconds between restarts of the inactivity timer
|
390
|
-
MIN_RESTART_INACTIVITY_TIMER_INTERVAL = 60
|
391
|
-
|
392
|
-
# Number of seconds to wait for ping response from a mapper when checking connectivity
|
393
|
-
PING_TIMEOUT = 30
|
394
|
-
|
395
|
-
# Default maximum number of consecutive ping timeouts before attempt to reconnect
|
396
|
-
MAX_PING_TIMEOUTS = 3
|
397
|
-
|
398
|
-
# (EM::Timer) Timer while waiting for mapper ping response
|
399
|
-
attr_accessor :ping_timer
|
400
|
-
|
401
|
-
def initialize(sender, check_interval, ping_stats, exception_stats)
|
402
|
-
@sender = sender
|
403
|
-
@check_interval = check_interval
|
404
|
-
@ping_timeouts = {}
|
405
|
-
@ping_timer = nil
|
406
|
-
@ping_stats = ping_stats
|
407
|
-
@exception_stats = exception_stats
|
408
|
-
@last_received = Time.now
|
409
|
-
@message_received_callbacks = []
|
410
|
-
restart_inactivity_timer if @check_interval > 0
|
411
|
-
end
|
412
|
-
|
413
|
-
# Update the time this agent last received a request or response message
|
414
|
-
# and restart the inactivity timer thus deferring the next connectivity check
|
415
|
-
# Also forward this message receipt notification to any callbacks that have registered
|
416
|
-
#
|
417
|
-
# === Block
|
418
|
-
# Optional block without parameters that is activated when a message is received
|
419
|
-
#
|
420
|
-
# === Return
|
421
|
-
# true:: Always return true
|
422
|
-
def message_received(&callback)
|
423
|
-
if block_given?
|
424
|
-
@message_received_callbacks << callback
|
425
|
-
else
|
426
|
-
@message_received_callbacks.each { |c| c.call }
|
427
|
-
if @check_interval > 0
|
428
|
-
now = Time.now
|
429
|
-
if (now - @last_received) > MIN_RESTART_INACTIVITY_TIMER_INTERVAL
|
430
|
-
@last_received = now
|
431
|
-
restart_inactivity_timer
|
432
|
-
end
|
433
|
-
end
|
434
|
-
end
|
435
|
-
true
|
436
|
-
end
|
437
|
-
|
438
|
-
# Check whether broker connection is usable by pinging a mapper via that broker
|
439
|
-
# Attempt to reconnect if ping does not respond in PING_TIMEOUT seconds and
|
440
|
-
# if have reached timeout limit
|
441
|
-
# Ignore request if already checking a connection
|
442
|
-
#
|
443
|
-
# === Parameters
|
444
|
-
# id(String):: Identity of specific broker to use to send ping, defaults to any
|
445
|
-
# currently connected broker
|
446
|
-
# max_ping_timeouts(Integer):: Maximum number of ping timeouts before attempt
|
447
|
-
# to reconnect, defaults to MAX_PING_TIMEOUTS
|
448
|
-
#
|
449
|
-
# === Return
|
450
|
-
# true:: Always return true
|
451
|
-
def check(id = nil, max_ping_timeouts = MAX_PING_TIMEOUTS)
|
452
|
-
unless @terminating || @ping_timer || (id && !@sender.broker.connected?(id))
|
453
|
-
@ping_id = id
|
454
|
-
@ping_timer = EM::Timer.new(PING_TIMEOUT) do
|
455
|
-
if @ping_id
|
456
|
-
begin
|
457
|
-
@ping_stats.update("timeout")
|
458
|
-
@ping_timer = nil
|
459
|
-
@ping_timeouts[@ping_id] = (@ping_timeouts[@ping_id] || 0) + 1
|
460
|
-
if @ping_timeouts[@ping_id] >= max_ping_timeouts
|
461
|
-
Log.error("Mapper ping via broker #{@ping_id} timed out after #{PING_TIMEOUT} seconds and now " +
|
462
|
-
"reached maximum of #{max_ping_timeouts} timeout#{max_ping_timeouts > 1 ? 's' : ''}, " +
|
463
|
-
"attempting to reconnect")
|
464
|
-
host, port, index, priority = @sender.broker.identity_parts(@ping_id)
|
465
|
-
@sender.agent.connect(host, port, index, priority, force = true)
|
466
|
-
else
|
467
|
-
Log.warning("Mapper ping via broker #{@ping_id} timed out after #{PING_TIMEOUT} seconds")
|
468
|
-
end
|
469
|
-
rescue Exception => e
|
470
|
-
Log.error("Failed to reconnect to broker #{@ping_id}", e, :trace)
|
471
|
-
@exception_stats.track("ping timeout", e)
|
472
|
-
end
|
473
|
-
else
|
474
|
-
@ping_timer = nil
|
475
|
-
end
|
476
|
-
end
|
477
|
-
|
478
|
-
handler = lambda do |_|
|
479
|
-
begin
|
480
|
-
if @ping_timer
|
481
|
-
@ping_stats.update("success")
|
482
|
-
@ping_timer.cancel
|
483
|
-
@ping_timer = nil
|
484
|
-
@ping_timeouts[@ping_id] = 0
|
485
|
-
@ping_id = nil
|
486
|
-
end
|
487
|
-
rescue Exception => e
|
488
|
-
Log.error("Failed to cancel mapper ping", e, :trace)
|
489
|
-
@exception_stats.track("cancel ping", e)
|
490
|
-
end
|
491
|
-
end
|
492
|
-
request = Request.new("/mapper/ping", nil, {:from => @sender.identity, :token => AgentIdentity.generate})
|
493
|
-
@sender.pending_requests[request.token] = PendingRequest.new(:send_persistent_request, Time.now, handler)
|
494
|
-
ids = [@ping_id] if @ping_id
|
495
|
-
@ping_id = @sender.__send__(:publish, request, ids).first
|
496
|
-
end
|
497
|
-
true
|
498
|
-
end
|
499
|
-
|
500
|
-
# Prepare for agent termination
|
501
|
-
#
|
502
|
-
# === Return
|
503
|
-
# true:: Always return true
|
504
|
-
def terminate
|
505
|
-
@terminating = true
|
506
|
-
@check_interval = 0
|
507
|
-
if @ping_timer
|
508
|
-
@ping_timer.cancel
|
509
|
-
@ping_timer = nil
|
510
|
-
end
|
511
|
-
if @inactivity_timer
|
512
|
-
@inactivity_timer.cancel
|
513
|
-
@inactivity_timer = nil
|
514
|
-
end
|
515
|
-
true
|
516
|
-
end
|
517
|
-
|
518
|
-
protected
|
519
|
-
|
520
|
-
# Start timer that waits for inactive messaging period to end before checking connectivity
|
521
|
-
#
|
522
|
-
# === Return
|
523
|
-
# true:: Always return true
|
524
|
-
def restart_inactivity_timer
|
525
|
-
@inactivity_timer.cancel if @inactivity_timer
|
526
|
-
@inactivity_timer = EM::Timer.new(@check_interval) do
|
527
|
-
begin
|
528
|
-
check(id = nil, max_ping_timeouts = 1)
|
529
|
-
rescue Exception => e
|
530
|
-
Log.error("Failed connectivity check", e, :trace)
|
531
|
-
@exception_stats.track("check connectivity", e)
|
532
|
-
end
|
533
|
-
end
|
534
|
-
true
|
535
|
-
end
|
33
|
+
include OperationResultHelper
|
536
34
|
|
537
|
-
|
35
|
+
class SendFailure < RuntimeError; end
|
36
|
+
class TemporarilyOffline < RuntimeError; end
|
538
37
|
|
539
38
|
# Factor used on each retry iteration to achieve exponential backoff
|
540
39
|
RETRY_BACKOFF_FACTOR = 3
|
541
40
|
|
542
41
|
# (PendingRequests) Requests waiting for a response
|
543
|
-
|
42
|
+
attr_reader :pending_requests
|
544
43
|
|
545
|
-
# (OfflineHandler) Handler for requests when disconnected
|
44
|
+
# (OfflineHandler) Handler for requests when client disconnected
|
546
45
|
attr_reader :offline_handler
|
547
46
|
|
548
|
-
# (ConnectivityChecker)
|
47
|
+
# (ConnectivityChecker) AMQP broker connection checker
|
549
48
|
attr_reader :connectivity_checker
|
550
49
|
|
551
|
-
# (RightAMQP::HABrokerClient) High availability AMQP broker client
|
552
|
-
attr_accessor :broker
|
553
|
-
|
554
50
|
# (String) Identity of the associated agent
|
555
51
|
attr_reader :identity
|
556
52
|
|
557
53
|
# (Agent) Associated agent
|
558
54
|
attr_reader :agent
|
559
55
|
|
56
|
+
# (Symbol) RightNet communication mode: :http or :amqp
|
57
|
+
attr_reader :mode
|
58
|
+
|
560
59
|
# For direct access to current sender
|
561
60
|
#
|
562
61
|
# === Return
|
@@ -568,36 +67,43 @@ module RightScale
|
|
568
67
|
# Initialize sender
|
569
68
|
#
|
570
69
|
# === Parameters
|
571
|
-
# agent(Agent):: Agent using this sender; uses its identity,
|
70
|
+
# agent(Agent):: Agent using this sender; uses its identity, client, and following options:
|
572
71
|
# :exception_callback(Proc):: Callback with following parameters that is activated on exception events:
|
573
72
|
# exception(Exception):: Exception
|
574
73
|
# message(Packet):: Message being processed
|
575
74
|
# agent(Agent):: Reference to agent
|
576
|
-
# :offline_queueing(Boolean):: Whether to queue request if currently
|
75
|
+
# :offline_queueing(Boolean):: Whether to queue request if client currently disconnected,
|
577
76
|
# also requires agent invocation of initialize_offline_queue and start_offline_queue methods below,
|
578
|
-
# as well as enable_offline_mode and disable_offline_mode as
|
579
|
-
# :ping_interval(Integer):: Minimum number of seconds since last message receipt to ping
|
77
|
+
# as well as enable_offline_mode and disable_offline_mode as client connection status changes
|
78
|
+
# :ping_interval(Integer):: Minimum number of seconds since last message receipt to ping RightNet
|
580
79
|
# to check connectivity, defaults to 0 meaning do not ping
|
581
80
|
# :restart_callback(Proc):: Callback that is activated on each restart vote with votes being initiated
|
582
|
-
# by offline queue exceeding MAX_QUEUED_REQUESTS or by repeated failures to access
|
81
|
+
# by offline queue exceeding MAX_QUEUED_REQUESTS or by repeated failures to access RightNet when online
|
583
82
|
# :retry_timeout(Numeric):: Maximum number of seconds to retry request before give up
|
584
83
|
# :retry_interval(Numeric):: Number of seconds before initial request retry, increases exponentially
|
585
84
|
# :time_to_live(Integer):: Number of seconds before a request expires and is to be ignored
|
586
85
|
# by the receiver, 0 means never expire
|
86
|
+
# :async_response(Boolean):: Whether to handle responses asynchronously or to handle them immediately
|
87
|
+
# upon arrival (for use by applications that were written expecting asynchronous AMQP responses)
|
587
88
|
# :secure(Boolean):: true indicates to use Security features of rabbitmq to restrict agents to themselves
|
588
89
|
def initialize(agent)
|
589
90
|
@agent = agent
|
590
91
|
@identity = @agent.identity
|
591
92
|
@options = @agent.options || {}
|
592
|
-
@
|
93
|
+
@mode = @agent.mode
|
94
|
+
@request_queue = @agent.request_queue
|
593
95
|
@secure = @options[:secure]
|
594
96
|
@retry_timeout = RightSupport::Stats.nil_if_zero(@options[:retry_timeout])
|
595
97
|
@retry_interval = RightSupport::Stats.nil_if_zero(@options[:retry_interval])
|
596
98
|
@pending_requests = PendingRequests.new
|
597
|
-
|
99
|
+
@terminating = nil
|
598
100
|
reset_stats
|
599
101
|
@offline_handler = OfflineHandler.new(@options[:restart_callback], @offline_stats)
|
600
|
-
@connectivity_checker =
|
102
|
+
@connectivity_checker = if @mode == :amqp
|
103
|
+
# Only need connectivity checker for AMQP broker since RightHttpClient does its own checking
|
104
|
+
# via periodic session renewal
|
105
|
+
ConnectivityChecker.new(self, @options[:ping_interval] || 0, @ping_stats, @exception_stats)
|
106
|
+
end
|
601
107
|
@@instance = self
|
602
108
|
end
|
603
109
|
|
@@ -622,7 +128,7 @@ module RightScale
|
|
622
128
|
end
|
623
129
|
|
624
130
|
# Switch to offline mode
|
625
|
-
# In this mode requests are queued in memory rather than sent
|
131
|
+
# In this mode requests are queued in memory rather than sent
|
626
132
|
# Idempotent
|
627
133
|
#
|
628
134
|
# === Return
|
@@ -631,7 +137,7 @@ module RightScale
|
|
631
137
|
@offline_handler.enable if @options[:offline_queueing]
|
632
138
|
end
|
633
139
|
|
634
|
-
# Switch back to sending requests
|
140
|
+
# Switch back to sending requests after in memory queue gets flushed
|
635
141
|
# Idempotent
|
636
142
|
#
|
637
143
|
# === Return
|
@@ -640,12 +146,12 @@ module RightScale
|
|
640
146
|
@offline_handler.disable if @options[:offline_queueing]
|
641
147
|
end
|
642
148
|
|
643
|
-
# Determine whether currently
|
149
|
+
# Determine whether currently connected to RightNet via client
|
644
150
|
#
|
645
151
|
# === Return
|
646
|
-
# (Boolean):: true if offline or if
|
647
|
-
def
|
648
|
-
|
152
|
+
# (Boolean):: true if offline or if client disconnected, otherwise false
|
153
|
+
def connected?
|
154
|
+
@mode == :http ? @agent.client.connected? : @agent.client.connected.size == 0
|
649
155
|
end
|
650
156
|
|
651
157
|
# Update the time this agent last received a request or response message
|
@@ -657,45 +163,7 @@ module RightScale
|
|
657
163
|
# === Return
|
658
164
|
# true:: Always return true
|
659
165
|
def message_received(&callback)
|
660
|
-
@connectivity_checker.message_received(&callback)
|
661
|
-
end
|
662
|
-
|
663
|
-
# Send a request to a single target or multiple targets with no response expected other
|
664
|
-
# than routing failures
|
665
|
-
# Do not persist the request en route
|
666
|
-
# Enqueue the request if the target is not currently available
|
667
|
-
# Never automatically retry the request
|
668
|
-
# Set time-to-live to be forever
|
669
|
-
#
|
670
|
-
# === Parameters
|
671
|
-
# type(String):: Dispatch route for the request; typically identifies actor and action
|
672
|
-
# payload(Object):: Data to be sent with marshalling en route
|
673
|
-
# target(String|Hash):: Identity of specific target, hash for selecting potentially multiple
|
674
|
-
# targets, or nil if routing solely using type
|
675
|
-
# :tags(Array):: Tags that must all be associated with a target for it to be selected
|
676
|
-
# :scope(Hash):: Scoping to be used to restrict routing
|
677
|
-
# :account(Integer):: Restrict to agents with this account id
|
678
|
-
# :shard(Integer):: Restrict to agents with this shard id, or if value is Packet::GLOBAL,
|
679
|
-
# ones with no shard id
|
680
|
-
# :selector(Symbol):: Which of the matched targets to be selected, either :any or :all,
|
681
|
-
# defaults to :any
|
682
|
-
#
|
683
|
-
# === Block
|
684
|
-
# Optional block used to process routing responses asynchronously with the following parameter:
|
685
|
-
# result(Result):: Response with an OperationResult of SUCCESS, RETRY, NON_DELIVERY, or ERROR,
|
686
|
-
# with an initial SUCCESS response containing the targets to which the mapper published the
|
687
|
-
# request and any additional responses indicating any failures to actually route the request
|
688
|
-
# to those targets, use RightScale::OperationResult.from_results to decode
|
689
|
-
#
|
690
|
-
# === Return
|
691
|
-
# true:: Always return true
|
692
|
-
#
|
693
|
-
# === Raise
|
694
|
-
# SendFailure:: If publishing of request failed unexpectedly
|
695
|
-
# TemporarilyOffline:: If cannot publish request because currently not connected
|
696
|
-
# to any brokers and offline queueing is disabled
|
697
|
-
def send_push(type, payload = nil, target = nil, &callback)
|
698
|
-
build_and_send_packet(:send_push, type, payload, target, callback)
|
166
|
+
@connectivity_checker.message_received(&callback) if @connectivity_checker
|
699
167
|
end
|
700
168
|
|
701
169
|
# Send a request to a single target or multiple targets with no response expected other
|
@@ -703,14 +171,16 @@ module RightScale
|
|
703
171
|
# Persist the request en route to reduce the chance of it being lost at the expense of some
|
704
172
|
# additional network overhead
|
705
173
|
# Enqueue the request if the target is not currently available
|
706
|
-
# Never automatically retry the request
|
174
|
+
# Never automatically retry the request if there is the possibility of it being duplicated
|
707
175
|
# Set time-to-live to be forever
|
708
176
|
#
|
709
177
|
# === Parameters
|
710
178
|
# type(String):: Dispatch route for the request; typically identifies actor and action
|
711
179
|
# payload(Object):: Data to be sent with marshalling en route
|
712
|
-
# target(
|
713
|
-
# targets, or nil
|
180
|
+
# target(Hash|NilClass) Target for request, which may be a specific agent (using :agent_id),
|
181
|
+
# potentially multiple targets (using :tags, :scope, :selector), or nil to route solely
|
182
|
+
# using type:
|
183
|
+
# :agent_id(String):: serialized identity of specific target
|
714
184
|
# :tags(Array):: Tags that must all be associated with a target for it to be selected
|
715
185
|
# :scope(Hash):: Scoping to be used to restrict routing
|
716
186
|
# :account(Integer):: Restrict to agents with this account id
|
@@ -718,23 +188,25 @@ module RightScale
|
|
718
188
|
# ones with no shard id
|
719
189
|
# :selector(Symbol):: Which of the matched targets to be selected, either :any or :all,
|
720
190
|
# defaults to :any
|
191
|
+
# token(String|NilClass):: Token uniquely identifying request; defaults to random generated
|
721
192
|
#
|
722
193
|
# === Block
|
723
194
|
# Optional block used to process routing responses asynchronously with the following parameter:
|
724
195
|
# result(Result):: Response with an OperationResult of SUCCESS, RETRY, NON_DELIVERY, or ERROR,
|
725
|
-
# with an initial SUCCESS response containing the targets to which the
|
726
|
-
#
|
196
|
+
# with an initial SUCCESS response containing the targets to which the request was sent
|
197
|
+
# and any additional responses indicating any failures to actually route the request
|
727
198
|
# to those targets, use RightScale::OperationResult.from_results to decode
|
728
199
|
#
|
729
200
|
# === Return
|
730
201
|
# true:: Always return true
|
731
202
|
#
|
732
203
|
# === Raise
|
733
|
-
#
|
734
|
-
#
|
735
|
-
#
|
736
|
-
|
737
|
-
|
204
|
+
# ArgumentError:: If target invalid
|
205
|
+
# SendFailure:: If sending of request failed unexpectedly
|
206
|
+
# TemporarilyOffline:: If cannot send request because RightNet client currently disconnected
|
207
|
+
# and offline queueing is disabled
|
208
|
+
def send_push(type, payload = nil, target = nil, token = nil, &callback)
|
209
|
+
build_and_send_packet(:send_push, type, payload, target, token, callback)
|
738
210
|
end
|
739
211
|
|
740
212
|
# Send a request to a single target with a response expected
|
@@ -750,13 +222,16 @@ module RightScale
|
|
750
222
|
# === Parameters
|
751
223
|
# type(String):: Dispatch route for the request; typically identifies actor and action
|
752
224
|
# payload(Object):: Data to be sent with marshalling en route
|
753
|
-
# target(
|
754
|
-
# randomly
|
225
|
+
# target(Hash|NilClass) Target for request, which may be a specific agent (using :agent_id),
|
226
|
+
# one chosen randomly from potentially multiple targets (using :tags, :scope), or nil to
|
227
|
+
# route solely using type:
|
228
|
+
# :agent_id(String):: serialized identity of specific target
|
755
229
|
# :tags(Array):: Tags that must all be associated with a target for it to be selected
|
756
230
|
# :scope(Hash):: Scoping to be used to restrict routing
|
757
231
|
# :account(Integer):: Restrict to agents with this account id
|
758
232
|
# :shard(Integer):: Restrict to agents with this shard id, or if value is Packet::GLOBAL,
|
759
233
|
# ones with no shard id
|
234
|
+
# token(String|NilClass):: Token uniquely identifying request; defaults to random generated
|
760
235
|
#
|
761
236
|
# === Block
|
762
237
|
# Required block used to process response asynchronously with the following parameter:
|
@@ -767,67 +242,59 @@ module RightScale
|
|
767
242
|
# true:: Always return true
|
768
243
|
#
|
769
244
|
# === Raise
|
770
|
-
# ArgumentError:: If block missing
|
771
|
-
|
772
|
-
# TemporarilyOffline:: If cannot publish request because currently not connected
|
773
|
-
# to any brokers and offline queueing is disabled
|
774
|
-
def send_retryable_request(type, payload = nil, target = nil, &callback)
|
245
|
+
# ArgumentError:: If target invalid or block missing
|
246
|
+
def send_request(type, payload = nil, target = nil, token = nil, &callback)
|
775
247
|
raise ArgumentError, "Missing block for response callback" unless callback
|
776
|
-
build_and_send_packet(:
|
248
|
+
build_and_send_packet(:send_request, type, payload, target, token, callback)
|
777
249
|
end
|
778
250
|
|
779
|
-
#
|
780
|
-
# Persist the request en route to reduce the chance of it being lost at the expense of some
|
781
|
-
# additional network overhead
|
782
|
-
# Enqueue the request if the target is not currently available
|
783
|
-
# Never automatically retry the request if there is the possibility of the request being duplicated
|
784
|
-
# Set time-to-live to be forever
|
785
|
-
# Note that receiving a response does not guarantee that the request activity has actually
|
786
|
-
# completed since the request processing may involve other asynchronous requests
|
251
|
+
# Build and send packet
|
787
252
|
#
|
788
253
|
# === Parameters
|
254
|
+
# kind(Symbol):: Kind of request: :send_push or :send_request
|
789
255
|
# type(String):: Dispatch route for the request; typically identifies actor and action
|
790
256
|
# payload(Object):: Data to be sent with marshalling en route
|
791
|
-
# target(
|
792
|
-
#
|
257
|
+
# target(Hash|NilClass):: Identity of specific target as string, or hash for selecting targets
|
258
|
+
# :agent_id(String):: Identity of specific target
|
793
259
|
# :tags(Array):: Tags that must all be associated with a target for it to be selected
|
794
260
|
# :scope(Hash):: Scoping to be used to restrict routing
|
795
261
|
# :account(Integer):: Restrict to agents with this account id
|
796
262
|
# :shard(Integer):: Restrict to agents with this shard id, or if value is Packet::GLOBAL,
|
797
263
|
# ones with no shard id
|
798
|
-
#
|
799
|
-
#
|
800
|
-
#
|
801
|
-
# result(Result):: Response with an OperationResult of SUCCESS, RETRY, NON_DELIVERY, or ERROR,
|
802
|
-
# use RightScale::OperationResult.from_results to decode
|
264
|
+
# :selector(Symbol):: Which of the matched targets to be selected: :any or :all
|
265
|
+
# token(String|NilClass):: Token uniquely identifying request; defaults to random generated
|
266
|
+
# callback(Proc|nil):: Block used to process routing response
|
803
267
|
#
|
804
268
|
# === Return
|
805
269
|
# true:: Always return true
|
806
270
|
#
|
807
271
|
# === Raise
|
808
|
-
# ArgumentError:: If
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
272
|
+
# ArgumentError:: If target invalid
|
273
|
+
def build_and_send_packet(kind, type, payload, target, token, callback)
|
274
|
+
if (packet = build_packet(kind, type, payload, target, token, callback))
|
275
|
+
action = type.split('/').last
|
276
|
+
received_at = @request_stats.update(action, packet.token)
|
277
|
+
@request_kind_stats.update((packet.selector == :all ? "fanout" : kind.to_s)[5..-1])
|
278
|
+
send("#{@mode}_send", kind, target, packet, received_at, callback)
|
279
|
+
end
|
280
|
+
true
|
815
281
|
end
|
816
282
|
|
817
283
|
# Build packet or queue it if offline
|
818
284
|
#
|
819
285
|
# === Parameters
|
820
|
-
# kind(Symbol):: Kind of
|
821
|
-
# or :send_persistent_request
|
286
|
+
# kind(Symbol):: Kind of request: :send_push or :send_request
|
822
287
|
# type(String):: Dispatch route for the request; typically identifies actor and action
|
823
288
|
# payload(Object):: Data to be sent with marshalling en route
|
824
|
-
# target(
|
289
|
+
# target(Hash|NilClass):: Identity of specific target as string, or hash for selecting targets
|
290
|
+
# :agent_id(String):: Identity of specific target
|
825
291
|
# :tags(Array):: Tags that must all be associated with a target for it to be selected
|
826
292
|
# :scope(Hash):: Scoping to be used to restrict routing
|
827
293
|
# :account(Integer):: Restrict to agents with this account id
|
828
294
|
# :shard(Integer):: Restrict to agents with this shard id, or if value is Packet::GLOBAL,
|
829
295
|
# ones with no shard id
|
830
296
|
# :selector(Symbol):: Which of the matched targets to be selected: :any or :all
|
297
|
+
# token(String|NilClass):: Token uniquely identifying request; defaults to random generated
|
831
298
|
# callback(Boolean):: Whether this request has an associated response callback
|
832
299
|
#
|
833
300
|
# === Return
|
@@ -835,30 +302,32 @@ module RightScale
|
|
835
302
|
#
|
836
303
|
# === Raise
|
837
304
|
# ArgumentError:: If target is invalid
|
838
|
-
def build_packet(kind, type, payload, target, callback = false)
|
839
|
-
validate_target(target,
|
840
|
-
if
|
305
|
+
def build_packet(kind, type, payload, target, token, callback = false)
|
306
|
+
validate_target(target, kind == :send_push)
|
307
|
+
if queueing?
|
841
308
|
@offline_handler.queue_request(kind, type, payload, target, callback)
|
842
309
|
nil
|
843
310
|
else
|
844
|
-
|
845
|
-
persistent = !!(kind_str =~ /persistent/)
|
846
|
-
if kind_str =~ /push/
|
311
|
+
if kind == :send_push
|
847
312
|
packet = Push.new(type, payload)
|
848
313
|
packet.selector = target[:selector] || :any if target.is_a?(Hash)
|
314
|
+
packet.persistent = true
|
849
315
|
packet.confirm = true if callback
|
850
316
|
else
|
851
317
|
packet = Request.new(type, payload)
|
852
318
|
ttl = @options[:time_to_live]
|
853
|
-
packet.expires_at = Time.now.to_i + ttl if
|
319
|
+
packet.expires_at = Time.now.to_i + ttl if ttl && ttl != 0
|
854
320
|
packet.selector = :any
|
855
321
|
end
|
856
322
|
packet.from = @identity
|
857
|
-
packet.token =
|
858
|
-
packet.persistent = persistent
|
323
|
+
packet.token = token || RightSupport::Data::UUID.generate
|
859
324
|
if target.is_a?(Hash)
|
860
|
-
|
861
|
-
|
325
|
+
if (agent_id = target[:agent_id])
|
326
|
+
packet.target = agent_id
|
327
|
+
else
|
328
|
+
packet.tags = target[:tags] || []
|
329
|
+
packet.scope = target[:scope]
|
330
|
+
end
|
862
331
|
else
|
863
332
|
packet.target = target
|
864
333
|
end
|
@@ -866,36 +335,6 @@ module RightScale
|
|
866
335
|
end
|
867
336
|
end
|
868
337
|
|
869
|
-
# Send packet
|
870
|
-
#
|
871
|
-
# === Parameters
|
872
|
-
# kind(Symbol):: Kind of send request: :send_push, :send_persistent_push, :send_retryable_request,
|
873
|
-
# or :send_persistent_request
|
874
|
-
# packet(Push|Request|NilClass):: Packet to send; nothing sent if nil
|
875
|
-
# callback(Proc|nil):: Block used to process routing response
|
876
|
-
#
|
877
|
-
# === Return
|
878
|
-
# true:: Always return true
|
879
|
-
#
|
880
|
-
# === Raise
|
881
|
-
# SendFailure:: If publishing of request fails unexpectedly
|
882
|
-
# TemporarilyOffline:: If cannot publish request because currently not connected
|
883
|
-
# to any brokers and offline queueing is disabled
|
884
|
-
def send_packet(kind, packet, callback)
|
885
|
-
if packet
|
886
|
-
method = packet.type.split('/').last
|
887
|
-
received_at = @request_stats.update(method, packet.token)
|
888
|
-
@request_kind_stats.update((packet.selector == :all ? kind.to_s.sub(/push/, "fanout") : kind.to_s)[5..-1])
|
889
|
-
@pending_requests[packet.token] = PendingRequest.new(kind, received_at, callback) if callback
|
890
|
-
if !packet.persistent && kind.to_s =~ /request/
|
891
|
-
publish_with_timeout_retry(packet, packet.token)
|
892
|
-
else
|
893
|
-
publish(packet)
|
894
|
-
end
|
895
|
-
end
|
896
|
-
true
|
897
|
-
end
|
898
|
-
|
899
338
|
# Handle response to a request
|
900
339
|
#
|
901
340
|
# === Parameters
|
@@ -904,9 +343,9 @@ module RightScale
|
|
904
343
|
# === Return
|
905
344
|
# true:: Always return true
|
906
345
|
def handle_response(response)
|
907
|
-
token = response.token
|
908
346
|
if response.is_a?(Result)
|
909
|
-
|
347
|
+
token = response.token
|
348
|
+
if (result = OperationResult.from_results(response))
|
910
349
|
if result.non_delivery?
|
911
350
|
@non_delivery_stats.update(result.content.nil? ? "nil" : result.content.inspect)
|
912
351
|
elsif result.error?
|
@@ -917,26 +356,26 @@ module RightScale
|
|
917
356
|
@result_stats.update(response.results.nil? ? "nil" : response.results)
|
918
357
|
end
|
919
358
|
|
920
|
-
if
|
921
|
-
if result && result.non_delivery? &&
|
359
|
+
if (pending_request = @pending_requests[token])
|
360
|
+
if result && result.non_delivery? && pending_request.kind == :send_request
|
922
361
|
if result.content == OperationResult::TARGET_NOT_CONNECTED
|
923
362
|
# Log and temporarily ignore so that timeout retry mechanism continues, but save reason for use below if timeout
|
924
363
|
# Leave purging of associated request until final response, i.e., success response or retry timeout
|
925
|
-
if
|
926
|
-
@pending_requests[
|
364
|
+
if (parent_token = pending_request.retry_parent_token)
|
365
|
+
@pending_requests[parent_token].non_delivery = result.content
|
927
366
|
else
|
928
|
-
|
367
|
+
pending_request.non_delivery = result.content
|
929
368
|
end
|
930
369
|
Log.info("Non-delivery of <#{token}> because #{result.content}")
|
931
|
-
elsif result.content == OperationResult::RETRY_TIMEOUT &&
|
370
|
+
elsif result.content == OperationResult::RETRY_TIMEOUT && pending_request.non_delivery
|
932
371
|
# Request timed out but due to another non-delivery reason, so use that reason since more germane
|
933
|
-
response.results = OperationResult.non_delivery(
|
934
|
-
|
372
|
+
response.results = OperationResult.non_delivery(pending_request.non_delivery)
|
373
|
+
deliver_response(response, pending_request)
|
935
374
|
else
|
936
|
-
|
375
|
+
deliver_response(response, pending_request)
|
937
376
|
end
|
938
377
|
else
|
939
|
-
|
378
|
+
deliver_response(response, pending_request)
|
940
379
|
end
|
941
380
|
elsif result && result.non_delivery?
|
942
381
|
Log.info("Non-delivery of <#{token}> because #{result.content}")
|
@@ -947,16 +386,20 @@ module RightScale
|
|
947
386
|
true
|
948
387
|
end
|
949
388
|
|
950
|
-
# Take any actions necessary to quiesce
|
389
|
+
# Take any actions necessary to quiesce client interaction in preparation
|
951
390
|
# for agent termination but allow message receipt to continue
|
952
391
|
#
|
953
392
|
# === Return
|
954
393
|
# (Array):: Number of pending non-push requests and age of youngest request
|
955
394
|
def terminate
|
956
|
-
@offline_handler
|
957
|
-
|
958
|
-
|
959
|
-
|
395
|
+
if @offline_handler
|
396
|
+
@offline_handler.terminate
|
397
|
+
@connectivity_checker.terminate if @connectivity_checker
|
398
|
+
pending = @pending_requests.kind(:send_request)
|
399
|
+
[pending.size, pending.youngest_age]
|
400
|
+
else
|
401
|
+
[0, nil]
|
402
|
+
end
|
960
403
|
end
|
961
404
|
|
962
405
|
# Create displayable dump of unfinished non-push request information
|
@@ -966,11 +409,13 @@ module RightScale
|
|
966
409
|
# info(Array(String)):: Receive time and token for each request in descending time order
|
967
410
|
def dump_requests
|
968
411
|
info = []
|
969
|
-
@pending_requests
|
970
|
-
|
412
|
+
if @pending_requests
|
413
|
+
@pending_requests.kind(:send_request).each do |token, request|
|
414
|
+
info << "#{request.receive_time.localtime} <#{token}>"
|
415
|
+
end
|
416
|
+
info.sort!.reverse!
|
417
|
+
info = info[0..49] + ["..."] if info.size > 50
|
971
418
|
end
|
972
|
-
info.sort.reverse
|
973
|
-
info = info[0..49] + ["..."] if info.size > 50
|
974
419
|
info
|
975
420
|
end
|
976
421
|
|
@@ -1006,31 +451,34 @@ module RightScale
|
|
1006
451
|
# "send failure"(Hash|nil):: Send failure activity stats with keys "total", "percent", "last", and "rate"
|
1007
452
|
# with percentage breakdown per failure type, or nil if none
|
1008
453
|
def stats(reset = false)
|
1009
|
-
|
1010
|
-
|
1011
|
-
|
1012
|
-
|
1013
|
-
|
1014
|
-
|
1015
|
-
|
1016
|
-
|
454
|
+
stats = {}
|
455
|
+
if @agent
|
456
|
+
offlines = @offline_stats.all
|
457
|
+
offlines.merge!("duration" => @offline_stats.avg_duration) if offlines
|
458
|
+
if @pending_requests.size > 0
|
459
|
+
pending = {}
|
460
|
+
pending["pushes"] = @pending_requests.kind(:send_push).size
|
461
|
+
requests = @pending_requests.kind(:send_request)
|
462
|
+
if (pending["requests"] = requests.size) > 0
|
463
|
+
pending["oldest age"] = requests.oldest_age
|
464
|
+
end
|
1017
465
|
end
|
466
|
+
stats = {
|
467
|
+
"exceptions" => @exception_stats.stats,
|
468
|
+
"non-deliveries" => @non_delivery_stats.all,
|
469
|
+
"offlines" => offlines,
|
470
|
+
"pings" => @ping_stats.all,
|
471
|
+
"request kinds" => @request_kind_stats.all,
|
472
|
+
"requests" => @request_stats.all,
|
473
|
+
"requests pending" => pending,
|
474
|
+
"response time" => @request_stats.avg_duration,
|
475
|
+
"result errors" => @result_error_stats.all,
|
476
|
+
"results" => @result_stats.all,
|
477
|
+
"retries" => @retry_stats.all,
|
478
|
+
"send failures" => @send_failure_stats.all
|
479
|
+
}
|
480
|
+
reset_stats if reset
|
1018
481
|
end
|
1019
|
-
stats = {
|
1020
|
-
"exceptions" => @exception_stats.stats,
|
1021
|
-
"non-deliveries" => @non_delivery_stats.all,
|
1022
|
-
"offlines" => offlines,
|
1023
|
-
"pings" => @ping_stats.all,
|
1024
|
-
"request kinds" => @request_kind_stats.all,
|
1025
|
-
"requests" => @request_stats.all,
|
1026
|
-
"requests pending" => pending,
|
1027
|
-
"response time" => @request_stats.avg_duration,
|
1028
|
-
"result errors" => @result_error_stats.all,
|
1029
|
-
"results" => @result_stats.all,
|
1030
|
-
"retries" => @retry_stats.all,
|
1031
|
-
"send failures" => @send_failure_stats.all
|
1032
|
-
}
|
1033
|
-
reset_stats if reset
|
1034
482
|
stats
|
1035
483
|
end
|
1036
484
|
|
@@ -1056,9 +504,11 @@ module RightScale
|
|
1056
504
|
|
1057
505
|
# Validate target argument of send per the semantics of each kind of send:
|
1058
506
|
# - The target is either a specific target name, a non-empty hash, or nil
|
1059
|
-
# - A specific target name must be a string
|
507
|
+
# - A specific target name must be a string (or use :agent_id key)
|
1060
508
|
# - A non-empty hash target
|
1061
509
|
# - may have keys in symbol or string format
|
510
|
+
# - may contain an :agent_id to select a specific target, but then cannot
|
511
|
+
# have any other keys
|
1062
512
|
# - may be allowed to contain a :selector key with value :any or :all,
|
1063
513
|
# depending on the kind of send
|
1064
514
|
# - may contain a :scope key with a hash value with keys :account and/or :shard
|
@@ -1075,11 +525,15 @@ module RightScale
|
|
1075
525
|
# === Raise
|
1076
526
|
# ArgumentError:: If target is invalid
|
1077
527
|
def validate_target(target, allow_selector)
|
1078
|
-
choices = (allow_selector ? ":selector, " : "") + ":tags and/or :scope"
|
528
|
+
choices = ":agent_id OR " + (allow_selector ? ":selector, " : "") + ":tags and/or :scope"
|
1079
529
|
if target.is_a?(Hash)
|
1080
530
|
t = SerializationHelper.symbolize_keys(target)
|
1081
531
|
|
1082
|
-
if
|
532
|
+
if (agent_id = t[:agent_id])
|
533
|
+
raise ArgumentError, "Invalid target: #{target.inspect}" if t.size > 1
|
534
|
+
end
|
535
|
+
|
536
|
+
if (selector = t[:selector])
|
1083
537
|
if allow_selector
|
1084
538
|
selector = selector.to_sym
|
1085
539
|
unless [:any, :all].include?(selector)
|
@@ -1091,7 +545,7 @@ module RightScale
|
|
1091
545
|
end
|
1092
546
|
end
|
1093
547
|
|
1094
|
-
if scope = t[:scope]
|
548
|
+
if (scope = t[:scope])
|
1095
549
|
if scope.is_a?(Hash)
|
1096
550
|
scope = SerializationHelper.symbolize_keys(scope)
|
1097
551
|
unless (scope[:account] || scope[:shard]) && (scope.keys - [:account, :shard]).empty?
|
@@ -1107,7 +561,7 @@ module RightScale
|
|
1107
561
|
raise ArgumentError, "Invalid target tags (#{t[:tags].inspect}), must be an array"
|
1108
562
|
end
|
1109
563
|
|
1110
|
-
unless (selector || scope || tags) && (t.keys - [:selector, :scope, :tags]).empty?
|
564
|
+
unless (agent_id || selector || scope || tags) && (t.keys - [:agent_id, :selector, :scope, :tags]).empty?
|
1111
565
|
raise ArgumentError, "Invalid target hash (#{target.inspect}), choices are #{choices}"
|
1112
566
|
end
|
1113
567
|
target = t
|
@@ -1117,79 +571,144 @@ module RightScale
|
|
1117
571
|
true
|
1118
572
|
end
|
1119
573
|
|
1120
|
-
#
|
574
|
+
# Send request via HTTP and then immediately handle response
|
1121
575
|
#
|
1122
576
|
# === Parameters
|
1123
|
-
# kind(Symbol):: Kind of
|
1124
|
-
#
|
1125
|
-
#
|
1126
|
-
#
|
1127
|
-
#
|
1128
|
-
# :tags(Array):: Tags that must all be associated with a target for it to be selected
|
1129
|
-
# :scope(Hash):: Scoping to be used to restrict routing
|
1130
|
-
# :account(Integer):: Restrict to agents with this account id
|
1131
|
-
# :shard(Integer):: Restrict to agents with this shard id, or if value is Packet::GLOBAL,
|
1132
|
-
# ones with no shard id
|
1133
|
-
# :selector(Symbol):: Which of the matched targets to be selected: :any or :all
|
1134
|
-
# callback(Proc|nil):: Block used to process routing response
|
577
|
+
# kind(Symbol):: Kind of request: :send_push or :send_request
|
578
|
+
# target(Hash|String|nil):: Target for request
|
579
|
+
# packet(Push|Request):: Request packet to send
|
580
|
+
# received_at(Time):: Time when request received
|
581
|
+
# callback(Proc|nil):: Block used to process response
|
1135
582
|
#
|
1136
583
|
# === Return
|
1137
584
|
# true:: Always return true
|
585
|
+
def http_send(kind, target, packet, received_at, callback)
|
586
|
+
begin
|
587
|
+
method = packet.class.name.split("::").last.downcase
|
588
|
+
result = success_result(@agent.client.send(method, packet.type, packet.payload, target, packet.token))
|
589
|
+
rescue Exceptions::Unauthorized => e
|
590
|
+
result = error_result(e.message)
|
591
|
+
rescue Exceptions::ConnectivityFailure => e
|
592
|
+
if queueing?
|
593
|
+
@offline_handler.queue_request(kind, packet.type, packet.payload, target, callback)
|
594
|
+
result = nil
|
595
|
+
else
|
596
|
+
result = retry_result(e.message)
|
597
|
+
end
|
598
|
+
rescue Exceptions::RetryableError => e
|
599
|
+
result = retry_result(e.message)
|
600
|
+
rescue Exceptions::InternalServerError => e
|
601
|
+
result = error_result("#{e.server} internal error")
|
602
|
+
rescue Exceptions::Terminating => e
|
603
|
+
result = nil
|
604
|
+
rescue StandardError => e
|
605
|
+
# These errors are either unexpected errors or RestClient errors with an http_body
|
606
|
+
# giving details about the error that are conveyed in the error_result
|
607
|
+
if e.respond_to?(:http_body)
|
608
|
+
# No need to log here since any HTTP request errors have already been logged
|
609
|
+
result = error_result(e.inspect)
|
610
|
+
else
|
611
|
+
agent_type = AgentIdentity.parse(@identity).agent_type
|
612
|
+
Log.error("Failed to send #{packet.trace} #{packet.type}", e, :trace)
|
613
|
+
@exception_stats.track("request", e)
|
614
|
+
result = error_result("#{agent_type.capitalize} agent internal error")
|
615
|
+
end
|
616
|
+
end
|
617
|
+
|
618
|
+
if result && packet.is_a?(Request)
|
619
|
+
result = Result.new(packet.token, @identity, result, from = packet.target)
|
620
|
+
result.received_at = received_at.to_f
|
621
|
+
@pending_requests[packet.token] = PendingRequest.new(kind, received_at, callback) if callback
|
622
|
+
if @options[:async_response]
|
623
|
+
EM.next_tick { handle_response(result) }
|
624
|
+
else
|
625
|
+
handle_response(result)
|
626
|
+
end
|
627
|
+
end
|
628
|
+
true
|
629
|
+
end
|
630
|
+
|
631
|
+
# Send request via AMQP
|
632
|
+
# If lack connectivity and queueing enabled, queue request
|
1138
633
|
#
|
1139
|
-
# ===
|
1140
|
-
#
|
1141
|
-
#
|
1142
|
-
#
|
1143
|
-
#
|
1144
|
-
|
1145
|
-
|
1146
|
-
|
634
|
+
# === Parameters
|
635
|
+
# kind(Symbol):: Kind of request: :send_push or :send_request
|
636
|
+
# target(Hash|String|nil):: Target for request
|
637
|
+
# received_at(Time):: Time when request received
|
638
|
+
# packet(Push|Request):: Request packet to send
|
639
|
+
# callback(Proc|nil):: Block used to process response
|
640
|
+
#
|
641
|
+
# === Return
|
642
|
+
# true:: Always return true
|
643
|
+
def amqp_send(kind, target, packet, received_at, callback)
|
644
|
+
begin
|
645
|
+
@pending_requests[packet.token] = PendingRequest.new(kind, received_at, callback) if callback
|
646
|
+
if packet.class == Request
|
647
|
+
amqp_send_retry(packet, packet.token)
|
648
|
+
else
|
649
|
+
amqp_send_once(packet)
|
650
|
+
end
|
651
|
+
rescue TemporarilyOffline => e
|
652
|
+
if queueing?
|
653
|
+
# Queue request until come back online
|
654
|
+
@offline_handler.queue_request(kind, packet.type, packet.payload, target, callback)
|
655
|
+
@pending_requests.delete(packet.token) if callback
|
656
|
+
else
|
657
|
+
# Send retry response so that requester, e.g., RetryableRequest, can retry
|
658
|
+
result = OperationResult.retry("lost RightNet connectivity")
|
659
|
+
handle_response(Result.new(packet.token, @identity, result, @identity))
|
660
|
+
end
|
661
|
+
rescue SendFailure => e
|
662
|
+
# Send non-delivery response so that requester, e.g., RetryableRequest, can retry
|
663
|
+
result = OperationResult.non_delivery("send failed unexpectedly")
|
664
|
+
handle_response(Result.new(packet.token, @identity, result, @identity))
|
665
|
+
end
|
1147
666
|
true
|
1148
667
|
end
|
1149
668
|
|
1150
|
-
#
|
669
|
+
# Send request via AMQP without retrying
|
1151
670
|
# Use mandatory flag to request return of message if it cannot be delivered
|
1152
671
|
#
|
1153
672
|
# === Parameters
|
1154
|
-
# packet(Push|Request)::
|
673
|
+
# packet(Push|Request):: Request packet to send
|
1155
674
|
# ids(Array|nil):: Identity of specific brokers to choose from, or nil if any okay
|
1156
675
|
#
|
1157
676
|
# === Return
|
1158
|
-
# (Array):: Identity of brokers published
|
677
|
+
# (Array):: Identity of brokers to which request was published
|
1159
678
|
#
|
1160
679
|
# === Raise
|
1161
|
-
# SendFailure:: If
|
1162
|
-
# TemporarilyOffline:: If cannot
|
1163
|
-
#
|
1164
|
-
def
|
1165
|
-
|
1166
|
-
|
1167
|
-
|
1168
|
-
|
1169
|
-
|
1170
|
-
|
1171
|
-
|
1172
|
-
|
1173
|
-
|
1174
|
-
|
1175
|
-
|
1176
|
-
|
1177
|
-
|
1178
|
-
|
1179
|
-
|
1180
|
-
end
|
680
|
+
# SendFailure:: If sending of request failed unexpectedly
|
681
|
+
# TemporarilyOffline:: If cannot send request because RightNet client currently disconnected
|
682
|
+
# and offline queueing is disabled
|
683
|
+
def amqp_send_once(packet, ids = nil)
|
684
|
+
name =
|
685
|
+
exchange = {:type => :fanout, :name => @request_queue, :options => {:durable => true, :no_declare => @secure}}
|
686
|
+
@agent.client.publish(exchange, packet, :persistent => packet.persistent, :mandatory => true,
|
687
|
+
:log_filter => [:tags, :target, :tries, :persistent], :brokers => ids)
|
688
|
+
rescue RightAMQP::HABrokerClient::NoConnectedBrokers => e
|
689
|
+
msg = "Failed to publish request #{packet.trace} #{packet.type}"
|
690
|
+
Log.error(msg, e)
|
691
|
+
@send_failure_stats.update("NoConnectedBrokers")
|
692
|
+
raise TemporarilyOffline.new(msg + " (#{e.class}: #{e.message})")
|
693
|
+
rescue Exception => e
|
694
|
+
msg = "Failed to publish request #{packet.trace} #{packet.type}"
|
695
|
+
Log.error(msg, e, :trace)
|
696
|
+
@send_failure_stats.update(e.class.name)
|
697
|
+
@exception_stats.track("publish", e, packet)
|
698
|
+
raise SendFailure.new(msg + " (#{e.class}: #{e.message})")
|
1181
699
|
end
|
1182
700
|
|
1183
|
-
#
|
701
|
+
# Send request via AMQP with one or more retries if do not receive a response in time
|
1184
702
|
# Send timeout result if reach configured retry timeout limit
|
1185
703
|
# Use exponential backoff with RETRY_BACKOFF_FACTOR for retry spacing
|
1186
704
|
# Adjust retry interval by average response time to avoid adding to system load
|
1187
705
|
# when system gets slow
|
1188
706
|
# Rotate through brokers on retries
|
707
|
+
# Check connectivity after first retry timeout
|
1189
708
|
#
|
1190
709
|
# === Parameters
|
1191
|
-
#
|
1192
|
-
#
|
710
|
+
# packet(Request):: Request packet to send
|
711
|
+
# parent_token(String):: Token for original request
|
1193
712
|
# count(Integer):: Number of retries so far
|
1194
713
|
# multiplier(Integer):: Multiplier for retry interval for exponential backoff
|
1195
714
|
# elapsed(Integer):: Elapsed time in seconds since this request was first attempted
|
@@ -1197,79 +716,74 @@ module RightScale
|
|
1197
716
|
#
|
1198
717
|
# === Return
|
1199
718
|
# true:: Always return true
|
1200
|
-
def
|
1201
|
-
|
719
|
+
def amqp_send_retry(packet, parent_token, count = 0, multiplier = 1, elapsed = 0, broker_ids = nil)
|
720
|
+
check_broker_ids = amqp_send_once(packet, broker_ids)
|
1202
721
|
|
1203
|
-
if @retry_interval && @retry_timeout &&
|
722
|
+
if @retry_interval && @retry_timeout && parent_token
|
1204
723
|
interval = [(@retry_interval * multiplier) + (@request_stats.avg_duration || 0), @retry_timeout - elapsed].min
|
1205
724
|
EM.add_timer(interval) do
|
1206
725
|
begin
|
1207
|
-
if
|
726
|
+
if @pending_requests[parent_token]
|
1208
727
|
count += 1
|
1209
728
|
elapsed += interval
|
1210
729
|
if elapsed < @retry_timeout
|
1211
|
-
|
1212
|
-
|
1213
|
-
@pending_requests[
|
1214
|
-
@pending_requests[
|
1215
|
-
broker_ids ||= @
|
1216
|
-
|
1217
|
-
|
1218
|
-
@retry_stats.update(
|
730
|
+
packet.tries << packet.token
|
731
|
+
packet.token = RightSupport::Data::UUID.generate
|
732
|
+
@pending_requests[parent_token].retry_parent_token = parent_token if count == 1
|
733
|
+
@pending_requests[packet.token] = @pending_requests[parent_token]
|
734
|
+
broker_ids ||= @agent.client.all
|
735
|
+
amqp_send_retry(packet, parent_token, count, multiplier * RETRY_BACKOFF_FACTOR, elapsed,
|
736
|
+
broker_ids.push(broker_ids.shift))
|
737
|
+
@retry_stats.update(packet.type.split('/').last)
|
1219
738
|
else
|
1220
|
-
Log.warning("RE-SEND TIMEOUT after #{elapsed.to_i} seconds for #{
|
739
|
+
Log.warning("RE-SEND TIMEOUT after #{elapsed.to_i} seconds for #{packet.trace} #{packet.type}")
|
1221
740
|
result = OperationResult.non_delivery(OperationResult::RETRY_TIMEOUT)
|
1222
741
|
@non_delivery_stats.update(result.content)
|
1223
|
-
handle_response(Result.new(
|
742
|
+
handle_response(Result.new(packet.token, @identity, result, @identity))
|
1224
743
|
end
|
1225
|
-
@connectivity_checker.check(
|
744
|
+
@connectivity_checker.check(check_broker_ids.first) if check_broker_ids.any? && count == 1
|
1226
745
|
end
|
1227
746
|
rescue TemporarilyOffline => e
|
1228
|
-
|
1229
|
-
Log.error("Failed retry for #{request.trace} #{request.type} because temporarily offline")
|
1230
|
-
result = OperationResult.retry("lost connectivity")
|
1231
|
-
handle_response(Result.new(request.token, request.reply_to, result, @identity))
|
747
|
+
Log.error("Failed retry for #{packet.trace} #{packet.type} because temporarily offline")
|
1232
748
|
rescue SendFailure => e
|
1233
|
-
|
1234
|
-
Log.error("Failed retry for #{request.trace} #{request.type} because of send failure")
|
1235
|
-
result = OperationResult.non_delivery("retry failed")
|
1236
|
-
handle_response(Result.new(request.token, request.reply_to, result, @identity))
|
749
|
+
Log.error("Failed retry for #{packet.trace} #{packet.type} because of send failure")
|
1237
750
|
rescue Exception => e
|
1238
751
|
# Not sending a response here because something more basic is broken in the retry
|
1239
752
|
# mechanism and don't want an error response to preempt a delayed actual response
|
1240
|
-
Log.error("Failed retry for #{
|
1241
|
-
@exception_stats.track("retry", e,
|
753
|
+
Log.error("Failed retry for #{packet.trace} #{packet.type} without responding", e, :trace)
|
754
|
+
@exception_stats.track("retry", e, packet)
|
1242
755
|
end
|
1243
756
|
end
|
1244
757
|
end
|
1245
758
|
true
|
1246
759
|
end
|
1247
760
|
|
1248
|
-
# Deliver the response and remove associated
|
761
|
+
# Deliver the response and remove associated non-push requests from pending
|
762
|
+
# including all associated retry requests
|
1249
763
|
#
|
1250
764
|
# === Parameters
|
1251
765
|
# response(Result):: Packet received as result of request
|
1252
|
-
#
|
766
|
+
# pending_request(Hash):: Associated pending request
|
1253
767
|
#
|
1254
768
|
# === Return
|
1255
769
|
# true:: Always return true
|
1256
|
-
def
|
1257
|
-
@request_stats.finish(
|
770
|
+
def deliver_response(response, pending_request)
|
771
|
+
@request_stats.finish(pending_request.receive_time, response.token)
|
1258
772
|
|
1259
|
-
@pending_requests.delete(response.token) if
|
1260
|
-
if
|
1261
|
-
@pending_requests.reject! { |k, v| k ==
|
773
|
+
@pending_requests.delete(response.token) if pending_request.kind == :send_request
|
774
|
+
if (parent_token = pending_request.retry_parent_token)
|
775
|
+
@pending_requests.reject! { |k, v| k == parent_token || v.retry_parent_token == parent_token }
|
1262
776
|
end
|
1263
777
|
|
1264
|
-
|
778
|
+
pending_request.response_handler.call(response) if pending_request.response_handler
|
1265
779
|
true
|
1266
780
|
end
|
1267
781
|
|
1268
|
-
#
|
782
|
+
# Determine whether currently queueing requests because offline
|
1269
783
|
#
|
1270
784
|
# === Return
|
1271
|
-
# (Boolean):: true if
|
1272
|
-
def
|
785
|
+
# (Boolean):: true if queueing, otherwise false
|
786
|
+
def queueing?
|
1273
787
|
@options[:offline_queueing] && @offline_handler.queueing?
|
1274
788
|
end
|
1275
789
|
|