right_agent 0.13.5 → 0.14.0

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.
@@ -24,7 +24,7 @@ module RightScale
24
24
 
25
25
  # Cache for requests that have been dispatched recently
26
26
  # This cache is intended for use in checking for duplicate requests
27
- # Since this is a local cache, it is not usable for requests received from a shared queue
27
+ # when there is only one server servicing a queue
28
28
  class DispatchedCache
29
29
 
30
30
  # Maximum number of seconds to retain a dispatched request in cache
@@ -43,16 +43,15 @@ module RightScale
43
43
  @max_age = MAX_AGE
44
44
  end
45
45
 
46
- # Store dispatched request token in cache unless from shared queue
46
+ # Store dispatched request token in cache
47
47
  #
48
48
  # === Parameters
49
49
  # token(String):: Generated message identifier
50
- # shared_queue(String|nil):: Name of shared queue if being dispatched from a shared queue
51
50
  #
52
51
  # === Return
53
52
  # true:: Always return true
54
- def store(token, shared_queue)
55
- if token && shared_queue.nil?
53
+ def store(token)
54
+ if token
56
55
  now = Time.now.to_i
57
56
  if @cache.has_key?(token)
58
57
  @cache[token] = now
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright (c) 2009-2011 RightScale Inc
2
+ # Copyright (c) 2009-2012 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
@@ -25,8 +25,8 @@ module RightScale
25
25
  # Dispatching of payload to specified actor
26
26
  class Dispatcher
27
27
 
28
- # Response queue name
29
- RESPONSE_QUEUE = "response"
28
+ class InvalidRequestType < Exception; end
29
+ class DuplicateRequest < Exception; end
30
30
 
31
31
  # (ActorRegistry) Registry for actors
32
32
  attr_reader :registry
@@ -34,164 +34,67 @@ module RightScale
34
34
  # (String) Identity of associated agent
35
35
  attr_reader :identity
36
36
 
37
- # (RightAMQP::HABrokerClient) High availability AMQP broker client
38
- attr_reader :broker
39
-
40
- # (EM) Event machine class (exposed for unit tests)
41
- attr_accessor :em
37
+ # For direct access to current dispatcher
38
+ #
39
+ # === Return
40
+ # (Dispatcher):: This dispatcher instance if defined, otherwise nil
41
+ def self.instance
42
+ @@instance if defined?(@@instance)
43
+ end
42
44
 
43
45
  # Initialize dispatcher
44
46
  #
45
47
  # === Parameters
46
- # agent(Agent):: Agent using this dispatcher; uses its identity, broker, registry, and following options:
47
- # :secure(Boolean):: true indicates to use Security features of RabbitMQ to restrict agents to themselves
48
- # :single_threaded(Boolean):: true indicates to run all operations in one thread; false indicates
49
- # to do requested work on event machine defer thread and all else, such as pings on main thread
50
- # :threadpool_size(Integer):: Number of threads in event machine thread pool
48
+ # agent(Agent):: Agent using this dispatcher; uses its identity and registry
51
49
  # dispatched_cache(DispatchedCache|nil):: Cache for dispatched requests that is used for detecting
52
50
  # duplicate requests, or nil if duplicate checking is disabled
53
51
  def initialize(agent, dispatched_cache = nil)
54
52
  @agent = agent
55
- @broker = @agent.broker
56
53
  @registry = @agent.registry
57
54
  @identity = @agent.identity
58
- options = @agent.options
59
- @secure = options[:secure]
60
- @single_threaded = options[:single_threaded]
61
- @pending_dispatches = 0
62
- @em = EM
63
- @em.threadpool_size = (options[:threadpool_size] || 20).to_i
55
+ @dispatched_cache = dispatched_cache
64
56
  reset_stats
57
+ @@instance = self
58
+ end
65
59
 
66
- # Only access this cache from primary thread
67
- @dispatched_cache = dispatched_cache
60
+ # Determine whether able to route requests to specified actor
61
+ #
62
+ # === Parameters
63
+ # actor(String):: Actor name
64
+ #
65
+ # === Return
66
+ # (Boolean):: true if can route to actor, otherwise false
67
+ def routable?(actor)
68
+ !!@registry.actor_for(actor)
68
69
  end
69
70
 
