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.
Files changed (67) hide show
  1. data/README.rdoc +10 -8
  2. data/Rakefile +31 -5
  3. data/lib/right_agent.rb +6 -1
  4. data/lib/right_agent/actor.rb +4 -20
  5. data/lib/right_agent/actors/agent_manager.rb +1 -1
  6. data/lib/right_agent/agent.rb +357 -144
  7. data/lib/right_agent/agent_config.rb +7 -6
  8. data/lib/right_agent/agent_identity.rb +13 -11
  9. data/lib/right_agent/agent_tag_manager.rb +60 -64
  10. data/{spec/results_mock.rb → lib/right_agent/clients.rb} +10 -24
  11. data/lib/right_agent/clients/api_client.rb +383 -0
  12. data/lib/right_agent/clients/auth_client.rb +247 -0
  13. data/lib/right_agent/clients/balanced_http_client.rb +369 -0
  14. data/lib/right_agent/clients/base_retry_client.rb +495 -0
  15. data/lib/right_agent/clients/right_http_client.rb +279 -0
  16. data/lib/right_agent/clients/router_client.rb +493 -0
  17. data/lib/right_agent/command/command_io.rb +4 -4
  18. data/lib/right_agent/command/command_parser.rb +2 -2
  19. data/lib/right_agent/command/command_runner.rb +1 -1
  20. data/lib/right_agent/connectivity_checker.rb +179 -0
  21. data/lib/right_agent/core_payload_types/secure_document_location.rb +2 -2
  22. data/lib/right_agent/dispatcher.rb +12 -10
  23. data/lib/right_agent/enrollment_result.rb +16 -12
  24. data/lib/right_agent/exceptions.rb +34 -20
  25. data/lib/right_agent/history.rb +10 -5
  26. data/lib/right_agent/log.rb +5 -5
  27. data/lib/right_agent/minimal.rb +1 -0
  28. data/lib/right_agent/multiplexer.rb +1 -1
  29. data/lib/right_agent/offline_handler.rb +270 -0
  30. data/lib/right_agent/packets.rb +7 -7
  31. data/lib/right_agent/payload_formatter.rb +1 -1
  32. data/lib/right_agent/pending_requests.rb +128 -0
  33. data/lib/right_agent/platform.rb +1 -1
  34. data/lib/right_agent/protocol_version_mixin.rb +69 -0
  35. data/lib/right_agent/{idempotent_request.rb → retryable_request.rb} +7 -7
  36. data/lib/right_agent/scripts/agent_controller.rb +28 -26
  37. data/lib/right_agent/scripts/agent_deployer.rb +37 -22
  38. data/lib/right_agent/scripts/common_parser.rb +10 -3
  39. data/lib/right_agent/secure_identity.rb +1 -1
  40. data/lib/right_agent/sender.rb +299 -785
  41. data/lib/right_agent/serialize/secure_serializer.rb +3 -1
  42. data/lib/right_agent/serialize/secure_serializer_initializer.rb +2 -2
  43. data/lib/right_agent/serialize/serializable.rb +8 -3
  44. data/right_agent.gemspec +49 -18
  45. data/spec/agent_config_spec.rb +7 -7
  46. data/spec/agent_identity_spec.rb +7 -4
  47. data/spec/agent_spec.rb +43 -7
  48. data/spec/agent_tag_manager_spec.rb +72 -83
  49. data/spec/clients/api_client_spec.rb +423 -0
  50. data/spec/clients/auth_client_spec.rb +272 -0
  51. data/spec/clients/balanced_http_client_spec.rb +576 -0
  52. data/spec/clients/base_retry_client_spec.rb +635 -0
  53. data/spec/clients/router_client_spec.rb +594 -0
  54. data/spec/clients/spec_helper.rb +111 -0
  55. data/spec/command/command_io_spec.rb +1 -1
  56. data/spec/command/command_parser_spec.rb +1 -1
  57. data/spec/connectivity_checker_spec.rb +83 -0
  58. data/spec/dispatcher_spec.rb +3 -2
  59. data/spec/enrollment_result_spec.rb +2 -2
  60. data/spec/history_spec.rb +51 -39
  61. data/spec/offline_handler_spec.rb +340 -0
  62. data/spec/pending_requests_spec.rb +136 -0
  63. data/spec/{idempotent_request_spec.rb → retryable_request_spec.rb} +73 -73
  64. data/spec/sender_spec.rb +835 -1052
  65. data/spec/serialize/secure_serializer_spec.rb +3 -2
  66. data/spec/spec_helper.rb +54 -1
  67. metadata +71 -12
