brown 1.1.2 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,321 @@
1
+ Brown is a "framework for autonomous agents". That is, essentially, a
2
+ high-falutin' way of saying that you can write some code to do some stuff.
3
+
4
+ More precisely, Brown agents are (typically) small, standalone blocks of
5
+ code (encapsulated in a single class) which wait for some stimuli, and then
6
+ react to it. Often, that stimuli is receiving a message (via an AMQP broker
7
+ such as [RabbitMQ](http://rabbitmq.org/), however an agent can do anything
8
+ it pleases (query a database, watch a filesystem, receive HTTP requests,
9
+ whatever) to get stimuli to respond to.
10
+
11
+
12
+ # Installation
13
+
14
+ It's a gem:
15
+
16
+ gem install brown
17
+
18
+ There's also the wonders of [the Gemfile](http://bundler.io):
19
+
20
+ gem 'brown'
21
+
22
+ If you're the sturdy type that likes to run from git:
23
+
24
+ rake build; gem install pkg/brown-<whatever>.gem
25
+
26
+ Or, if you've eschewed the convenience of Rubygems entirely, then you
27
+ presumably know what to do already.
28
+
29
+
30
+ # Usage
31
+
32
+ To make something an agent, you simply create a subclass of `Brown::Agent`.
33
+ You can then use a simple DSL to define "stimuli", each of which (when
34
+ triggered) cause a new instance of the class to be instantiated and a method
35
+ (specified by the stimulus) to be invoked in a separate thread. You can do
36
+ arbitrary things to detect stimuli, however there are a number of
37
+ pre-defined stimuli you can use to do standard things, like run something
38
+ periodically, or process a message on an AMQP queue.
39
+
40
+ As a very simple example, say you wanted to print `foo` every five seconds.
41
+ (Yes, you *could* do this in a loop, but humour me, would you?) Using the
42
+ built-in `every` stimuli, you could do it like this:
43
+
44
+ class FooTicker < Brown::Agent
45
+ every 5 do
46
+ puts "foo"
47
+ end
48
+ end
49
+
50
+ FooTicker.run
51
+
52
+ To demonstrate that each trip of the timer runs in a separate thread, you
53
+ could extend this a little:
54
+
55
+ class FooTicker < Brown::Agent
56
+ every 5 do
57
+ puts "#{self} is fooing in thread #{Thread.current}"
58
+ end
59
+ end
60
+
61
+ FooTicker.run
62
+
63
+ Every five seconds, it should print out a different `FooTicker` and `Thread`
64
+ object.
65
+
66
+ To show you how `every` is implemented behind the scenes, we can implement
67
+ this directly using the generic method, `stimulate`:
68
+
69
+ class FooTicker < Brown::Agent
70
+ stimulate :foo do |worker|
71
+ sleep 5
72
+ worker.call
73
+ end
74
+
75
+ def foo
76
+ puts "#{self} is fooing in thread #{Thread.current}"
77
+ end
78
+ end
79
+
80
+ FooTicker.run
81
+
82
+ What a `stimulate` declaration says is, quite simply:
83
+
84
+ * Run this block over and over and over and over again
85
+ * When the block wants a worker to run, it should run `worker.call`
86
+ * I'll then create a new instance of the agent class, and call the method
87
+ name you passed to `stimulate` in a separate thread.
88
+
89
+ You can pass arguments to the agent method call, by giving them to
90
+ `worker.call`.
91
+
92
+
93
+ ## AMQP publishing / consumption
94
+
95
+ Since message-based communication is a common pattern amongst cooperating
96
+ groups of agents, Brown comes with some helpers to make using AMQP painless.
97
+
98
+ Firstly, to publish a message, you need to declare a publisher, and then use
99
+ it somewhere. To declare a publisher, you use the `amqp_publisher` method:
100
+
101
+ class AmqpPublishingAgent < Brown::Agent
102
+ amqp_publisher :foo
103
+ end
104
+
105
+ There are a number of options you can add to this call, to set the AMQP
106
+ server URL, change the way that the AMQP exchange is declared, and a few
107
+ other things. For all the details on those, see the API docs for
108
+ {Brown::Agent.amqp_publisher}.
109
+
110
+ Once you have declared a publisher, you can send messages through it:
111
+
112
+ class AmqpPublishingAgent < Brown::Agent
113
+ amqp_publisher :foo, exchange_name: :foo, exchange_type: :fanout
114
+
115
+ every 5 do
116
+ foo.publish("FOO!")
117
+ end
118
+ end
119
+
120
+ The above example will perform the extremely important task of sending a
121
+ message containing the body `FOO!` every five seconds, forever. Hopefully
122
+ you can come up with some more practical uses for this functionality.
123
+
124
+
125
+ ### Consuming Messages
126
+
127
+ Messages being received are just like any other stimulus: you give a block
128
+ of code to run when a message is received. In its simplest form, it looks
129
+ like this:
130
+
131
+ class AmqpListenerAgent < Brown::Agent
132
+ amqp_listener :foo do |msg|
133
+ logger.info "Received message: #{msg.payload}"
134
+ msg.ack
135
+ end
136
+ end
137
+
138
+ This example sets up a queue to receive messages send to the exchange `foo`,
139
+ and then simply logs every message it receives. Note the `msg.ack` call;
140
+ this is important so that the broker knows that the message has been
141
+ received and can send you another message. If you forget to do this, you'll
142
+ only ever receive one message.
143
+
144
+ The `amqp_listener` method can take a *lot* of different options to
145
+ customise how it works; you'll want to read {Brown::Agent.amqp_listener} to
146
+ find out all about it.
147
+
148
+
149
+ ## Running agents on the command line
150
+
151
+ The easiest way to run agents "in production" is to use the `brown` command.
152
+ Simply pass a list of files which contain subclasses of `Brown::Agent`, and
153
+ those classes will be run in individual threads, with automatic restarting.
154
+ Convenient, huh?
155
+
156
+
157
+ ## Testing
158
+
159
+ Brown comes with facilities to unit test all of your agents. Since agents
160
+ simply receive stimuli and act on them, testing is quite simple in
161
+ principle, but the parallelism inherent in agents can make them hard to test
162
+ without some extra helpers.
163
+
164
+ To enable the additional testing helpers, you must `require
165
+ 'brown/test_helpers'` somewhere in your testing setup, before you define
166
+ your agents. This will add a bunch of extra methods, defined in
167
+ {Brown::TestHelpers} to {Brown::Agent}, which you can then call to examine
168
+ certain aspects of the agent (such as `memo?(name)` and
169
+ `amqp_publisher?(name)`) as well as send stimuli to the agent and have it
170
+ behave appropriately, which you can then make assertions about (either by
171
+ examining the new state of the overall system, or through the use of
172
+ mocks/spies).
173
+
174
+ While full documentation for all of the helper methods are available in the
175
+ YARD docs for {Brown::TestHelpers}, here are some specific tips for using
176
+ them to test certain aspects of your agents in popular testing frameworks.
177
+
178
+
179
+ ### RSpec
180
+
181
+ To test a directly declared stimulus, you don't need to do very much -- you
182
+ can just instantiate the agent class and call the method you want:
183
+
184
+ class StimulationAgent < Brown::Agent
185
+ stimulate :foo do |worker|
186
+ # Something something
187
+ worker.call
188
+ end
189
+ end
190
+
191
+ describe StimulationAgent do
192
+ it "does something" do
193
+ subject.foo
194
+
195
+ expect(something).to eq(something_else)
196
+ end
197
+ end
198
+
199
+ For memos, you can assert that an agent has a given memo quite easily:
200
+
201
+ class MemoAgent < Brown::Agent
202
+ memo :blargh do
203
+ "ohai"
204
+ end
205
+ end
206
+
207
+ describe MemoAgent do
208
+ it "has the memo" do
209
+ expect(MemoAgent).to have_memo(:blargh)
210
+ end
211
+ end
212
+
213
+ Then, on top of that, you can assert the value is as you expected, because
214
+ memos are accessable at the class level:
215
+
216
+ it "has the right value" do
217
+ expect(MemoAgent.blargh(:test)).to eq("ohai")
218
+ end
219
+
220
+ Or even put it in a let:
221
+
222
+ context "value" do
223
+ let(:value) { MemoAgent.blargh(:test) }
224
+ end
225
+
226
+ Note in the above examples that we passed the special value `:test` to the
227
+ call to `.blargh`; that was to let it know that we're definitely testing it
228
+ out. Recall that, ordinarily, a memo that is declared "unsafe" can only be
229
+ accessed inside a block passed to the memo method. For testing purposes,
230
+ rather than having to pass a block, we instead just pass in the special
231
+ `:test` symbol and it'll let us get the value back. Note that this won't
232
+ work unless you have `require`d `'brown/test_helpers'` *before* you defined
233
+ the agent class.
234
+
235
+ Testing timers is pretty straightforward, too; just trigger away:
236
+
237
+ class TimerAgent < Brown::Agent
238
+ every 10 do
239
+ $stderr.puts "Tick tock"
240
+ end
241
+ end
242
+
243
+ describe TimerAgent do
244
+ it "goes off on time" do
245
+ expect($stderr).to receive(:info).with("Tick tock")
246
+
247
+ TimerAgent.trigger(10)
248
+ end
249
+ end
250
+
251
+ It is pretty trivial to assert that some particular message was published
252
+ via AMQP:
253
+
254
+ class PublishTimerAgent < Brown::Agent
255
+ amqp_publisher :time
256
+
257
+ every 86400 do
258
+ time.publish "One day more!"
259
+ end
260
+ end
261
+
262
+ describe PublishTimerAgent do
263
+ it "publishes to schedule" do
264
+ expect(PublishTimerAgent.time).to receive(:publish).with("One day more!")
265
+
266
+ PublishTimerAgent.trigger(86400)
267
+ end
268
+ end
269
+
270
+ Testing what happens when a particular message gets received isn't much
271
+ trickier:
272
+
273
+ class ReceiverAgent < Brown::Agent
274
+ amqp_listener "some_exchange" do |msg|
275
+ $stderr.puts "Message: #{msg.payload}"
276
+ msg.ack
277
+ end
278
+ end
279
+
280
+ describe ReceiverAgent do
281
+ it "receives the message OK" do
282
+ expect($stderr).to receive(:puts).with("Message: ohai!")
283
+
284
+ was_acked = ReceiverAgent.amqp_receive("some_exchange", "ohai!")
285
+ expect(was_acked).to be(true)
286
+ end
287
+ end
288
+
289
+
290
+ ### Minitest / other testing frameworks
291
+
292
+ I don't have any examples for other testing frameworks, because I only use
293
+ RSpec. Contributions on this topic would be greatly appreciated.
294
+
295
+
296
+ # Contributing
297
+
298
+ Bug reports should be sent to the [Github issue
299
+ tracker](https://github.com/mpalmer/brown/issues), or
300
+ [e-mailed](mailto:theshed+brown@hezmatt.org). Patches can be sent as a
301
+ Github pull request, or [e-mailed](mailto:theshed+brown@hezmatt.org).
302
+
303
+
304
+ # Licence
305
+
306
+ Unless otherwise stated, everything in this repo is covered by the following
307
+ copyright notice:
308
+
309
+ Copyright (C) 2015 Matt Palmer <matt@hezmatt.org>
310
+
311
+ This program is free software: you can redistribute it and/or modify it
312
+ under the terms of the GNU General Public License version 3, as
313
+ published by the Free Software Foundation.
314
+
315
+ This program is distributed in the hope that it will be useful,
316
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
317
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
318
+ GNU General Public License for more details.
319
+
320
+ You should have received a copy of the GNU General Public License
321
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
data/bin/brown CHANGED
@@ -1,31 +1,91 @@
1
+ #!/usr/bin/env ruby
2
+
1
3
  # Run an agent. Any agent.
2
4
 
3
5
  require 'envied'
6
+ require 'logger'
4
7
 
5
8
  envied_config = (ENVied.config || ENVied::Configuration.new).tap do |cfg|
6
9
  cfg.enable_defaults!
7
- cfg.variable :BROWN_ACL_PATH
8
- cfg.variable :RABBITMQ_SERVER
10
+ cfg.variable :AMQP_URL, :string, :default => "amqp://localhost"
9
11
  cfg.variable :BROWN_LOG_LEVEL, :string, :default => "info"
10
12
  end
11
13
 
12
14
  ENVied.require(:default, :config => envied_config)
13
15
 
14
- # Trick smith-using agents into loving us -- since we have a `smith.rb` and
15
- # we should be the only thing in $LOAD_PATH at the moment, our version will
16
- # be loaded and the real smith will never darken our door.
17
- require 'smith'
18
-
19
- require *ARGV
16
+ ARGV.each { |f| require f }
20
17
 
21
18
  agent_classes = ObjectSpace.each_object(Class).select do |k|
22
19
  k != Brown::Agent and k.ancestors.include?(Brown::Agent)
23
20
  end
24
21
 
25
- Brown.start(:server_url => ENVied.RABBITMQ_SERVER,
26
- :log_level => ENVied.BROWN_LOG_LEVEL) do
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| "#{$$} [#{s[0]}] #{msg}\n" }
25
+
26
+ agents = ThreadGroup.new
27
+
28
+ def stop_agents(agents)
29
+ agents.list.each do |th|
30
+ th[:agent_class] && th[:agent_class].stop
31
+ end
32
+ end
33
+
34
+ Signal.trap("INT") do
35
+ $stderr.puts "Received SIGINT; stopping agents and exiting"
36
+ stop_agents(agents)
37
+ end
38
+ Signal.trap("TERM") do
39
+ $stderr.puts "Received SIGTERM; stopping agents and exiting"
40
+ stop_agents(agents)
41
+ end
42
+ Signal.trap("HUP", "IGNORE")
43
+ Signal.trap("USR1") do
44
+ Brown::Agent.more_log_detail
45
+ $stderr.puts "Log level now #{Logger::SEV_LABEL[Brown::Agent.logger.level]}"
46
+ end
47
+ Signal.trap("USR2") do
48
+ Brown::Agent.less_log_detail
49
+ $stderr.puts "Log level now #{Logger::SEV_LABEL[Brown::Agent.logger.level]}"
50
+ end
51
+
52
+ Brown::Agent.logger.info { "Brown starting up..." }
53
+
54
+ agent_classes.each do |klass|
55
+ th = Thread.new(klass) do |klass|
56
+ klass.run
57
+ Thread[:agent_class] = klass
58
+ end
59
+
60
+ agents.add(th)
61
+
62
+ Brown::Agent.logger.info { "Started agent #{klass}" }
63
+ end
64
+
65
+ loop do
66
+ sleep 1
67
+
68
+ agents.list.each do |th|
69
+ unless th.alive?
70
+ begin
71
+ th.join
72
+ rescue Exception => ex
73
+ Brown::Agent.logger.fatal { "Agent #{th[:agent_class]} crashed: #{ex.message} (#{ex.class})" }
74
+ Brown::Agent.logger.info { ex.backtrace.map { |l| " #{l}" }.join("\n") }
75
+ else
76
+ Brown::Agent.logger.warn "Agent #{th[:agent_class]} terminated itself"
77
+ end
78
+ klass = th[:agent_class]
79
+ th[:agent_class] = nil
80
+
81
+ Brown::Agent.logger.info { "Re-starting #{klass} agent" }
27
82
 
28
- Brown::ACLLoader.load_all(ENVied.BROWN_ACL_PATH.split(":"))
83
+ th = Thread.new(klass) do |klass|
84
+ klass.run
85
+ Thread[:agent_class] = klass
86
+ end
29
87
 
30
- agent_classes.each { |k| k.new.run }
88
+ agents.add(th)
89
+ end
90
+ end
31
91
  end
@@ -1,30 +1,40 @@
1
- require "git-version-bump"
1
+ begin
2
+ require 'git-version-bump'
3
+ rescue LoadError
4
+ nil
5
+ end
2
6
 
3
7
  Gem::Specification.new do |s|
4
- s.name = "brown"
5
- s.version = GVB.version
6
- s.date = GVB.date
7
-
8
- s.summary = "Run individual smith agents directly from the command line"
8
+ s.name = "brown"
9
9
 
10
- s.licenses = ["GPL-3"]
10
+ s.version = GVB.version rescue "0.0.0.1.NOGVB"
11
+ s.date = GVB.date rescue Time.now.strftime("%Y-%m-%d")
11
12
 
12
- s.authors = ["Richard Heycock", "Matt Palmer"]
13
+ s.platform = Gem::Platform::RUBY
13
14
 
14
- s.files = `git ls-files -z`.split("\0")
15
- s.executables = %w{brown}
15
+ s.summary = "Autonomous agent framework"
16
16
 
17
- s.has_rdoc = false
17
+ s.authors = ["Matt Palmer"]
18
+ s.email = ["theshed+brown@hezmatt.org"]
19
+ s.homepage = "http://theshed.hezmatt.org/brown"
18
20
 
19
- s.add_runtime_dependency "amqp", "~> 1.4"
20
- s.add_runtime_dependency "envied", "~> 0.8"
21
- s.add_runtime_dependency "eventmachine-le", "~> 1.0"
22
- s.add_runtime_dependency "extlib", "~> 0.9"
23
- s.add_runtime_dependency "murmurhash3", "~> 0.1"
24
- s.add_runtime_dependency "protobuf", "~> 3.0"
21
+ s.files = `git ls-files -z`.split("\0").reject { |f| f =~ /^(G|spec|Rakefile)/ }
22
+ s.executables = %w{brown}
25
23
 
26
- s.add_development_dependency "bundler"
27
- s.add_development_dependency "github-release"
28
- s.add_development_dependency "git-version-bump", "~> 0.10"
29
- s.add_development_dependency "rake", "~> 10.4", ">= 10.4.2"
24
+ s.required_ruby_version = ">= 2.1.0"
25
+
26
+ s.add_runtime_dependency "bunny", "~> 1.7"
27
+ s.add_runtime_dependency "envied", "~> 0.8"
28
+
29
+ s.add_development_dependency 'bundler'
30
+ s.add_development_dependency 'github-release'
31
+ s.add_development_dependency 'guard-spork'
32
+ s.add_development_dependency 'guard-rspec'
33
+ s.add_development_dependency 'pry-byebug'
34
+ s.add_development_dependency 'rake', '~> 10.4', '>= 10.4.2'
35
+ # Needed for guard
36
+ s.add_development_dependency 'rb-inotify', '~> 0.9'
37
+ s.add_development_dependency 'redcarpet'
38
+ s.add_development_dependency 'rspec'
39
+ s.add_development_dependency 'yard'
30
40
  end