70
- # Dispatch request to appropriate actor for servicing
71
- # Handle returning of result to requester including logging any exceptions
71
+ # Route request to appropriate actor for servicing
72
72
  # Reject requests whose TTL has expired or that are duplicates of work already dispatched
73
- # Work is done in background defer thread if single threaded option is false
74
- # Acknowledge request after actor has responded
75
73
  #
76
74
  # === Parameters
77
75
  # request(Request|Push):: Packet containing request
78
76
  # header(AMQP::Frame::Header|nil):: Request header containing ack control
79
- # shared_queue(String|nil):: Name of shared queue if being dispatched from a shared queue
80
77
  #
81
78
  # === Return
82
- # (Result|nil):: Result from dispatched request, nil if not dispatched because dup or stale
83
- def dispatch(request, header = nil, shared_queue = nil)
84
- begin
85
- ack_deferred = false
86
-
87
- # Determine which actor this request is for
88
- prefix, method = request.type.split('/')[1..-1]
89
- method ||= :index
90
- method = method.to_sym
91
- actor = @registry.actor_for(prefix)
92
- token = request.token
93
- received_at = @requests.update(method, (token if request.kind_of?(Request)))
94
- if actor.nil?
95
- Log.error("No actor for dispatching request <#{token}> of type #{request.type}")
96
- return nil
97
- end
98
- method_idempotent = actor.class.idempotent?(method)
99
-
100
- # Reject this request if its TTL has expired
101
- if (expires_at = request.expires_at) && expires_at > 0 && received_at.to_i >= expires_at
102
- @rejects.update("expired (#{method})")
103
- Log.info("REJECT EXPIRED <#{token}> from #{request.from} TTL #{RightSupport::Stats.elapsed(received_at.to_i - expires_at)} ago")
104
- if request.is_a?(Request)
105
- # For agents that do not know about non-delivery, use error result
106
- non_delivery = if request.recv_version < 13
107
- OperationResult.error("Could not deliver request (#{OperationResult::TTL_EXPIRATION})")
108
- else
109
- OperationResult.non_delivery(OperationResult::TTL_EXPIRATION)
110
- end
111
- result = Result.new(token, request.reply_to, non_delivery, @identity, request.from, request.tries, request.persistent)
112
- exchange = {:type => :queue, :name => RESPONSE_QUEUE, :options => {:durable => true, :no_declare => @secure}}
113
- @broker.publish(exchange, result, :persistent => true, :mandatory => true)
114
- end
115
- return nil
116
- end
117
-
118
- # Reject this request if it is a duplicate
119
- if !method_idempotent && @dispatched_cache
120
- if by = @dispatched_cache.serviced_by(token)
121
- @rejects.update("duplicate (#{method})")
122
- Log.info("REJECT DUP <#{token}> serviced by #{by == @identity ? 'self' : by}")
123
- return nil
124
- end
125
- request.tries.each do |t|
126
- if by = @dispatched_cache.serviced_by(t)
127
- @rejects.update("retry duplicate (#{method})")
128
- Log.info("REJECT RETRY DUP <#{token}> of <#{t}> serviced by #{by == @identity ? 'self' : by}")
129
- return nil
130
- end
131
- end
132
- end
133
-
134
- # Proc for performing request in actor
135
- operation = lambda do
136
- begin
137
- @pending_dispatches += 1
138
- @last_request_dispatch_time = received_at.to_i
139
- @dispatched_cache.store(token, shared_queue) if !method_idempotent && @dispatched_cache
140
- if actor.method(method).arity.abs == 1
141
- actor.__send__(method, request.payload)
142
- else
143
- actor.__send__(method, request.payload, request)
144
- end
145
- rescue Exception => e
146
- @pending_dispatches = [@pending_dispatches - 1, 0].max
147
- OperationResult.error(handle_exception(actor, method, request, e))
148
- end
149
- end
150
-
151
- # Proc for sending response
152
- callback = lambda do |r|
153
- begin
154
- if request.kind_of?(Request)
155
- duration = @requests.finish(received_at, token)
156
- r = Result.new(token, request.reply_to, r, @identity, request.from, request.tries, request.persistent, duration)
157
- exchange = {:type => :queue, :name => RESPONSE_QUEUE, :options => {:durable => true, :no_declare => @secure}}
158
- @broker.publish(exchange, r, :persistent => true, :mandatory => true, :log_filter => [:tries, :persistent, :duration])
159
- end
160
- rescue RightAMQP::HABrokerClient::NoConnectedBrokers => e
161
- Log.error("Failed to publish result of dispatched request #{request.trace}", e)
162
- rescue Exception => e
163
- Log.error("Failed to publish result of dispatched request #{request.trace}", e, :trace)
164
- @exceptions.track("publish response", e)
165
- ensure
166
- header.ack if header
167
- @pending_dispatches = [@pending_dispatches - 1, 0].max
168
- end
169
- r # For unit tests
170
- end
171
-
172
- # Process request and send response, if any
173
- begin
174
- ack_deferred = true
175
- if @single_threaded
176
- @em.next_tick { callback.call(operation.call) }
177
- else
178
- @em.defer(operation, callback)
179
- end
180
- rescue Exception
181
- header.ack if header
182
- raise
183
- end
184
- ensure
185
- header.ack unless ack_deferred || header.nil?
186
- end
187
- end
188
-
189
- # Determine age of youngest request dispatch
79
+ # (Result|nil):: Result of request, or nil if there is no result because request is a Push
190
80
  #