@@ -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.__send__(target, *args)
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 IdempotentRequest
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):: Targets from which to randomly choose one
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.send_retryable_request(@operation, @payload, retrieve_target(@targets)) { |r| handle_response(r) }
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 # IdempotentRequest
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 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 base id ID to build agent's identity
55
- # --token, -t TOKEN Use token TOKEN to build agent's identity
56
- # --prefix, -x PREFIX Use prefix PREFIX to build agent's identity
57
- # --type TYPE Use agent type TYPE to build agent's' identity,98589
58
- # defaults to AGENT with any trailing '_[0-9]+' removed
59
- # --list, -l List all configured agents
60
- # --user, -u USER Set AMQP user
61
- # --pass, -p PASS Set AMQP password
62
- # --vhost, -v VHOST Set AMQP vhost
63
- # --host, -h HOST Set AMQP server hostname
64
- # --port, -P PORT Set AMQP server port
65
- # --cfg-dir, -c DIR Set directory containing configuration for all agents
66
- # --pid-dir, -z DIR Set directory containing agent process id files
67
- # --log-dir DIR Set log directory
68
- # --log-level LVL Log level (debug, info, warning, error or fatal)
69
- # --foreground, -f Run agent in foreground
70
- # --interactive, -I Spawn an irb console after starting agent
71
- # --test Use test settings
72
- # --help Display help
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 monit restart
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 id ID to build agent's identity
26
- # --token, -t TOKEN Use token TOKEN to build agent's identity
27
- # --prefix, -x PREFIX Use prefix PREFIX to build agent's identity
28
- # --type TYPE Use agent type TYPE to build agent's' identity,
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
- # --secure-identity, -S Derive actual token from given TOKEN and ID
31
- # --url Set agent AMQP connection URL (host, port, user, pass, vhost)
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 mapper to check connectivity, 0 means disable ping
47
- # --reconnect-interval SEC Set number of seconds between broker reconnect attempts
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, -m Generate monit configuration file
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('-w', '--monit') do
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 or mapper identity from options
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[:token]
127
- token = RightScale::SecureIdentity.derive(base_id, options[:token]) if options[:secure_identity]
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 delimeter. Thus a public token can always be
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
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright (c) 2009-2012 RightScale Inc
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
- # This class allows sending requests to agents without having to run a local mapper
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 broker connections
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
- class SendFailure < Exception; end
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
- end # ConnectivityChecker
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
- attr_accessor :pending_requests
42
+ attr_reader :pending_requests
544
43
 
545
- # (OfflineHandler) Handler for requests when disconnected from broker
44
+ # (OfflineHandler) Handler for requests when client disconnected
546
45
  attr_reader :offline_handler
547
46
 
548
- # (ConnectivityChecker) Broker connection checker
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, broker, and following options:
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 not connected to any brokers,
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 broker connections status changes
579
- # :ping_interval(Integer):: Minimum number of seconds since last message receipt to ping the mapper
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 mapper when online
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
- @broker = @agent.broker
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 = ConnectivityChecker.new(self, @options[:ping_interval] || 0, @ping_stats, @exception_stats)
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 to the mapper
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 to mapper after in memory queue gets flushed
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 offline
149
+ # Determine whether currently connected to RightNet via client
644
150
  #
645
151
  # === Return
646
- # (Boolean):: true if offline or if not connected to any brokers, otherwise false
647
- def offline?
648
- (@options[:offline_queueing] && @offline_handler.offline?) || @broker.connected.size == 0
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(String|Hash):: Identity of specific target, hash for selecting potentially multiple
713
- # targets, or nil if routing solely using type
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 mapper published the
726
- # request and any additional responses indicating any failures to actually route the request
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
- # SendFailure:: If publishing of request failed unexpectedly
734
- # TemporarilyOffline:: If cannot publish request because currently not connected
735
- # to any brokers and offline queueing is disabled
736
- def send_persistent_push(type, payload = nil, target = nil, &callback)
737
- build_and_send_packet(:send_persistent_push, type, payload, target, callback)
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(String|Hash):: Identity of specific target, hash for selecting targets of which one is picked
754
- # randomly, or nil if routing solely using type
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
- # SendFailure:: If publishing of request failed unexpectedly
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(:send_retryable_request, type, payload, target, callback)
248
+ build_and_send_packet(:send_request, type, payload, target, token, callback)
777
249
  end
