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.
data/bin/brown CHANGED
@@ -2,98 +2,35 @@
2
2
 
3
3
  # Run an agent. Any agent.
4
4
 
5
- require 'envied'
6
- require 'logger'
5
+ require "brown"
7
6
 
8
- envied_config = (ENVied.config || ENVied::Configuration.new).tap do |cfg|
9
- cfg.enable_defaults!
10
- cfg.variable :AMQP_URL, :string, :default => "amqp://localhost"
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
- ENVied.require(:default, :config => envied_config)
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
- Brown::Agent.logger = Logger.new($stderr)
23
- Brown::Agent.logger.level = Logger.const_get(ENVied.BROWN_LOG_LEVEL.upcase.to_sym)
24
- Brown::Agent.logger.formatter = proc { |s,dt,n,msg| "#{$$}-#{Thread.current.object_id} [#{s[0]}] #{msg}\n" }
25
-
26
- agents = []
27
-
28
- def stop_agents(agents)
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
- loop do
68
- if agents.empty?
69
- break
70
- end
71
-
72
- sleep 0.5
27
+ agent_class = agent_classes.first
73
28
 
74
- agents.each do |th|
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
- Brown::Agent.logger.info { "Re-starting #{klass} agent" }
86
-
87
- klass = th[:agent_class]
88
- agents.delete(th)
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
- s.add_runtime_dependency "envied", "~> 0.8"
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', '~> 10.4', '>= 10.4.2'
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 'logger'
2
- require 'securerandom'
1
+ require "service_skeleton"
3
2
 
4
3
  # A Brown Agent. The whole reason we're here.
5
4
  #
6
- # An agent is the fundamental unit of work in the Brown universe. Pretty
7
- # much everything you do to an agent is done on its class; the individual
8
- # instances of the agent are run by the agent, internally, when reacting to
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
- stimulate(worker_method) do |worker|
285
- __send__(queue_memo) do |queue|
286
- queue.subscribe(manual_ack: true, block: true) do |di, prop, payload|
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
- @thread_group ||= ThreadGroup.new
309
- @runner_thread = Thread.current
13
+ @op_mutex = Mutex.new
14
+ @op_cv = ConditionVariable.new
15
+ end
310
16
 
311
- (stimuli || {}).each do |s|
312
- @thread_group.add(
313
- Thread.new(s) do |s|
314
- begin
315
- s.run
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
- @thread_group.list.each do |th|
324
- begin
325
- th.join
326
- rescue Brown::StopSignal
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
- # Stop the agent running.
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
- (@thread_group.list rescue []).each do |th|
352
- th.join
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
- def more_log_detail
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
- ch.queue(queue_name, durable: true).tap do |q|
403
- exchange_list.each do |exchange_name|
404
- if exchange_name != ""
405
- begin
406
- q.bind(exchange_name)
407
- rescue Bunny::NotFound => ex
408
- logger.error { "bind failed: #{ex.message}" }
409
- sleep 5
410
- return bind_queue(
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
- end
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"