191
- # === Return
192
- # (Integer|nil):: Age in seconds of youngest dispatch, or nil if none
193
- def dispatch_age
194
- Time.now.to_i - @last_request_dispatch_time if @last_request_dispatch_time && @pending_dispatches > 0
81
+ # === Raise
82
+ # InvalidRequestType:: If the request cannot be routed to an actor
83
+ # DuplicateRequest:: If request rejected because it has already been processed
84
+ def dispatch(request)
85
+ token = request.token
86
+ actor, method, idempotent = route(request)
87
+ received_at = @request_stats.update(method, (token if request.is_a?(Request)))
88
+ if dup = duplicate?(request, method, idempotent)
89
+ raise DuplicateRequest.new(dup)
90
+ end
91
+ unless result = expired?(request, method)
92
+ result = perform(request, actor, method, idempotent)
93
+ end
94
+ if request.is_a?(Request)
95
+ duration = @request_stats.finish(received_at, token)
96
+ Result.new(token, request.reply_to, result, @identity, request.from, request.tries, request.persistent, duration)
97
+ end
195
98
  end
196
99
 
197
100
  # Get dispatcher statistics
@@ -203,30 +106,25 @@ module RightScale
203
106
  # stats(Hash):: Current statistics:
204
107
  # "dispatched cache"(Hash|nil):: Number of dispatched requests cached and age of youngest and oldest,
205
108
  # or nil if empty
109
+ # "dispatch failures"(Hash|nil):: Dispatch failure activity stats with keys "total", "percent", "last", and "rate"
110
+ # with percentage breakdown per failure type, or nil if none
206
111
  # "exceptions"(Hash|nil):: Exceptions raised per category, or nil if none
207
112
  # "total"(Integer):: Total for category
208
113
  # "recent"(Array):: Most recent as a hash of "count", "type", "message", "when", and "where"
209
114
  # "rejects"(Hash|nil):: Request reject activity stats with keys "total", "percent", "last", and "rate"
210
- # "pending"(Hash|nil):: Pending request "total" and "youngest age", or nil if none
211
115
  # with percentage breakdown per reason ("duplicate (<method>)", "retry duplicate (<method>)", or
212
116
  # "stale (<method>)"), or nil if none
213
117
  # "requests"(Hash|nil):: Request activity stats with keys "total", "percent", "last", and "rate"
214
118
  # with percentage breakdown per request type, or nil if none
215
119
  # "response time"(Float):: Average number of seconds to respond to a request recently
216
120
  def stats(reset = false)
217
- pending = if @pending_dispatches > 0
218
- {
219
- "total" => @pending_dispatches,
220
- "youngest age" => dispatch_age
221
- }
222
- end
223
121
  stats = {
224
- "dispatched cache" => (@dispatched_cache.stats if @dispatched_cache),
225
- "exceptions" => @exceptions.stats,
226
- "pending" => pending,
227
- "rejects" => @rejects.all,
228
- "requests" => @requests.all,
229
- "response time" => @requests.avg_duration
122
+ "dispatched cache" => (@dispatched_cache.stats if @dispatched_cache),
123
+ "dispatch failures" => @dispatch_failure_stats.all,
124
+ "exceptions" => @exception_stats.stats,
125
+ "rejects" => @reject_stats.all,
126
+ "requests" => @request_stats.all,
127
+ "response time" => @request_stats.avg_duration
230
128
  }