778
250
 
779
- # Send a request to a single target with a response expected
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(String|Hash):: Identity of specific target, hash for selecting targets of which one is picked
792
- # randomly, or nil if routing solely using type
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
- # === Block
800
- # Required block used to process response asynchronously with the following parameter:
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 block missing
809
- # TemporarilyOffline:: If cannot publish request because currently not connected
810
- # to any brokers and offline queueing is disabled
811
- # SendFailure:: If publishing of request failed unexpectedly
812
- def send_persistent_request(type, payload = nil, target = nil, &callback)
813
- raise ArgumentError, "Missing block for response callback" unless callback
814
- build_and_send_packet(:send_persistent_request, type, payload, target, callback)
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 send request: :send_push, :send_persistent_push, :send_retryable_request,
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(String|Hash):: Identity of specific target, or hash for selecting targets
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, !!(kind.to_s =~ /push/))
840
- if should_queue?
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
- kind_str = kind.to_s
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 !persistent && ttl && ttl != 0
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 = AgentIdentity.generate
858
- packet.persistent = persistent
323
+ packet.token = token || RightSupport::Data::UUID.generate
859
324
  if target.is_a?(Hash)
860
- packet.tags = target[:tags] || []
861
- packet.scope = target[:scope]
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
- if result = OperationResult.from_results(response)
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 handler = @pending_requests[token]
921
- if result && result.non_delivery? && handler.kind == :send_retryable_request
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 parent = handler.retry_parent
926
- @pending_requests[parent].non_delivery = result.content
364
+ if (parent_token = pending_request.retry_parent_token)
365
+ @pending_requests[parent_token].non_delivery = result.content
927
366
  else
928
- handler.non_delivery = result.content
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 && handler.non_delivery
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(handler.non_delivery)
934
- deliver(response, handler)
372
+ response.results = OperationResult.non_delivery(pending_request.non_delivery)
373
+ deliver_response(response, pending_request)
935
374
  else
936
- deliver(response, handler)
375
+ deliver_response(response, pending_request)
937
376
  end
938
377
  else
939
- deliver(response, handler)
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 mapper interaction in preparation
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.terminate
957
- @connectivity_checker.terminate
958
- pending = @pending_requests.kind(PendingRequests::REQUEST_KINDS)
959
- [pending.size, pending.youngest_age]
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.kind(PendingRequests::REQUEST_KINDS).each do |token, request|
970
- info << "#{request.receive_time.localtime} <#{token}>"
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
- offlines = @offline_stats.all
1010
- offlines.merge!("duration" => @offline_stats.avg_duration) if offlines
1011
- if @pending_requests.size > 0
1012
- pending = {}
1013
- pending["pushes"] = @pending_requests.kind(PendingRequests::PUSH_KINDS).size
1014
- requests = @pending_requests.kind(PendingRequests::REQUEST_KINDS)
1015
- if (pending["requests"] = requests.size) > 0
1016
- pending["oldest age"] = requests.oldest_age
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 selector = t[:selector]
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
- # Build and send packet
574
+ # Send request via HTTP and then immediately handle response
1121
575
  #
1122
576
  # === Parameters
1123
- # kind(Symbol):: Kind of send request: :send_push, :send_persistent_push, :send_retryable_request,
1124
- # or :send_persistent_request
1125
- # type(String):: Dispatch route for the request; typically identifies actor and action
1126
- # payload(Object):: Data to be sent with marshalling en route
1127
- # target(String|Hash):: Identity of specific target, or hash for selecting targets
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
- # === Raise
1140
- # ArgumentError:: If target is invalid
1141
- # SendFailure:: If publishing of request fails unexpectedly
1142
- # TemporarilyOffline:: If cannot publish request because currently not connected
1143
- # to any brokers and offline queueing is disabled
1144
- def build_and_send_packet(kind, type, payload, target, callback)
1145
- packet = build_packet(kind, type, payload, target, callback)
1146
- send_packet(kind, packet, callback)
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
- # Publish request to request queue
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):: Packet to be sent
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 to
677
+ # (Array):: Identity of brokers to which request was published
1159
678
  #
