right_agent 0.10.13 → 0.13.5
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/right_agent.rb +2 -0
- data/lib/right_agent/actor.rb +45 -10
- data/lib/right_agent/actor_registry.rb +5 -5
- data/lib/right_agent/actors/agent_manager.rb +4 -4
- data/lib/right_agent/agent.rb +97 -37
- data/lib/right_agent/agent_tag_manager.rb +1 -2
- data/lib/right_agent/command/command_io.rb +1 -3
- data/lib/right_agent/command/command_runner.rb +9 -3
- data/lib/right_agent/dispatched_cache.rb +110 -0
- data/lib/right_agent/dispatcher.rb +119 -180
- data/lib/right_agent/history.rb +136 -0
- data/lib/right_agent/log.rb +6 -3
- data/lib/right_agent/monkey_patches/ruby_patch.rb +0 -1
- data/lib/right_agent/pid_file.rb +1 -1
- data/lib/right_agent/platform.rb +2 -2
- data/lib/right_agent/platform/linux.rb +8 -1
- data/lib/right_agent/platform/windows.rb +1 -1
- data/lib/right_agent/sender.rb +57 -41
- data/right_agent.gemspec +4 -4
- data/spec/actor_registry_spec.rb +7 -8
- data/spec/actor_spec.rb +87 -24
- data/spec/agent_spec.rb +107 -8
- data/spec/command/command_runner_spec.rb +12 -1
- data/spec/dispatched_cache_spec.rb +142 -0
- data/spec/dispatcher_spec.rb +110 -129
- data/spec/history_spec.rb +234 -0
- data/spec/idempotent_request_spec.rb +1 -1
- data/spec/log_spec.rb +15 -0
- data/spec/operation_result_spec.rb +4 -2
- data/spec/platform/darwin_spec.rb +13 -0
- data/spec/platform/linux_spec.rb +38 -0
- data/spec/platform/platform_spec.rb +46 -51
- data/spec/platform/windows_spec.rb +13 -0
- data/spec/sender_spec.rb +81 -38
- metadata +12 -9
- data/lib/right_agent/monkey_patches/ruby_patch/singleton_patch.rb +0 -45
- data/spec/platform/darwin.rb +0 -11
- data/spec/platform/linux.rb +0 -23
- data/spec/platform/windows.rb +0 -11
data/lib/right_agent.rb
CHANGED
@@ -47,7 +47,9 @@ require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'agent_tag_manager')
|
|
47
47
|
require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'actor'))
|
48
48
|
require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'actor_registry'))
|
49
49
|
require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'dispatcher'))
|
50
|
+
require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'dispatched_cache'))
|
50
51
|
require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'sender'))
|
51
52
|
require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'secure_identity'))
|
52
53
|
require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'idempotent_request'))
|
54
|
+
require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'history'))
|
53
55
|
require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'agent'))
|
data/lib/right_agent/actor.rb
CHANGED
@@ -59,21 +59,45 @@ module RightScale
|
|
59
59
|
prefix = to_s.to_const_path
|
60
60
|
end
|
61
61
|
|
62
|
+
# Add methods to list of services supported by actor and mark these methods
|
63
|
+
# as idempotent
|
64
|
+
#
|
65
|
+
# === Parameters
|
66
|
+
# methods(Array):: Symbol names for methods being exposed as actor idempotent services
|
67
|
+
#
|
68
|
+
# === Return
|
69
|
+
# true:: Always return true
|
70
|
+
def expose_idempotent(*methods)
|
71
|
+
@exposed ||= {}
|
72
|
+
methods.each do |m|
|
73
|
+
if @exposed[m] == false
|
74
|
+
Log.warning("Method #{m} declared both idempotent and non-idempotent, assuming non-idempotent")
|
75
|
+
else
|
76
|
+
@exposed[m] = true
|
77
|
+
end
|
78
|
+
end
|
79
|
+
true
|
80
|
+
end
|
81
|
+
|
62
82
|
# Add methods to list of services supported by actor
|
83
|
+
# By default these methods are not idempotent
|
63
84
|
#
|
64
85
|
# === Parameters
|
65
86
|
# meths(Array):: Symbol names for methods being exposed as actor services
|
66
87
|
#
|
67
88
|
# === Return
|
68
|
-
#
|
69
|
-
def
|
70
|
-
@exposed ||=
|
71
|
-
|
72
|
-
|
89
|
+
# true:: Always return true
|
90
|
+
def expose_non_idempotent(*methods)
|
91
|
+
@exposed ||= {}
|
92
|
+
methods.each do |m|
|
93
|
+
Log.warning("Method #{m} declared both idempotent and non-idempotent, assuming non-idempotent") if @exposed[m]
|
94
|
+
@exposed[m] = false
|
73
95
|
end
|
74
|
-
|
96
|
+
true
|
75
97
|
end
|
76
98
|
|
99
|
+
alias :expose :expose_non_idempotent
|
100
|
+
|
77
101
|
# Get /prefix/method paths that actor responds to
|
78
102
|
#
|
79
103
|
# === Parameters
|
@@ -83,14 +107,25 @@ module RightScale
|
|
83
107
|
# (Array):: /prefix/method strings
|
84
108
|
def provides_for(prefix)
|
85
109
|
return [] unless @exposed
|
86
|
-
@exposed.select do |
|
87
|
-
if instance_methods.include?(
|
110
|
+
@exposed.each_key.select do |method|
|
111
|
+
if instance_methods.include?(method.to_s) or instance_methods.include?(method.to_sym)
|
88
112
|
true
|
89
113
|
else
|
90
|
-
Log.warning("Exposing non-existing method #{
|
114
|
+
Log.warning("Exposing non-existing method #{method} in actor #{prefix}")
|
91
115
|
false
|
92
116
|
end
|
93
|
-
end.map {|
|
117
|
+
end.map { |method| "/#{prefix}/#{method}".squeeze('/') }
|
118
|
+
end
|
119
|
+
|
120
|
+
# Determine whether actor method is idempotent
|
121
|
+
#
|
122
|
+
# === Parameters
|
123
|
+
# method(Symbol):: Name for actor method
|
124
|
+
#
|
125
|
+
# === Return
|
126
|
+
# (Boolean):: true if idempotent, false otherwise
|
127
|
+
def idempotent?(method)
|
128
|
+
@exposed[method] if @exposed
|
94
129
|
end
|
95
130
|
|
96
131
|
# Set method called when dispatching to this actor fails
|
@@ -49,15 +49,15 @@ module RightScale
|
|
49
49
|
log_msg += ", prefix #{prefix}" if prefix && !prefix.empty?
|
50
50
|
Log.info(log_msg)
|
51
51
|
prefix ||= actor.class.default_prefix
|
52
|
-
actors[prefix.to_s] = actor
|
52
|
+
@actors[prefix.to_s] = actor
|
53
53
|
end
|
54
54
|
|
55
55
|
# Retrieve services provided by all of the registered actors
|
56
56
|
#
|
57
57
|
# === Return
|
58
|
-
#
|
58
|
+
# (Array):: List of unique /prefix/method path strings
|
59
59
|
def services
|
60
|
-
actors.map {|prefix, actor| actor.class.provides_for(prefix) }.flatten.uniq
|
60
|
+
@actors.map { |prefix, actor| actor.class.provides_for(prefix) }.flatten.uniq
|
61
61
|
end
|
62
62
|
|
63
63
|
# Retrieve actor by prefix
|
@@ -66,9 +66,9 @@ module RightScale
|
|
66
66
|
# prefix(String):: Prefix identifying actor
|
67
67
|
#
|
68
68
|
# === Return
|
69
|
-
#
|
69
|
+
# (Actor|nil):: Retrieved actor, or nil if unknown
|
70
70
|
def actor_for(prefix)
|
71
|
-
|
71
|
+
@actors[prefix]
|
72
72
|
end
|
73
73
|
|
74
74
|
end # ActorRegistry
|
@@ -30,8 +30,8 @@ class AgentManager
|
|
30
30
|
|
31
31
|
on_exception { |meth, deliverable, e| RightScale::ExceptionMailer.deliver_notification(meth, deliverable, e) }
|
32
32
|
|
33
|
-
|
34
|
-
|
33
|
+
expose_idempotent :ping, :stats, :profile, :set_log_level, :connect, :disconnect, :connect_failed, :tune_heartbeat
|
34
|
+
expose_non_idempotent :execute, :terminate
|
35
35
|
|
36
36
|
# Valid log levels
|
37
37
|
LEVELS = [:debug, :info, :warn, :error, :fatal]
|
@@ -255,8 +255,8 @@ class AgentManager
|
|
255
255
|
# true
|
256
256
|
def terminate(options = nil)
|
257
257
|
RightScale::CommandRunner.stop
|
258
|
-
# Delay terminate a bit to give
|
259
|
-
EM.
|
258
|
+
# Delay terminate a bit to give request message a chance to be ack'd and reply to be sent
|
259
|
+
EM.add_timer(1) { @agent.terminate }
|
260
260
|
true
|
261
261
|
end
|
262
262
|
|
data/lib/right_agent/agent.rb
CHANGED
@@ -73,9 +73,12 @@ module RightScale
|
|
73
73
|
:check_interval => 5 * 60,
|
74
74
|
:grace_timeout => 30,
|
75
75
|
:prefetch => 1,
|
76
|
-
:heartbeat =>
|
76
|
+
:heartbeat => 0
|
77
77
|
}
|
78
78
|
|
79
|
+
# Default block to be activated when finish terminating
|
80
|
+
DEFAULT_TERMINATE_BLOCK = lambda { EM.stop if EM.reactor_running? }
|
81
|
+
|
79
82
|
# Initializes a new agent and establishes an AMQP connection.
|
80
83
|
# This must be used inside EM.run block or if EventMachine reactor
|
81
84
|
# is already started, for instance, by a Thin server that your Merb/Rails
|
@@ -112,6 +115,7 @@ module RightScale
|
|
112
115
|
# :grace_timeout(Integer):: Maximum number of seconds to wait after last request received before
|
113
116
|
# terminating regardless of whether there are still unfinished requests
|
114
117
|
# :dup_check(Boolean):: Whether to check for and reject duplicate requests, e.g., due to retries
|
118
|
+
# or redelivery by broker after server failure
|
115
119
|
# :prefetch(Integer):: Maximum number of messages the AMQP broker is to prefetch for this agent
|
116
120
|
# before it receives an ack. Value 1 ensures that only last unacknowledged gets redelivered
|
117
121
|
# if the agent crashes. Value 0 means unlimited prefetch.
|
@@ -119,9 +123,10 @@ module RightScale
|
|
119
123
|
# exception(Exception):: Exception
|
120
124
|
# message(Packet):: Message being processed
|
121
125
|
# agent(Agent):: Reference to agent
|
122
|
-
# :ready_callback(Proc):: Called once agent is connected ready
|
123
|
-
# :restart_callback(Proc)::
|
124
|
-
#
|
126
|
+
# :ready_callback(Proc):: Called once agent is connected to broker and ready for service (no argument)
|
127
|
+
# :restart_callback(Proc):: Called on each restart vote with votes being initiated by offline queue
|
128
|
+
# exceeding MAX_QUEUED_REQUESTS or by repeated failures to access mapper when online (no argument)
|
129
|
+
# :abnormal_terminate_callback(Proc):: Called at end of termination when terminate abnormally (no argument)
|
125
130
|
# :services(Symbol):: List of services provided by this agent. Defaults to all methods exposed by actors.
|
126
131
|
# :secure(Boolean):: true indicates to use security features of RabbitMQ to restrict agents to themselves
|
127
132
|
# :single_threaded(Boolean):: true indicates to run all operations in one thread; false indicates
|
@@ -160,8 +165,8 @@ module RightScale
|
|
160
165
|
@tags.flatten!
|
161
166
|
@options.freeze
|
162
167
|
@deferred_tasks = []
|
163
|
-
@
|
164
|
-
@last_stat_reset_time =
|
168
|
+
@history = History.new(@identity)
|
169
|
+
@last_stat_reset_time = Time.now
|
165
170
|
reset_agent_stats
|
166
171
|
true
|
167
172
|
end
|
@@ -174,11 +179,15 @@ module RightScale
|
|
174
179
|
Log.init(@identity, @options[:log_path], :print => true)
|
175
180
|
Log.level = @options[:log_level] if @options[:log_level]
|
176
181
|
RightSupport::Log::Mixin.default_logger = Log
|
182
|
+
@history.update("start")
|
177
183
|
now = Time.now
|
178
184
|
Log.info("[start] Agent #{@identity} starting; time: #{now.utc}; utc_offset: #{now.utc_offset}")
|
179
185
|
Log.debug("Start options:")
|
180
|
-
log_opts = @options.inject([])
|
186
|
+
log_opts = @options.inject([]) do |t, (k, v)|
|
187
|
+
t << "- #{k}: #{k.to_s =~ /pass/ ? '****' : (v.respond_to?(:each) ? v.inspect : v)}"
|
188
|
+
end
|
181
189
|
log_opts.each { |l| Log.debug(l) }
|
190
|
+
terminate_callback = @options[:abnormal_terminate_callback]
|
182
191
|
|
183
192
|
begin
|
184
193
|
# Capture process id in file after optional daemonize
|
@@ -200,11 +209,12 @@ module RightScale
|
|
200
209
|
EM.add_timer(1) do
|
201
210
|
begin
|
202
211
|
@registry = ActorRegistry.new
|
203
|
-
@dispatcher =
|
204
|
-
@sender =
|
212
|
+
@dispatcher = create_dispatcher
|
213
|
+
@sender = create_sender
|
205
214
|
load_actors
|
206
215
|
setup_traps
|
207
216
|
setup_queues
|
217
|
+
@history.update("run")
|
208
218
|
start_console if @options[:console] && !@options[:daemonize]
|
209
219
|
|
210
220
|
# Need to keep reconnect interval at least :connect_timeout in size,
|
@@ -216,29 +226,29 @@ module RightScale
|
|
216
226
|
@check_status_brokers = @broker.all
|
217
227
|
EM.next_tick { @options[:ready_callback].call } if @options[:ready_callback]
|
218
228
|
@check_status_timer = EM::PeriodicTimer.new(interval) { check_status }
|
229
|
+
rescue SystemExit
|
230
|
+
raise
|
219
231
|
rescue Exception => e
|
220
|
-
|
221
|
-
EM.stop
|
232
|
+
terminate("failed startup after connecting to a broker", e, &terminate_callback)
|
222
233
|
end
|
223
234
|
end
|
224
235
|
elsif status == :failed
|
225
|
-
|
226
|
-
EM.stop
|
236
|
+
terminate("failed to connect to any brokers during startup", &terminate_callback)
|
227
237
|
elsif status == :timeout
|
228
|
-
|
229
|
-
|
238
|
+
terminate("failed to connect to any brokers after #{@options[:connect_timeout]} seconds during startup",
|
239
|
+
&terminate_callback)
|
230
240
|
else
|
231
|
-
|
232
|
-
|
241
|
+
terminate("broker connect attempt failed unexpectedly with status #{status} during startup",
|
242
|
+
&terminate_callback)
|
233
243
|
end
|
234
244
|
end
|
235
|
-
rescue SystemExit
|
236
|
-
raise
|
245
|
+
rescue SystemExit
|
246
|
+
raise
|
237
247
|
rescue PidFile::AlreadyRunning
|
248
|
+
EM.stop if EM.reactor_running?
|
238
249
|
raise
|
239
250
|
rescue Exception => e
|
240
|
-
|
241
|
-
raise e
|
251
|
+
terminate("failed startup", e, &terminate_callback)
|
242
252
|
end
|
243
253
|
true
|
244
254
|
end
|
@@ -430,17 +440,21 @@ module RightScale
|
|
430
440
|
end
|
431
441
|
|
432
442
|
# Handle packet received
|
443
|
+
# Delegate packet acknowledgement to dispatcher/sender
|
444
|
+
# Ignore requests if in the process of terminating but continue to accept responses
|
433
445
|
#
|
434
446
|
# === Parameters
|
435
447
|
# packet(Request|Push|Result):: Packet received
|
448
|
+
# header(AMQP::Frame::Header|nil):: Request header containing ack control
|
436
449
|
#
|
437
450
|
# === Return
|
438
451
|
# true:: Always return true
|
439
|
-
def receive(packet)
|
452
|
+
def receive(packet, header = nil)
|
440
453
|
begin
|
441
454
|
case packet
|
442
|
-
when Push, Request then @dispatcher.dispatch(packet) unless @terminating
|
443
|
-
when Result then @sender.handle_response(packet)
|
455
|
+
when Push, Request then @dispatcher.dispatch(packet, header) unless @terminating
|
456
|
+
when Result then @sender.handle_response(packet, header)
|
457
|
+
else header.ack if header
|
444
458
|
end
|
445
459
|
@sender.message_received
|
446
460
|
rescue RightAMQP::HABrokerClient::NoConnectedBrokers => e
|
@@ -465,20 +479,29 @@ module RightScale
|
|
465
479
|
|
466
480
|
# Gracefully terminate execution by allowing unfinished tasks to complete
|
467
481
|
# Immediately terminate if called a second time
|
482
|
+
# Report reason for termination if it is abnormal
|
483
|
+
#
|
484
|
+
# === Parameters
|
485
|
+
# reason(String):: Reason for abnormal termination, if any
|
486
|
+
# exception(Exception|String):: Exception or other parenthetical error information, if any
|
468
487
|
#
|
469
488
|
# === Block
|
470
489
|
# Optional block to be executed after termination is complete
|
471
490
|
#
|
472
491
|
# === Return
|
473
492
|
# true:: Always return true
|
474
|
-
def terminate(&block)
|
493
|
+
def terminate(reason = nil, exception = nil, &block)
|
494
|
+
block ||= DEFAULT_TERMINATE_BLOCK
|
475
495
|
begin
|
476
|
-
|
477
|
-
|
496
|
+
@history.update("stop")
|
497
|
+
Log.error("[stop] Terminating because #{reason}", exception, :trace) if reason
|
498
|
+
if @terminating || @broker.nil?
|
499
|
+
@terminating = true
|
478
500
|
@termination_timer.cancel if @termination_timer
|
479
501
|
@termination_timer = nil
|
480
|
-
|
481
|
-
|
502
|
+
Log.info("[stop] Terminating immediately")
|
503
|
+
block.call
|
504
|
+
@history.update("graceful exit") if @broker.nil?
|
482
505
|
else
|
483
506
|
@terminating = true
|
484
507
|
@check_status_timer.cancel if @check_status_timer
|
@@ -495,7 +518,7 @@ module RightScale
|
|
495
518
|
request_count, request_age = @sender.terminate
|
496
519
|
Log.info("[stop] The following #{request_count} requests initiated as recently as #{request_age} " +
|
497
520
|
"seconds ago are being dropped:\n " + @sender.dump_requests.join("\n ")) if request_age
|
498
|
-
@broker.close { block.call
|
521
|
+
@broker.close { block.call }
|
499
522
|
end
|
500
523
|
|
501
524
|
wait_time = [timeout - (request_age || timeout), timeout - (dispatch_age || timeout), 0].max
|
@@ -513,19 +536,21 @@ module RightScale
|
|
513
536
|
finish.call
|
514
537
|
rescue Exception => e
|
515
538
|
Log.error("Failed while finishing termination", e, :trace)
|
516
|
-
|
539
|
+
begin block.call; rescue Exception; end
|
517
540
|
end
|
518
541
|
end
|
519
542
|
end
|
520
543
|
else
|
521
|
-
block.call
|
522
|
-
EM.stop if EM.reactor_running?
|
544
|
+
block.call
|
523
545
|
end
|
546
|
+
@history.update("graceful exit")
|
524
547
|
end
|
525
548
|
end
|
549
|
+
rescue SystemExit
|
550
|
+
raise
|
526
551
|
rescue Exception => e
|
527
552
|
Log.error("Failed to terminate gracefully", e, :trace)
|
528
|
-
|
553
|
+
begin block.call; rescue Exception; end
|
529
554
|
end
|
530
555
|
true
|
531
556
|
end
|
@@ -553,7 +578,7 @@ module RightScale
|
|
553
578
|
"send stats" => @sender.stats(reset),
|
554
579
|
"last reset time" => @last_stat_reset_time.to_i,
|
555
580
|
"stat time" => now.to_i,
|
556
|
-
"service uptime" =>
|
581
|
+
"service uptime" => @history.analyze_service,
|
557
582
|
"machine uptime" => Platform.shell.uptime
|
558
583
|
}
|
559
584
|
stats["revision"] = @revision if @revision
|
@@ -654,6 +679,23 @@ module RightScale
|
|
654
679
|
false
|
655
680
|
end
|
656
681
|
|
682
|
+
# Create dispatcher for handling incoming requests
|
683
|
+
#
|
684
|
+
# === Return
|
685
|
+
# (Dispatcher):: New dispatcher
|
686
|
+
def create_dispatcher
|
687
|
+
cache = DispatchedCache.new(@identity) if @options[:dup_check]
|
688
|
+
Dispatcher.new(self, cache)
|
689
|
+
end
|
690
|
+
|
691
|
+
# Create manager for outgoing requests
|
692
|
+
#
|
693
|
+
# === Return
|
694
|
+
# (Sender):: New sender
|
695
|
+
def create_sender
|
696
|
+
Sender.new(self)
|
697
|
+
end
|
698
|
+
|
657
699
|
# Load the ruby code for the actors
|
658
700
|
#
|
659
701
|
# === Return
|
@@ -724,7 +766,7 @@ module RightScale
|
|
724
766
|
queue = {:name => @identity, :options => {:durable => true, :no_declare => @options[:secure]}}
|
725
767
|
filter = [:from, :tags, :tries, :persistent]
|
726
768
|
options = {:ack => true, Request => filter, Push => filter, Result => [:from], :brokers => ids}
|
727
|
-
ids = @broker.subscribe(queue, nil, options) { |_, packet| receive(packet) }
|
769
|
+
ids = @broker.subscribe(queue, nil, options) { |_, packet, header| receive(packet, header) }
|
728
770
|
end
|
729
771
|
|
730
772
|
# Setup signal traps
|
@@ -737,7 +779,7 @@ module RightScale
|
|
737
779
|
EM.next_tick do
|
738
780
|
begin
|
739
781
|
terminate do
|
740
|
-
|
782
|
+
DEFAULT_TERMINATE_BLOCK.call
|
741
783
|
old.call if old.is_a? Proc
|
742
784
|
end
|
743
785
|
rescue Exception => e
|
@@ -796,10 +838,28 @@ module RightScale
|
|
796
838
|
true
|
797
839
|
end
|
798
840
|
|
841
|
+
begin
|
842
|
+
check_other(@check_status_count)
|
843
|
+
rescue Exception => e
|
844
|
+
Log.error("Failed to perform other check status check", e)
|
845
|
+
@exceptions.track("check status", e)
|
846
|
+
end
|
847
|
+
|
799
848
|
@check_status_count += 1
|
800
849
|
true
|
801
850
|
end
|
802
851
|
|
852
|
+
# Allow derived classes to perform any other useful periodic checks
|
853
|
+
#
|
854
|
+
# === Parameters
|
855
|
+
# check_status_count(Integer):: Counter that is incremented for each status check
|
856
|
+
#
|
857
|
+
# === Return
|
858
|
+
# true:: Always return true
|
859
|
+
def check_other(check_status_count)
|
860
|
+
true
|
861
|
+
end
|
862
|
+
|
803
863
|
# Store unique tags
|
804
864
|
#
|
805
865
|
# === Parameters
|
@@ -20,13 +20,12 @@
|
|
20
20
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
21
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
22
|
|
23
|
-
require 'singleton'
|
24
23
|
|
25
24
|
module RightScale
|
26
25
|
|
27
26
|
# Agent tags management
|
28
27
|
class AgentTagManager
|
29
|
-
include
|
28
|
+
include RightSupport::Ruby::EasySingleton
|
30
29
|
|
31
30
|
# (Agent) Agent being managed
|
32
31
|
attr_accessor :agent
|