231
129
  reset_stats if reset
232
130
  stats
@@ -239,12 +137,103 @@ module RightScale
239
137
  # === Return
240
138
  # true:: Always return true
241
139
  def reset_stats
242
- @rejects = RightSupport::Stats::Activity.new
243
- @requests = RightSupport::Stats::Activity.new
244
- @exceptions = RightSupport::Stats::Exceptions.new(@agent)
140
+ @reject_stats = RightSupport::Stats::Activity.new
141
+ @request_stats = RightSupport::Stats::Activity.new
142
+ @dispatch_failure_stats = RightSupport::Stats::Activity.new
143
+ @exception_stats = RightSupport::Stats::Exceptions.new(@agent)
245
144
  true
246
145
  end
247
146
 
147
+ # Determine if request TTL has expired
148
+ #
149
+ # === Parameters
150
+ # request(Push|Request):: Request to be checked
151
+ # method(String):: Actor method requested to be performed
152
+ #
153
+ # === Return
154
+ # (OperationResult|nil):: Error result if expired, otherwise nil
155
+ def expired?(request, method)
156
+ if (expires_at = request.expires_at) && expires_at > 0 && (now = Time.now.to_i) >= expires_at
157
+ @reject_stats.update("expired (#{method})")
158
+ Log.info("REJECT EXPIRED <#{request.token}> from #{request.from} TTL #{RightSupport::Stats.elapsed(now - expires_at)} ago")
159
+ # For agents that do not know about non-delivery, use error result
160
+ if request.recv_version < 13
161
+ OperationResult.error("Could not deliver request (#{OperationResult::TTL_EXPIRATION})")
162
+ else
163
+ OperationResult.non_delivery(OperationResult::TTL_EXPIRATION)
164
+ end
165
+ end
166
+ end
167
+
168
+ # Determine whether this request is a duplicate
169
+ #
170
+ # === Parameters
171
+ # request(Request|Push):: Packet containing request
172
+ # method(String):: Actor method requested to be performed
173
+ # idempotent(Boolean):: Whether this method is idempotent
174
+ #
175
+ # === Return
176
+ # (String|nil):: Messaging describing who already serviced request if it is a duplicate, otherwise nil
177
+ def duplicate?(request, method, idempotent)
178
+ if !idempotent && @dispatched_cache
179
+ if serviced_by = @dispatched_cache.serviced_by(request.token)
180
+ from_retry = ""
181
+ else
182
+ from_retry = "retry "
183
+ request.tries.each { |t| break if serviced_by = @dispatched_cache.serviced_by(t) }
184
+ end
185
+ if serviced_by
186
+ @reject_stats.update("#{from_retry}duplicate (#{method})")
187
+ msg = "<#{request.token}> already serviced by #{serviced_by == @identity ? 'self' : serviced_by}"
188
+ Log.info("REJECT #{from_retry.upcase}DUP #{msg}")
189
+ msg
190
+ end
191
+ end
192
+ end
193
+
194
+ # Use request type to route request to actor and an associated method
195
+ #
196
+ # === Parameters
197
+ # request(Push|Request):: Packet containing request
198
+ #
199
+ # === Return
200
+ # (Array):: Actor name, method name, and whether method is idempotent
201
+ #
202
+ # === Raise
203
+ # InvalidRequestType:: If the request cannot be routed to an actor
204
+ def route(request)
205
+ prefix, method = request.type.split('/')[1..-1]
206
+ method ||= :index
207
+ method = method.to_sym
208
+ actor = @registry.actor_for(prefix)
209
+ if actor.nil? || !actor.respond_to?(method)
210
+ raise InvalidRequestType.new("Unknown actor or method for dispatching request <#{request.token}> of type #{request.type}")
211
+ end
212
+ [actor, method, actor.class.idempotent?(method)]
213
+ end
214
+
215
+ # Perform requested action
216
+ #
217
+ # === Parameters
218
+ # request(Push|Request):: Packet containing request
219
+ # token(String):: Unique identity token for request
220
+ # method(String):: Actor method requested to be performed
221
+ # idempotent(Boolean):: Whether this method is idempotent
222
+ #
223
+ # === Return
224
+ # (OperationResult):: Result from performing a request
225
+ def perform(request, actor, method, idempotent)
226
+ @dispatched_cache.store(request.token) if @dispatched_cache && !idempotent
227
+ if actor.method(method).arity.abs == 1
228
+ actor.__send__(method, request.payload)
229
+ else
230
+ actor.__send__(method, request.payload, request)
231
+ end
232
+ rescue Exception => e
233
+ @dispatch_failure_stats.update("#{request.type}->#{e.class.name}")
234
+ OperationResult.error(handle_exception(actor, method, request, e))
235
+ end
236
+
248
237
  # Handle exception by logging it, calling the actors exception callback method,
