right_agent 0.13.5 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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