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.
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"