right_agent 0.10.13 → 0.13.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|