249
238
  # and gathering exception statistics
250
239
  #
@@ -268,10 +257,10 @@ module RightScale
268
257
  actor.instance_exec(method, request, exception, &actor.class.exception_callback)
269
258
  end
270
259
  end
271
- @exceptions.track(request.type, exception)
260
+ @exception_stats.track(request.type, exception)
272
261
  rescue Exception => e
273
262
  Log.error("Failed handling error for #{request.type}", e, :trace)
274
- @exceptions.track(request.type, e) rescue nil
263
+ @exception_stats.track(request.type, e) rescue nil
275
264
  end
276
265
  Log.format(error, exception)
277
266
  end
@@ -56,7 +56,7 @@ module RightScale
56
56
  if process_running?(pid)
57
57
  raise AlreadyRunning.new("#{@pid_file} already exists and process is running (pid: #{pid})")
58
58
  else
59
- Log.info "removing stale pid file: #{@pid_file}"
59
+ Log.info("Removing stale pid file: #{@pid_file}")
60
60
  remove
61
61
  end
62
62
  end
@@ -250,6 +250,20 @@ module RightScale
250
250
  platform_service(:installer)
251
251
  end
252
252
 
253
+ # Determines which cloud we're on by the cheap but simple expedient of
254
+ # reading the RightScale cloud file
255
+ def resolve_cloud_type
256
+ cloud_type = read_cloud_file
257
+ @ec2 = false
258
+ @rackspace = false
259
+ @eucalyptus = false
260
+ case cloud_type
261
+ when 'ec2' then @ec2 = true
262
+ when 'rackspace' then @rackspace = true
263
+ when 'eucalyptus' then @eucalyptus = true
264
+ end
265
+ end
266
+
253
267
  private
254
268
 
255
269
  # Load platform specific implementation
@@ -285,20 +299,6 @@ module RightScale
285
299
  require File.expand_path(File.join(File.dirname(__FILE__), 'platform', 'windows'))
286
300
  end
287
301
 
288
- # Determines which cloud we're on by the cheap but simple expedient of
289
- # reading the RightScale cloud file
290
- def resolve_cloud_type
291
- cloud_type = read_cloud_file
292
- @ec2 = false
293
- @rackspace = false
294
- @eucalyptus = false
295
- case cloud_type
296
- when 'ec2' then @ec2 = true
297
- when 'rackspace' then @rackspace = true
298
- when 'eucalyptus' then @eucalyptus = true
299
- end
300
- end
301
-
302
302
  # Reads the RightScale cloud file and returns its contents
303
303
  def read_cloud_file
304
304
  File.read(File.join(self.filesystem.right_scale_state_dir, 'cloud')) rescue nil
@@ -1,5 +1,5 @@
1
1
  # === Synopsis:
2
- # RightScale RightAgent Controller (rnac) - (c) 2009-2011 RightScale Inc
2
+ # RightScale RightAgent Controller (rnac) - (c) 2009-2012 RightScale Inc
3
3
  #
4
4
  # rnac is a command line tool for managing a RightAgent
5
5
  #
@@ -85,12 +85,10 @@ module RightScale
85
85
 
86
86
  FORCED_OPTIONS =
87
87
  {
88
- :threadpool_size => 1
89
88
  }
90
89
 
91
90
  DEFAULT_OPTIONS =
92
91
  {
93
- :single_threaded => true,
94
92
  :log_dir => Platform.filesystem.log_dir,
95
93
  :daemonize => true
96
94
  }
@@ -521,7 +519,7 @@ module RightScale
521
519
  end # RightScale
522
520
 
523
521
  #
524
- # Copyright (c) 2009-2011 RightScale Inc
522
+ # Copyright (c) 2009-2012 RightScale Inc
525
523
  #
526
524
  # Permission is hereby granted, free of charge, to any person obtaining
527
525
  # a copy of this software and associated documentation files (the