brown 1.1.2 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +4 -1
- data/.yardopts +1 -0
- data/LICENCE +674 -0
- data/README.md +321 -0
- data/bin/brown +72 -12
- data/brown.gemspec +31 -21
- data/lib/.gitkeep +0 -0
- data/lib/brown.rb +17 -3
- data/lib/brown/agent.rb +417 -21
- data/lib/brown/agent/amqp_message.rb +42 -0
- data/lib/brown/agent/amqp_message_mock.rb +28 -0
- data/lib/brown/agent/amqp_publisher.rb +169 -0
- data/lib/brown/agent/memo.rb +64 -0
- data/lib/brown/agent/stimulus.rb +143 -0
- data/lib/brown/test.rb +152 -0
- metadata +88 -64
- data/Gemfile +0 -3
- data/Rakefile +0 -19
- data/lib/brown/acl_loader.rb +0 -57
- data/lib/brown/acl_lookup.rb +0 -52
- data/lib/brown/amqp_errors.rb +0 -148
- data/lib/brown/logger.rb +0 -51
- data/lib/brown/message.rb +0 -73
- data/lib/brown/module_methods.rb +0 -134
- data/lib/brown/queue_definition.rb +0 -32
- data/lib/brown/queue_factory.rb +0 -33
- data/lib/brown/receiver.rb +0 -143
- data/lib/brown/sender.rb +0 -92
- data/lib/brown/util.rb +0 -58
- data/lib/smith.rb +0 -4
data/README.md
ADDED
@@ -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 :
|
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
|
-
|
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.
|
26
|
-
|
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
|
-
|
83
|
+
th = Thread.new(klass) do |klass|
|
84
|
+
klass.run
|
85
|
+
Thread[:agent_class] = klass
|
86
|
+
end
|
29
87
|
|
30
|
-
|
88
|
+
agents.add(th)
|
89
|
+
end
|
90
|
+
end
|
31
91
|
end
|
data/brown.gemspec
CHANGED
@@ -1,30 +1,40 @@
|
|
1
|
-
|
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
|
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.
|
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.
|
13
|
+
s.platform = Gem::Platform::RUBY
|
13
14
|
|
14
|
-
s.
|
15
|
-
s.executables = %w{brown}
|
15
|
+
s.summary = "Autonomous agent framework"
|
16
16
|
|
17
|
-
s.
|
17
|
+
s.authors = ["Matt Palmer"]
|
18
|
+
s.email = ["theshed+brown@hezmatt.org"]
|
19
|
+
s.homepage = "http://theshed.hezmatt.org/brown"
|
18
20
|
|
19
|
-
s.
|
20
|
-
s.
|
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.
|
27
|
-
|
28
|
-
s.
|
29
|
-
s.
|
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
|