brown 2.2.2 → 2.2.2.25.g85ddf08
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.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/Dockerfile +10 -0
- data/README.md +137 -99
- data/bin/brown +19 -82
- data/brown.gemspec +3 -3
- data/lib/brown.rb +2 -0
- data/lib/brown/agent.rb +41 -411
- data/lib/brown/agent/amqp.rb +20 -0
- data/lib/brown/agent/amqp/class_methods.rb +147 -0
- data/lib/brown/agent/amqp/initializer.rb +144 -0
- data/lib/brown/agent/amqp_message.rb +26 -4
- data/lib/brown/agent/amqp_publisher.rb +68 -27
- data/lib/brown/agent/class_methods.rb +166 -0
- data/lib/brown/agent/stimulus.rb +84 -77
- data/lib/brown/agent/stimulus/metrics.rb +17 -0
- data/lib/brown/rspec.rb +16 -5
- data/lib/brown/test.rb +16 -28
- metadata +18 -20
data/bin/brown
CHANGED
@@ -2,98 +2,35 @@
|
|
2
2
|
|
3
3
|
# Run an agent. Any agent.
|
4
4
|
|
5
|
-
require
|
6
|
-
require 'logger'
|
5
|
+
require "brown"
|
7
6
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
cfg.variable :BROWN_LOG_LEVEL, :string, :default => "info"
|
7
|
+
unless ARGV.length == 1
|
8
|
+
$stderr.puts "Must specify a single agent file to load."
|
9
|
+
exit 1
|
12
10
|
end
|
13
11
|
|
14
|
-
|
15
|
-
|
16
|
-
ARGV.each { |f| require f }
|
12
|
+
require ARGV.first
|
17
13
|
|
18
14
|
agent_classes = ObjectSpace.each_object(Class).select do |k|
|
19
15
|
k != Brown::Agent and k.ancestors.include?(Brown::Agent)
|
20
16
|
end
|
21
17
|
|
22
|
-
|
23
|
-
Brown::Agent.
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
agents.each do |th|
|
30
|
-
Brown::Agent.logger.debug { "Stopping agent #{th[:agent_class].inspect} (thread #{th.inspect})" }
|
31
|
-
th[:agent_class] && th[:agent_class].stop
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
Signal.trap("INT") do
|
36
|
-
$stderr.puts "Received SIGINT; stopping agents and exiting"
|
37
|
-
stop_agents(agents)
|
38
|
-
end
|
39
|
-
Signal.trap("TERM") do
|
40
|
-
$stderr.puts "Received SIGTERM; stopping agents and exiting"
|
41
|
-
stop_agents(agents)
|
42
|
-
end
|
43
|
-
Signal.trap("HUP", "IGNORE")
|
44
|
-
Signal.trap("USR1") do
|
45
|
-
Brown::Agent.more_log_detail
|
46
|
-
$stderr.puts "Log level now #{Logger::SEV_LABEL[Brown::Agent.logger.level]}"
|
47
|
-
end
|
48
|
-
Signal.trap("USR2") do
|
49
|
-
Brown::Agent.less_log_detail
|
50
|
-
$stderr.puts "Log level now #{Logger::SEV_LABEL[Brown::Agent.logger.level]}"
|
51
|
-
end
|
52
|
-
|
53
|
-
Brown::Agent.logger.info { "Brown starting up..." }
|
54
|
-
|
55
|
-
agent_classes.each do |klass|
|
56
|
-
th = Thread.new(klass) do |klass|
|
57
|
-
Thread.current[:agent_class] = klass
|
58
|
-
klass.run
|
59
|
-
end
|
60
|
-
|
61
|
-
agents << th
|
62
|
-
|
63
|
-
Brown::Agent.logger.info { "Started agent #{klass}" }
|
64
|
-
Brown::Agent.logger.debug { "Agent thread is #{th.inspect}" }
|
18
|
+
if agent_classes.length > 1
|
19
|
+
$stderr.puts "Multiple subclasses of Brown::Agent found. I don't know what to run."
|
20
|
+
exit 1
|
21
|
+
elsif
|
22
|
+
agent_classes.length == 0
|
23
|
+
$stderr.puts "No subclass of Brown::Agent found. I don't have anything to run."
|
24
|
+
exit 1
|
65
25
|
end
|
66
26
|
|
67
|
-
|
68
|
-
if agents.empty?
|
69
|
-
break
|
70
|
-
end
|
71
|
-
|
72
|
-
sleep 0.5
|
27
|
+
agent_class = agent_classes.first
|
73
28
|
|
74
|
-
|
75
|
-
Brown::Agent.logger.debug { "Examining agent thread #{th.inspect}" }
|
76
|
-
begin
|
77
|
-
if th.join(0.5)
|
78
|
-
Brown::Agent.logger.info { "Agent #{th[:agent_class]} exited cleanly; not restarting" }
|
79
|
-
agents.delete(th)
|
80
|
-
end
|
81
|
-
rescue Exception => ex
|
82
|
-
Brown::Agent.logger.fatal { "Agent #{th[:agent_class]} crashed: #{ex.message} (#{ex.class})" }
|
83
|
-
Brown::Agent.logger.info { ex.backtrace.map { |l| " #{l}" }.join("\n") }
|
29
|
+
$0 = "brown: #{agent_class.to_s}"
|
84
30
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
agents << Thread.new(klass) do |klass|
|
91
|
-
Thread.current[:agent_class] = klass
|
92
|
-
klass.run
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
end
|
31
|
+
begin
|
32
|
+
agent_class.new(ENV).start
|
33
|
+
rescue ServiceSkeleton::Error::InvalidEnvironmentError => ex
|
34
|
+
$stderr.puts "Configuration error: #{ex.message}"
|
35
|
+
exit 1
|
97
36
|
end
|
98
|
-
|
99
|
-
Brown::Agent.logger.info { "Brown exiting" }
|
data/brown.gemspec
CHANGED
@@ -24,14 +24,14 @@ Gem::Specification.new do |s|
|
|
24
24
|
s.required_ruby_version = ">= 2.1.0"
|
25
25
|
|
26
26
|
s.add_runtime_dependency "bunny", "~> 1.7"
|
27
|
-
|
27
|
+
s.add_runtime_dependency "service_skeleton", ">= 0.0.0.41.g9507cda"
|
28
28
|
|
29
29
|
s.add_development_dependency 'bundler'
|
30
|
+
s.add_development_dependency 'git-version-bump'
|
30
31
|
s.add_development_dependency 'github-release'
|
31
|
-
s.add_development_dependency 'guard-spork'
|
32
32
|
s.add_development_dependency 'guard-rspec'
|
33
33
|
s.add_development_dependency 'pry-byebug'
|
34
|
-
s.add_development_dependency 'rake', '
|
34
|
+
s.add_development_dependency 'rake', '>= 10.4.2'
|
35
35
|
# Needed for guard
|
36
36
|
s.add_development_dependency 'rb-inotify', '~> 0.9'
|
37
37
|
s.add_development_dependency 'redcarpet'
|
data/lib/brown.rb
CHANGED
@@ -13,7 +13,9 @@ module Brown
|
|
13
13
|
end
|
14
14
|
|
15
15
|
require_relative 'brown/agent'
|
16
|
+
require_relative 'brown/agent/amqp'
|
16
17
|
require_relative 'brown/agent/amqp_message'
|
17
18
|
require_relative 'brown/agent/amqp_publisher'
|
18
19
|
require_relative 'brown/agent/memo'
|
19
20
|
require_relative 'brown/agent/stimulus'
|
21
|
+
require_relative 'brown/agent/stimulus/metrics'
|
data/lib/brown/agent.rb
CHANGED
@@ -1,430 +1,60 @@
|
|
1
|
-
require
|
2
|
-
require 'securerandom'
|
1
|
+
require "service_skeleton"
|
3
2
|
|
4
3
|
# A Brown Agent. The whole reason we're here.
|
5
4
|
#
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
# stimuli.
|
10
|
-
#
|
11
|
-
class Brown::Agent
|
12
|
-
class << self
|
13
|
-
# Define a generic stimulus for this agent.
|
14
|
-
#
|
15
|
-
# This is a fairly low-level method, designed to provide a means for
|
16
|
-
# defining stimuli for which there isn't a higher-level, more-specific
|
17
|
-
# stimulus definition approach.
|
18
|
-
#
|
19
|
-
# When the agent is started (see {.run}), the block you provide will
|
20
|
-
# be executed in a dedicated thread. Every time the block finishes,
|
21
|
-
# it will be run again. Your block should do whatever it needs to do
|
22
|
-
# to detect when a stimuli is available (preferably by blocking
|
23
|
-
# somehow, rather than polling, because polling sucks). When your
|
24
|
-
# code detects that a stimulus has been received, it should run
|
25
|
-
# `worker.call`, passing in any arguments that are required to process
|
26
|
-
# the stimulus. That will then create a new instance of the agent
|
27
|
-
# class, and call the specified `method_name` on that instance,
|
28
|
-
# passing in the arguments that were passed to `worker.call`.
|
29
|
-
#
|
30
|
-
# @see .every
|
31
|
-
#
|
32
|
-
# @param method_name [Symbol] the name of the method to call when the
|
33
|
-
# stimulus is triggered.
|
34
|
-
#
|
35
|
-
# @yieldparam worker [Proc] call this when you want a stimulus
|
36
|
-
# processed, passing in anything that the stimulus processing method
|
37
|
-
# (as specified by `method_name`) needs to do its job.
|
38
|
-
#
|
39
|
-
def stimulate(method_name, &blk)
|
40
|
-
@stimuli ||= []
|
41
|
-
|
42
|
-
@stimuli << Brown::Agent::Stimulus.new(
|
43
|
-
method_name: method_name,
|
44
|
-
stimuli_proc: blk,
|
45
|
-
agent_class: self,
|
46
|
-
logger: logger
|
47
|
-
)
|
48
|
-
end
|
49
|
-
|
50
|
-
# Define a "memo" for this agent.
|
51
|
-
#
|
52
|
-
# A "memo" is an object which is common across all instances of a
|
53
|
-
# particular agent, and which is (usually) local to that agent. The
|
54
|
-
# intended purpose is for anything that is needed for processing
|
55
|
-
# stimuli, but which you don't want to recreate for every stimuli.
|
56
|
-
# Examples of this sort of thing include connection pools (database,
|
57
|
-
# HTTP connections, etc), config files, and caches. Basically,
|
58
|
-
# anything that has a non-trivial setup time, or which you *want* to
|
59
|
-
# share across all stimuli processing, should go in a memo.
|
60
|
-
#
|
61
|
-
# Because we do everything in threads, and because dealing with
|
62
|
-
# locking by hand is a nightmare, access to memos is, by default,
|
63
|
-
# protected by a mutex. This means that any time you want to do
|
64
|
-
# something with a memo, you call its name and pass a block to do
|
65
|
-
# whatever you want to do with the value, like this:
|
66
|
-
#
|
67
|
-
# config { |cfg| puts "foo is #{cfg[:foo]}" }
|
68
|
-
#
|
69
|
-
# Now, you can, if you want, "leak" this object out of the mutex, with
|
70
|
-
# various sorts of assignment. DO NOT SUCCUMB TO THIS TEMPTATION. If
|
71
|
-
# you do this, you will risk all sorts of concurrency bugs, where two
|
72
|
-
# threads try to read and/or manipulate the object at the same time
|
73
|
-
# and all hell breaks loose.
|
74
|
-
#
|
75
|
-
# If, *and only if*, you are **100% confident** that the
|
76
|
-
# object you want to work with is, in fact, entirely thread-safe (the
|
77
|
-
# documentation should mention this), then you can mark a memo object
|
78
|
-
# as "safe", either by passing `true` to {.memo}, or using the handy-dandy
|
79
|
-
# {.safe_memo} method. In this case, you can just reference the memo
|
80
|
-
# name wherever you like:
|
81
|
-
#
|
82
|
-
# safe_memo :db do
|
83
|
-
# Sequel.connect("postgres:///app_database")
|
84
|
-
# end
|
85
|
-
#
|
86
|
-
# #...
|
87
|
-
# db[:foo].where { :baz > 42 }
|
88
|
-
#
|
89
|
-
# Note that there is intentionally no way to reassign a memo object.
|
90
|
-
# This doesn't mean that memo objects are "read-only", however. The
|
91
|
-
# state of the object can be mutated by calling any method on the
|
92
|
-
# object that modifies it. If you want more read-only(ish) memos, you
|
93
|
-
# probably want to call `#freeze` on your object when you create it
|
94
|
-
# (although all the usual caveats about `#freeze` still apply).
|
95
|
-
#
|
96
|
-
# @see .safe_memo
|
97
|
-
#
|
98
|
-
# @param name [Symbol] the name of the memo, and hence the name of the
|
99
|
-
# method that should be called to retrieve the memo's value.
|
100
|
-
#
|
101
|
-
# @param safe [Boolean] whether or not the object will be "safe" for
|
102
|
-
# concurrent access by multiple threads. Do *not* enable this
|
103
|
-
# unless you are completely sure.
|
104
|
-
#
|
105
|
-
# @return void
|
106
|
-
#
|
107
|
-
def memo(name, safe=false, &generator)
|
108
|
-
name = name.to_sym
|
109
|
-
@memos ||= {}
|
110
|
-
@memos[name] = Brown::Agent::Memo.new(generator, safe)
|
111
|
-
|
112
|
-
define_method(name) do |test=nil, &blk|
|
113
|
-
self.class.__send__(name, test, &blk)
|
114
|
-
end
|
115
|
-
|
116
|
-
self.singleton_class.__send__(:define_method, name) do |test=nil, &blk|
|
117
|
-
memos[name].value(test, &blk)
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
|
-
# A variant of {.memo} which is intended for objects which are
|
122
|
-
# inherently thread-safe within themselves.
|
123
|
-
#
|
124
|
-
# @see .memo
|
125
|
-
#
|
126
|
-
def safe_memo(name, &generator)
|
127
|
-
memo(name, true, &generator)
|
128
|
-
end
|
129
|
-
|
130
|
-
# Execute a block of code periodically.
|
131
|
-
#
|
132
|
-
# This pretty much does what it says on the tin. Every
|
133
|
-
# `n` seconds (where `n` can be a float) the given block
|
134
|
-
# of code is executed.
|
135
|
-
#
|
136
|
-
# Don't expect too much precision in the interval; we just sleep
|
137
|
-
# between triggers, so there might be a bit of an extra delay between
|
138
|
-
# invocations.
|
139
|
-
#
|
140
|
-
# @param n [Numeric] The amount of time which should elapse between
|
141
|
-
# invocations of the block.
|
142
|
-
#
|
143
|
-
# @yield every `n` seconds.
|
144
|
-
#
|
145
|
-
def every(n, &blk)
|
146
|
-
method_name = ("every_#{n}__" + SecureRandom.uuid).to_sym
|
147
|
-
define_method(method_name, &blk)
|
148
|
-
|
149
|
-
stimulate(method_name) { |worker| sleep n; worker.call }
|
150
|
-
end
|
151
|
-
|
152
|
-
# Declare an AMQP publisher, and create an AMQP exchange to publish to.
|
153
|
-
#
|
154
|
-
# On the assumption that you already know [how exchanges
|
155
|
-
# work](http://www.rabbitmq.com/tutorials/amqp-concepts.html), I'll
|
156
|
-
# just dive right in.
|
157
|
-
#
|
158
|
-
# This method creates an accessor method on your agent named after the
|
159
|
-
# symbol you pass in as `name`, which returns an instance of
|
160
|
-
# `Brown::Agent::AMQPPublisher`. This object, in turn, defines an
|
161
|
-
# AMQP exchange when it is created, and has a
|
162
|
-
# `publish` method on it (see {Brown::Agent::AMQPPublisher#publish}) which
|
163
|
-
# sends arbitrary messages to the exchange.
|
164
|
-
#
|
165
|
-
# @param name [Symbol] the name of the accessor method to call when
|
166
|
-
# you want to reference this publisher in your agent code.
|
167
|
-
#
|
168
|
-
# @param publisher_opts [Hash] options which are passed to
|
169
|
-
# {Brown::Agent::AMQPPublisher#initialize}.
|
170
|
-
#
|
171
|
-
# This method is a thin shim around {Brown::Agent::AMQPPublisher#initialize};
|
172
|
-
# you should read that method's documentation for details of what
|
173
|
-
# constitutes valid `publisher_opts`, and also what exceptions can be
|
174
|
-
# raised.
|
175
|
-
#
|
176
|
-
# @see Brown::Agent::AMQPPublisher#initialize
|
177
|
-
# @see Brown::Agent::AMQPPublisher#publish
|
178
|
-
#
|
179
|
-
def amqp_publisher(name, publisher_opts = {})
|
180
|
-
opts = { :exchange_name => name }.merge(publisher_opts)
|
181
|
-
|
182
|
-
safe_memo(name) { Brown::Agent::AMQPPublisher.new(opts) }
|
183
|
-
end
|
184
|
-
|
185
|
-
# Listen for messages from an AMQP broker.
|
186
|
-
#
|
187
|
-
# We setup a queue, bound to the exchange specified by the
|
188
|
-
# `exchange_name` argument, and then proceed to hoover up all the
|
189
|
-
# messages we can.
|
190
|
-
#
|
191
|
-
# The name of the queue that is created by default is derived from the
|
192
|
-
# agent class name and the exchange name being bound to. This allows
|
193
|
-
# for multiple instances of the same agent, running in separate
|
194
|
-
# processes or machines, to share the same queue of messages to
|
195
|
-
# process, for throughput or redundancy reasons.
|
196
|
-
#
|
197
|
-
# @param exchange_name [#to_s, Array<#to_s>] the name of the exchange
|
198
|
-
# to bind to. You can also specify an array of exchange names, to
|
199
|
-
# have all of them put their messages into the one queue. This can
|
200
|
-
# be dangerous, because you need to make sure that your message
|
201
|
-
# handler can process the different types of messages that might be
|
202
|
-
# sent to the different exchangs.
|
203
|
-
#
|
204
|
-
# @param queue_name [#to_s] the name of the queue to create, if you
|
205
|
-
# don't want to use the class-derived default for some reason.
|
206
|
-
#
|
207
|
-
# @param amqp_url [#to_s] the URL of the AMQP broker to connect to.
|
208
|
-
#
|
209
|
-
# @param concurrency [Integer] how many messages to process in parallel.
|
210
|
-
# The default, `1`, means that a message will need to be acknowledged
|
211
|
-
# (by calling `message.ack`) in your worker `blk` before the broker
|
212
|
-
# will consider sending another.
|
213
|
-
#
|
214
|
-
# If your agent is capable of processing more than one message in
|
215
|
-
# parallel (because the agent spends a lot of its time waiting for
|
216
|
-
# databases or HTTP requests, for example, or perhaps you're running
|
217
|
-
# your agents in a Ruby VM which has no GIL) you should increase
|
218
|
-
# this value to improve performance. Alternately, if you want/need
|
219
|
-
# to batch processing (say, you insert 100 records into a database
|
220
|
-
# in a single query) you'll need to increase this to get multiple
|
221
|
-
# records at once.
|
222
|
-
#
|
223
|
-
# Setting this to `0` is only for the adventurous. It tells the
|
224
|
-
# broker to send your agent messages as fast as it can. You still
|
225
|
-
# need to acknowledge the messages as you finish processing them
|
226
|
-
# (otherwise the broker will not consider them "delivered") but you
|
227
|
-
# will always be sent more messages if there are more to send, even
|
228
|
-
# if you never acknowledge any of them. This *can* get you into an
|
229
|
-
# awful lot of trouble if you're not careful, so don't do it just
|
230
|
-
# because you can.
|
231
|
-
#
|
232
|
-
# @param blk [Proc] is called every time a message is received from
|
233
|
-
# the queue, and an instance of {Brown::Agent::AMQPMessage} will
|
234
|
-
# be passed as the sole argument.
|
235
|
-
#
|
236
|
-
# @yieldparam message [Brown::Agent::AMQPMessage] is passed to `blk`
|
237
|
-
# each time a message is received from the queue.
|
238
|
-
#
|
239
|
-
def amqp_listener(exchange_name = "",
|
240
|
-
queue_name: nil,
|
241
|
-
amqp_url: "amqp://localhost",
|
242
|
-
concurrency: 1,
|
243
|
-
&blk
|
244
|
-
)
|
245
|
-
listener_uuid = SecureRandom.uuid
|
246
|
-
worker_method = "amqp_listener_worker_#{listener_uuid}".to_sym
|
247
|
-
queue_memo = "amqp_listener_queue_#{listener_uuid}".to_sym
|
248
|
-
|
249
|
-
exchange_list = Array === exchange_name ?
|
250
|
-
exchange_name :
|
251
|
-
[exchange_name]
|
252
|
-
|
253
|
-
munged_exchange_list = exchange_list.map { |n| n.to_s == "" ? "" : "-#{n.to_s}" }.join
|
254
|
-
queue_name ||= self.name.to_s + munged_exchange_list
|
255
|
-
|
256
|
-
memo(queue_memo) do
|
257
|
-
begin
|
258
|
-
amqp = Bunny.new(amqp_url, logger: logger)
|
259
|
-
amqp.start
|
260
|
-
rescue Bunny::TCPConnectionFailed
|
261
|
-
logger.error { "Failed to connect to #{amqp_url}" }
|
262
|
-
sleep 5
|
263
|
-
retry
|
264
|
-
rescue Bunny::PossibleAuthenticationFailureError
|
265
|
-
logger.error { "Authentication failure for #{amqp_url}" }
|
266
|
-
sleep 5
|
267
|
-
retry
|
268
|
-
rescue StandardError => ex
|
269
|
-
logger.error { "Unknown error while trying to connect to #{amqp_url}: #{ex.message} (#{ex.class})" }
|
270
|
-
sleep 5
|
271
|
-
retry
|
272
|
-
end
|
273
|
-
|
274
|
-
bind_queue(
|
275
|
-
amqp_session: amqp,
|
276
|
-
queue_name: queue_name,
|
277
|
-
exchange_list: exchange_list,
|
278
|
-
concurrency: concurrency
|
279
|
-
)
|
280
|
-
end
|
281
|
-
|
282
|
-
define_method(worker_method, &blk)
|
5
|
+
class Brown::Agent < ServiceSkeleton
|
6
|
+
def initialize(*_)
|
7
|
+
super
|
283
8
|
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
worker.call Brown::Agent::AMQPMessage.new(di, prop, payload)
|
288
|
-
end
|
289
|
-
end
|
290
|
-
end
|
291
|
-
end
|
292
|
-
|
293
|
-
# Start the agent running.
|
294
|
-
#
|
295
|
-
# This fires off the stimuli listeners and then waits. If you want to
|
296
|
-
# do anything else while this runs, you'll want to fire this in a
|
297
|
-
# separate thread.
|
298
|
-
#
|
299
|
-
def run
|
300
|
-
begin
|
301
|
-
# Some memos (AMQPPublisher being the first) work best when
|
302
|
-
# initialized when the agent starts up. At some point in the
|
303
|
-
# future, we might implement selective initialisation, but for
|
304
|
-
# now we'll take the brutally-simple approach of getting
|
305
|
-
# everything.
|
306
|
-
(@memos || {}).keys.each { |k| send(k, &->(_){}) }
|
9
|
+
@memo_values = {}
|
10
|
+
@memo_mutexes = {}
|
11
|
+
@memo_mutexes_mutex = Mutex.new
|
307
12
|
|
308
|
-
|
309
|
-
|
13
|
+
@op_mutex = Mutex.new
|
14
|
+
@op_cv = ConditionVariable.new
|
15
|
+
end
|
310
16
|
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
rescue Brown::StopSignal
|
317
|
-
# OK then
|
318
|
-
end
|
319
|
-
end
|
320
|
-
)
|
17
|
+
def run
|
18
|
+
@op_mutex.synchronize do
|
19
|
+
@stimuli_workers = ((self.class.stimuli || []) + (@stimuli || [])).map do |s|
|
20
|
+
if s[:method_name]
|
21
|
+
s[:method] = self.method(s[:method_name])
|
321
22
|
end
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
# OK then
|
328
|
-
end
|
23
|
+
stimulus_metrics = Stimulus::Metrics.new(:"#{self.service_name}_#{s[:name]}", registry: self.metrics)
|
24
|
+
logger.debug(logloc) { "Starting stimulus #{s[:name]}" }
|
25
|
+
Brown::Agent::Stimulus.new(method: s[:method], stimuli_proc: s[:stimuli_proc], logger: logger, metrics: stimulus_metrics).tap do |stimulus|
|
26
|
+
stimulus.start!
|
27
|
+
logger.debug(logloc) { "Stimulus started" }
|
329
28
|
end
|
330
|
-
rescue Exception => ex
|
331
|
-
logger.error { "Agent #{self} caught unhandled exception: #{ex.message} (#{ex.class})" }
|
332
|
-
logger.info { ex.backtrace.map { |l| " #{l}" }.join("\n") }
|
333
|
-
stop
|
334
29
|
end
|
335
|
-
end
|
336
30
|
|
337
|
-
|
338
|
-
#
|
339
|
-
# This can either be called in a separate thread, or a signal handler, like
|
340
|
-
# so:
|
341
|
-
#
|
342
|
-
# Signal.trap("INT") { SomeAgent.stop }
|
343
|
-
#
|
344
|
-
# agent.run
|
345
|
-
#
|
346
|
-
def stop
|
347
|
-
(@thread_group.list rescue []).each do |th|
|
348
|
-
th.raise Brown::FinishSignal.new("agent finish")
|
349
|
-
end
|
31
|
+
@running = true
|
350
32
|
|
351
|
-
|
352
|
-
|
33
|
+
while @running
|
34
|
+
logger.debug(logloc) { "Agent runner taking a snooze" }
|
35
|
+
@op_cv.wait(@op_mutex)
|
353
36
|
end
|
354
|
-
|
355
|
-
@runner_thread.raise(Brown::StopSignal.new("Clean stop"))
|
356
|
-
end
|
357
|
-
|
358
|
-
# Set the logger that this agent will use to report problems.
|
359
|
-
#
|
360
|
-
# @param l [Logger]
|
361
|
-
#
|
362
|
-
def logger=(l)
|
363
|
-
@logger = l
|
364
|
-
end
|
365
|
-
|
366
|
-
# Get or set the logger for this agent.
|
367
|
-
#
|
368
|
-
# @param l [Logger]
|
369
|
-
#
|
370
|
-
# @return [Logger]
|
371
|
-
#
|
372
|
-
def logger(l=nil)
|
373
|
-
(@logger = (l || @logger)) || (self == Brown::Agent ? Logger.new($stderr) : Brown::Agent.logger)
|
374
37
|
end
|
38
|
+
end
|
375
39
|
|
376
|
-
|
377
|
-
logger.level -= 1
|
378
|
-
end
|
379
|
-
|
380
|
-
def less_log_detail
|
381
|
-
logger.level += 1
|
382
|
-
end
|
383
|
-
|
384
|
-
private
|
385
|
-
|
386
|
-
# The available stimuli.
|
387
|
-
#
|
388
|
-
# @return [Array<Brown::Agent::Stimulus>]
|
389
|
-
#
|
390
|
-
attr_reader :stimuli
|
391
|
-
|
392
|
-
# The available memos.
|
393
|
-
#
|
394
|
-
# @return [Hash<Symbol, Brown::Agent::Memo>]
|
395
|
-
#
|
396
|
-
attr_reader :memos
|
397
|
-
|
398
|
-
def bind_queue(amqp_session:, queue_name:, exchange_list:, concurrency:)
|
399
|
-
ch = amqp_session.create_channel
|
400
|
-
ch.prefetch(concurrency)
|
40
|
+
private
|
401
41
|
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
amqp_session: amqp_session,
|
412
|
-
queue_name: queue_name,
|
413
|
-
exchange_list: exchange_list,
|
414
|
-
concurrency: concurrency
|
415
|
-
)
|
416
|
-
end
|
417
|
-
end
|
418
|
-
end
|
42
|
+
def shutdown
|
43
|
+
logger.debug(logloc) { "Shutdown requested" }
|
44
|
+
@op_mutex.synchronize do
|
45
|
+
logger.debug(logloc) { "Shutdown starting" }
|
46
|
+
return unless @running
|
47
|
+
logger.debug(logloc) { "Stopping #{@stimuli_workers.length} stimulus workers" }
|
48
|
+
until @stimuli_workers.empty? do
|
49
|
+
@stimuli_workers.pop.stop!
|
50
|
+
logger.debug(logloc) { "One down, #{@stimuli_workers.length} to go" }
|
419
51
|
end
|
52
|
+
@running = false
|
53
|
+
logger.debug(logloc) { "Signalling for pickup" }
|
54
|
+
@op_cv.signal
|
420
55
|
end
|
421
|
-
|
422
|
-
|
423
|
-
# The logger for this agent.
|
424
|
-
#
|
425
|
-
# @return [Logger]
|
426
|
-
#
|
427
|
-
def logger
|
428
|
-
self.class.logger
|
56
|
+
logger.debug(logloc) { "Shutdown complete" }
|
429
57
|
end
|
430
58
|
end
|
59
|
+
|
60
|
+
require_relative "./agent/class_methods"
|