brown 1.1.2 → 2.1.0

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