brown 2.2.2 → 2.2.2.25.g85ddf08
Sign up to get free protection for your applications and to get access to all the features.
- 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"
|