1160
679
  # === Raise
1161
- # SendFailure:: If publishing of request fails unexpectedly
1162
- # TemporarilyOffline:: If cannot publish request because currently not connected
1163
- # to any brokers and offline queueing is disabled
1164
- def publish(request, ids = nil)
1165
- begin
1166
- exchange = {:type => :fanout, :name => "request", :options => {:durable => true, :no_declare => @secure}}
1167
- @broker.publish(exchange, request, :persistent => request.persistent, :mandatory => true,
1168
- :log_filter => [:tags, :target, :tries, :persistent], :brokers => ids)
1169
- rescue RightAMQP::HABrokerClient::NoConnectedBrokers => e
1170
- msg = "Failed to publish request #{request.trace} #{request.type}"
1171
- Log.error(msg, e)
1172
- @send_failure_stats.update("NoConnectedBrokers")
1173
- raise TemporarilyOffline.new(msg + " (#{e.class}: #{e.message})")
1174
- rescue Exception => e
1175
- msg = "Failed to publish request #{request.trace} #{request.type}"
1176
- Log.error(msg, e, :trace)
1177
- @send_failure_stats.update(e.class.name)
1178
- @exception_stats.track("publish", e, request)
1179
- raise SendFailure.new(msg + " (#{e.class}: #{e.message})")
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
- # Publish request with one or more retries if do not receive a response in time
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
- # request(Request):: Request to be sent
1192
- # parent(String):: Token for original request
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 publish_with_timeout_retry(request, parent, count = 0, multiplier = 1, elapsed = 0, broker_ids = nil)
1201
- published_broker_ids = publish(request, broker_ids)
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 && parent
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 handler = @pending_requests[parent]
726
+ if @pending_requests[parent_token]
1208
727
  count += 1
1209
728
  elapsed += interval
1210
729
  if elapsed < @retry_timeout
1211
- request.tries << request.token
1212
- request.token = AgentIdentity.generate
1213
- @pending_requests[parent].retry_parent = parent if count == 1
1214
- @pending_requests[request.token] = @pending_requests[parent]
1215
- broker_ids ||= @broker.all
1216
- publish_with_timeout_retry(request, parent, count, multiplier * RETRY_BACKOFF_FACTOR, elapsed,
1217
- broker_ids.push(broker_ids.shift))
1218
- @retry_stats.update(request.type.split('/').last)
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 #{request.trace} #{request.type}")
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(request.token, request.reply_to, result, @identity))
742
+ handle_response(Result.new(packet.token, @identity, result, @identity))
1224
743
  end
1225
- @connectivity_checker.check(published_broker_ids.first) if count == 1 && !published_broker_ids.empty?
744
+ @connectivity_checker.check(check_broker_ids.first) if check_broker_ids.any? && count == 1
1226
745
  end
1227
746
  rescue TemporarilyOffline => e
1228
- # Send retry response so that requester, e.g., IdempotentRequest, can retry
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
- # Send non-delivery response so that requester, e.g., IdempotentRequest, can retry
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 #{request.trace} #{request.type} without responding", e, :trace)
1241
- @exception_stats.track("retry", e, request)
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 request(s) from pending
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
- # handler(Hash):: Associated request handler
766
+ # pending_request(Hash):: Associated pending request
1253
767
  #
1254
768
  # === Return
1255
769
  # true:: Always return true
1256
- def deliver(response, handler)
1257
- @request_stats.finish(handler.receive_time, response.token)
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 PendingRequests::REQUEST_KINDS.include?(handler.kind)
1260
- if parent = handler.retry_parent
1261
- @pending_requests.reject! { |k, v| k == parent || v.retry_parent == parent }
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
- handler.response_handler.call(response) if handler.response_handler
778
+ pending_request.response_handler.call(response) if pending_request.response_handler
1265
779
  true
1266
780
  end
1267
781
 
1268
- # Should agent be queueing current request?
782
+ # Determine whether currently queueing requests because offline
1269
783
  #
1270
784
  # === Return
1271
- # (Boolean):: true if should queue request, otherwise false
1272
- def should_queue?
785
+ # (Boolean):: true if queueing, otherwise false
786
+ def queueing?
1273
787
  @options[:offline_queueing] && @offline_handler.queueing?
1274
788
  end
1275
789