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.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/Dockerfile +10 -0
- data/README.md +137 -99
- data/bin/brown +19 -82
- data/brown.gemspec +3 -3
- data/lib/brown.rb +2 -0
- data/lib/brown/agent.rb +41 -411
- data/lib/brown/agent/amqp.rb +20 -0
- data/lib/brown/agent/amqp/class_methods.rb +147 -0
- data/lib/brown/agent/amqp/initializer.rb +144 -0
- data/lib/brown/agent/amqp_message.rb +26 -4
- data/lib/brown/agent/amqp_publisher.rb +68 -27
- data/lib/brown/agent/class_methods.rb +166 -0
- data/lib/brown/agent/stimulus.rb +84 -77
- data/lib/brown/agent/stimulus/metrics.rb +17 -0
- data/lib/brown/rspec.rb +16 -5
- data/lib/brown/test.rb +16 -28
- metadata +18 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9a28012ee7ec217616a14b03b2fc61827d7f813b34d50e4a847ac6ab7a0e0303
|
4
|
+
data.tar.gz: a8671a1393a9e9c89a00eb8f949ebb901b6607fae694fe697dac0a771c4cff98
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3d375959226721589ad39fbca4087bbe360a1394739a1689746b5715c34403b5594d6a3270f47a557dc46d58ba407be0f9917a71d9f015a3140db5cf9f3acd6e
|
7
|
+
data.tar.gz: ac06d3e484d71111faa20d4ca761ba97f0525e68d5ba25769725e8a6d9db2efd108a59a38e52242da3e9e62b959a894ecf2f61865c6bffcbc018c544b575603d
|
data/.gitignore
CHANGED
data/Dockerfile
ADDED
data/README.md
CHANGED
@@ -21,7 +21,7 @@ There's also the wonders of [the Gemfile](http://bundler.io):
|
|
21
21
|
|
22
22
|
If you're the sturdy type that likes to run from git:
|
23
23
|
|
24
|
-
rake
|
24
|
+
rake install
|
25
25
|
|
26
26
|
Or, if you've eschewed the convenience of Rubygems entirely, then you
|
27
27
|
presumably know what to do already.
|
@@ -31,11 +31,10 @@ presumably know what to do already.
|
|
31
31
|
|
32
32
|
To make something an agent, you simply create a subclass of `Brown::Agent`.
|
33
33
|
You can then use a simple DSL to define "stimuli", each of which (when
|
34
|
-
triggered) cause a new
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
periodically, or process a message on an AMQP queue.
|
34
|
+
triggered) cause a new thread to be created, to call the handler method for
|
35
|
+
that stimulus. You can do arbitrary things to detect stimuli, however there
|
36
|
+
are a number of pre-defined stimuli you can use to do standard things, like run
|
37
|
+
something periodically, or process a message on an AMQP queue.
|
39
38
|
|
40
39
|
As a very simple example, say you wanted to print `foo` every five seconds.
|
41
40
|
(Yes, you *could* do this in a loop, but humour me, would you?) Using the
|
@@ -47,7 +46,7 @@ built-in `every` stimuli, you could do it like this:
|
|
47
46
|
end
|
48
47
|
end
|
49
48
|
|
50
|
-
FooTicker.run
|
49
|
+
FooTicker.new({}).run
|
51
50
|
|
52
51
|
To demonstrate that each trip of the timer runs in a separate thread, you
|
53
52
|
could extend this a little:
|
@@ -58,7 +57,7 @@ could extend this a little:
|
|
58
57
|
end
|
59
58
|
end
|
60
59
|
|
61
|
-
FooTicker.run
|
60
|
+
FooTicker.new({}).run
|
62
61
|
|
63
62
|
Every five seconds, it should print out a different `FooTicker` and `Thread`
|
64
63
|
object.
|
@@ -77,7 +76,7 @@ this directly using the generic method, `stimulate`:
|
|
77
76
|
end
|
78
77
|
end
|
79
78
|
|
80
|
-
FooTicker.run
|
79
|
+
FooTicker.new({}).run
|
81
80
|
|
82
81
|
What a `stimulate` declaration says is, quite simply:
|
83
82
|
|
@@ -90,25 +89,27 @@ You can pass arguments to the agent method call, by giving them to
|
|
90
89
|
`worker.call`.
|
91
90
|
|
92
91
|
|
93
|
-
##
|
92
|
+
## Sharing variables via memos
|
94
93
|
|
95
|
-
There is some state you will want to keep
|
96
|
-
|
97
|
-
|
98
|
-
|
94
|
+
There is some state you will want to keep for the lifetime of the agent.
|
95
|
+
Because all stimulus processing happens in multiple threads, there is a helper
|
96
|
+
available to try and prevent concurrent access to mutable state -- the concept
|
97
|
+
of "memos". These are persistent objects, which you access only in a block,
|
98
|
+
and which wraps your access in a mutex.
|
99
|
+
|
100
|
+
To declare them, you simply do:
|
99
101
|
|
100
102
|
class MemoUser < Brown::Agent
|
101
103
|
memo(:foo) { Foo.new }
|
102
104
|
end
|
103
105
|
|
104
|
-
The way this works is that the memo defines
|
105
|
-
|
106
|
-
|
107
|
-
|
106
|
+
The way this works is that the memo defines an instance method which, the first
|
107
|
+
time you run it, runs the provided block to create the memo object, which is
|
108
|
+
then cached. Thereafter, that cached object is provided, which can be mutated
|
109
|
+
(though not replaced) safely.
|
108
110
|
|
109
|
-
|
110
|
-
to
|
111
|
-
the memo, you must do so inside a block:
|
111
|
+
To acquire the lock, and run some code that requires the memoised object, you
|
112
|
+
pass a block to the memo method, which gets the object passed into it, like this:
|
112
113
|
|
113
114
|
class MemoUser < Brown::Agent
|
114
115
|
memo(:foo) { Foo.new }
|
@@ -120,6 +121,10 @@ the memo, you must do so inside a block:
|
|
120
121
|
end
|
121
122
|
end
|
122
123
|
|
124
|
+
Here you can see that the instance of `Foo` gets passed into the block given to
|
125
|
+
the call to `foo`, and then the `Foo#frob` method can be called safe in the
|
126
|
+
knowledge that nobody else is frobbing the foo at the same time.
|
127
|
+
|
123
128
|
The crucial thing to note here is that you *only have the memo lock inside
|
124
129
|
the block*. If you were to capture the memo object into a variable outside
|
125
130
|
the block, and then use it (read *or* write) outside the block, Really Bad
|
@@ -129,7 +134,8 @@ When you have multiple memos, it is entirely possible that you can end up
|
|
129
134
|
deadlocking your agent by acquiring the locks for various memos in different
|
130
135
|
orders. Those dining philosophers are always getting themselves in a
|
131
136
|
muddle. To prevent this problem, it is highly recommended that you always
|
132
|
-
acquire the locks for your memos in the order
|
137
|
+
acquire the locks for your memos in the same order -- by convention, the
|
138
|
+
"correct" order to access memos is the order they are placed in the class
|
133
139
|
definition:
|
134
140
|
|
135
141
|
class MemoUser < Brown::Agent
|
@@ -144,8 +150,7 @@ definition:
|
|
144
150
|
end
|
145
151
|
|
146
152
|
every(6) do
|
147
|
-
# Acquiring a single lock, even
|
148
|
-
# list, is fine
|
153
|
+
# Acquiring a single lock, even a different one, is fine
|
149
154
|
bar do |b|
|
150
155
|
b.baznicate(b)
|
151
156
|
end
|
@@ -160,9 +165,9 @@ definition:
|
|
160
165
|
end
|
161
166
|
end
|
162
167
|
|
163
|
-
every(
|
164
|
-
# This is
|
165
|
-
# YOU WILL GET DEADLOCKS!
|
168
|
+
every(8) do
|
169
|
+
# This is the WRONG WAY AROUND. DO NOT DO THIS!
|
170
|
+
# YOU WILL GET DEADLOCKS SOONER OR LATER!
|
166
171
|
bar do |b|
|
167
172
|
foo do |f|
|
168
173
|
b.baznicate(f)
|
@@ -188,6 +193,7 @@ of the memo object:
|
|
188
193
|
|
189
194
|
every(60) do
|
190
195
|
now do |t|
|
196
|
+
# This will not change the value of the memo object
|
191
197
|
t = Time.now
|
192
198
|
end
|
193
199
|
end
|
@@ -220,62 +226,35 @@ This example code will print the same time for a minute, before changing to
|
|
220
226
|
a new minute.
|
221
227
|
|
222
228
|
|
223
|
-
### Thread-safe memos
|
224
|
-
|
225
|
-
There are some classes which are themselves thread-safe -- usually because
|
226
|
-
the class author has gone to some trouble to provide zer own, more
|
227
|
-
fine-grained, locking on the data within the object. If you are *quite
|
228
|
-
sure* you have such a thread-safe object to memoise, you can use
|
229
|
-
{Brown::Agent::ClassMethods.safe_memo} for that purpose:
|
230
|
-
|
231
|
-
class SafeMemoUser < Brown::Agent
|
232
|
-
safe_memo(:foo) { ThreadSafeFoo.new }
|
233
|
-
end
|
234
|
-
|
235
|
-
The benefit of this form of memos is that you don't have to access them in a
|
236
|
-
block:
|
237
|
-
|
238
|
-
class SafeMemoUser < Brown::Agent
|
239
|
-
safe_memo(:foo) { ThreadSafeFoo.new }
|
240
|
-
|
241
|
-
every(10) do
|
242
|
-
foo.frob
|
243
|
-
end
|
244
|
-
end
|
245
|
-
|
246
|
-
Like regular memos, you cannot reassign a thread-safe memo to another
|
247
|
-
object:
|
248
|
-
|
249
|
-
class SafeMemoUser < Brown::Agent
|
250
|
-
safe_memo(:foo) { ThreadSafeFoo.new }
|
251
|
-
|
252
|
-
every(10) do
|
253
|
-
# This will explode with a NameError
|
254
|
-
foo = ThreadSafeFoo.new
|
255
|
-
end
|
256
|
-
end
|
257
|
-
|
258
|
-
|
259
229
|
## AMQP publishing / consumption
|
260
230
|
|
261
231
|
Since message-based communication is a common pattern amongst cooperating
|
262
232
|
groups of agents, Brown comes with some helpers to make using AMQP painless.
|
233
|
+
To use these helpers, your agent class must `include Brown::Agent::AMQP`.
|
234
|
+
|
263
235
|
|
264
|
-
|
236
|
+
### Publishing Messages
|
237
|
+
|
238
|
+
To publish a message, you need to declare a publisher, and then use
|
265
239
|
it somewhere. To declare a publisher, you use the `amqp_publisher` method:
|
266
240
|
|
267
241
|
class AmqpPublishingAgent < Brown::Agent
|
242
|
+
include Brown::Agent::AMQP
|
243
|
+
|
268
244
|
amqp_publisher :foo
|
269
245
|
end
|
270
246
|
|
271
247
|
There are a number of options you can add to this call, to set the AMQP
|
272
248
|
server URL, change the way that the AMQP exchange is declared, and a few
|
273
249
|
other things. For all the details on those, see the API docs for
|
274
|
-
{Brown::Agent.amqp_publisher}.
|
250
|
+
{Brown::Agent::AMQP.amqp_publisher}.
|
275
251
|
|
276
|
-
Once you have declared a publisher, you
|
252
|
+
Once you have declared a publisher, you get a method named after the
|
253
|
+
publisher, which you can send messages through:
|
277
254
|
|
278
255
|
class AmqpPublishingAgent < Brown::Agent
|
256
|
+
include Brown::Agent::AMQP
|
257
|
+
|
279
258
|
amqp_publisher :foo, exchange_name: :foo, exchange_type: :fanout
|
280
259
|
|
281
260
|
every 5 do
|
@@ -284,8 +263,8 @@ Once you have declared a publisher, you can send messages through it:
|
|
284
263
|
end
|
285
264
|
|
286
265
|
The above example will perform the extremely important task of sending a
|
287
|
-
message containing the body `FOO!` every five seconds, forever
|
288
|
-
|
266
|
+
message containing the body `FOO!` every five seconds, forever, to the
|
267
|
+
fanout exchange named `foo`.
|
289
268
|
|
290
269
|
|
291
270
|
### Consuming Messages
|
@@ -295,41 +274,82 @@ of code to run when a message is received. In its simplest form, it looks
|
|
295
274
|
like this:
|
296
275
|
|
297
276
|
class AmqpListenerAgent < Brown::Agent
|
277
|
+
include Brown::Agent::AMQP
|
278
|
+
|
298
279
|
amqp_listener :foo do |msg|
|
299
280
|
logger.info "Received message: #{msg.payload}"
|
300
281
|
msg.ack
|
301
282
|
end
|
302
283
|
end
|
303
284
|
|
304
|
-
This example sets up a queue to receive messages
|
305
|
-
and then
|
285
|
+
This example sets up a queue to receive messages sent to the exchange `foo`,
|
286
|
+
and then logs every message it receives. Note the `msg.ack` call;
|
306
287
|
this is important so that the broker knows that the message has been
|
307
288
|
received and can send you another message. If you forget to do this, you'll
|
308
289
|
only ever receive one message.
|
309
290
|
|
310
291
|
The `amqp_listener` method can take a *lot* of different options to
|
311
|
-
customise how it works; you'll want to read {Brown::Agent.amqp_listener} to
|
292
|
+
customise how it works; you'll want to read {Brown::Agent::AMQP.amqp_listener} to
|
312
293
|
find out all about it.
|
313
294
|
|
314
295
|
|
315
296
|
## Running agents on the command line
|
316
297
|
|
317
298
|
The easiest way to run agents "in production" is to use the `brown` command.
|
318
|
-
|
319
|
-
|
320
|
-
|
299
|
+
Pass it a file which contains the definition of a subclass of `Brown::Agent`,
|
300
|
+
and it'll fire off a new agent. Convenient, huh?
|
301
|
+
|
302
|
+
|
303
|
+
### Metrics, signals, and bears, oh my!
|
304
|
+
|
305
|
+
Brown uses the [`service_skeleton`](https://github.com/discourse/service_skeleton) gem
|
306
|
+
to manage agents, and so you have access to a wide variety of additional (optional)
|
307
|
+
features, including metrics, log management, and sensible signal handling. See [the
|
308
|
+
`service_skeleton` README](https://github.com/discourse/service_skeleton#readme) for details
|
309
|
+
of all that this fine framework has to offer.
|
310
|
+
|
311
|
+
|
312
|
+
### Running agents in Docke... er, I mean, Moby
|
313
|
+
|
314
|
+
Since Moby is the new hawtness, Brown provides a simple base container upon which you can layer
|
315
|
+
your agent code, and then spawn agents to your heart's content. As a "simple" example,
|
316
|
+
let's say you have some agents that need Sequel and Postgres, and your agents live in
|
317
|
+
the `lib/agents` subdirectory of your repo. The following `Dockerfile` would build
|
318
|
+
a new image containing all you need:
|
319
|
+
|
320
|
+
FROM womble/brown
|
321
|
+
|
322
|
+
RUN apt-get update \
|
323
|
+
&& apt-get -y install libpq-dev libpq5 \
|
324
|
+
&& gem install pg sequel \
|
325
|
+
&& apt-get -y purge libpq-dev \
|
326
|
+
&& apt-get -y autoremove --purge \
|
327
|
+
&& rm -rf /var/lib/apt/lists/*
|
328
|
+
|
329
|
+
COPY lib/* /usr/local/lib/ruby/2.6.0/
|
330
|
+
COPY lib/agents /agents
|
331
|
+
|
332
|
+
From there, it is a simple matter of building your new image, and running your agents,
|
333
|
+
by running a separate docker container from the common image, passing the filename
|
334
|
+
of each agent as the sole command-line argument:
|
335
|
+
|
336
|
+
docker build -t control .
|
337
|
+
docker run -n agent-86 -d control /agents/86.rb
|
338
|
+
docker run -n agent-99 -d control /agents/99.rb
|
339
|
+
|
340
|
+
... and you're up and running!
|
321
341
|
|
322
342
|
|
323
343
|
## Testing
|
324
344
|
|
325
|
-
Brown comes with facilities to
|
326
|
-
simply receive stimuli and act on them, testing is quite simple in
|
327
|
-
principle,
|
328
|
-
without some extra helpers.
|
345
|
+
Brown comes with facilities to write automated tests for your agents. Since
|
346
|
+
agents simply receive stimuli and act on them, testing is quite simple in
|
347
|
+
principle. However, the inherent parallelism going on behind the scenes can
|
348
|
+
make agents hard to test without some extra helpers.
|
329
349
|
|
330
350
|
To enable the additional testing helpers, you must `require 'brown/test'`
|
331
351
|
somewhere in your testing setup, before you define your agents. This will
|
332
|
-
add a bunch of extra methods, defined in {Brown::TestHelpers} to
|
352
|
+
add a bunch of extra methods, defined in {Brown::TestHelpers}, to
|
333
353
|
{Brown::Agent}, which you can then call to examine certain aspects of the
|
334
354
|
agent (such as `memo?(name)` and `amqp_publisher?(name)`) as well as send
|
335
355
|
stimuli to the agent and have it behave appropriately, which you can then
|
@@ -358,8 +378,10 @@ can just instantiate the agent class and call the method you want:
|
|
358
378
|
end
|
359
379
|
|
360
380
|
describe StimulationAgent do
|
381
|
+
let(:agent) { described_class.new({}) }
|
382
|
+
|
361
383
|
it "does something" do
|
362
|
-
|
384
|
+
agent.foo
|
363
385
|
|
364
386
|
expect(something).to eq(something_else)
|
365
387
|
end
|
@@ -374,53 +396,63 @@ For memos, you can assert that an agent has a given memo quite easily:
|
|
374
396
|
end
|
375
397
|
|
376
398
|
describe MemoAgent do
|
399
|
+
let(:agent) { described_class.new({}) }
|
400
|
+
|
377
401
|
it "has the memo" do
|
378
|
-
expect(
|
402
|
+
expect(agent).to have_memo(:blargh)
|
379
403
|
end
|
380
404
|
end
|
381
405
|
|
382
|
-
Then, on top of that, you can assert the value is as you expected,
|
383
|
-
|
406
|
+
Then, on top of that, you can assert the value is as you expected, with the
|
407
|
+
`#memo_value` method:
|
384
408
|
|
385
409
|
it "has the right value" do
|
386
|
-
expect(
|
410
|
+
expect(agent.memo_value(:blargh)).to eq("ohai")
|
387
411
|
end
|
388
412
|
|
389
413
|
Or even put it in a let:
|
390
414
|
|
391
415
|
context "value" do
|
392
|
-
let(:value) {
|
416
|
+
let(:value) { agent.memo_value(:blargh) }
|
393
417
|
end
|
394
418
|
|
395
|
-
Note in the above examples that we passed the special value `:test` to the
|
396
|
-
call to `.blargh`; that was to let it know that we're definitely testing it
|
397
|
-
out. Recall that, ordinarily, a memo that is declared "unsafe" can only be
|
398
|
-
accessed inside a block passed to the memo method. For testing purposes,
|
399
|
-
rather than having to pass a block, we instead just pass in the special
|
400
|
-
`:test` symbol and it'll let us get the value back. Note that this won't
|
401
|
-
work unless you have `require`d `'brown/test_helpers'` *before* you defined
|
402
|
-
the agent class.
|
403
|
-
|
404
419
|
Testing timers is pretty straightforward, too; just trigger away:
|
405
420
|
|
406
421
|
class TimerAgent < Brown::Agent
|
407
|
-
every
|
422
|
+
every 5 do
|
408
423
|
$stderr.puts "Tick tock"
|
409
424
|
end
|
425
|
+
|
426
|
+
every 10 do
|
427
|
+
$stderr.puts "BONG"
|
428
|
+
end
|
429
|
+
|
430
|
+
every 10 do
|
431
|
+
$stderr.puts "CRASH!"
|
432
|
+
end
|
410
433
|
end
|
411
434
|
|
412
435
|
describe TimerAgent do
|
436
|
+
let(:agent) { described_class.mew({}) }
|
437
|
+
|
413
438
|
it "goes off on time" do
|
414
|
-
expect($stderr).
|
439
|
+
expect($stderr).to_not receive(:info).with("Tick tock")
|
440
|
+
expect($stderr).to receive(:info).with("BONG")
|
441
|
+
expect($stderr).to receive(:info).with("CRASH!")
|
415
442
|
|
416
443
|
TimerAgent.trigger(10)
|
417
444
|
end
|
418
445
|
end
|
419
446
|
|
447
|
+
Calling `#trigger` calls all of the `every` stimuli which run every number
|
448
|
+
of seconds given.
|
449
|
+
|
420
450
|
It is pretty trivial to assert that some particular message was published
|
421
451
|
via AMQP:
|
422
452
|
|
423
453
|
class PublishTimerAgent < Brown::Agent
|
454
|
+
include Brown::Agent::AMQP
|
455
|
+
|
424
456
|
amqp_publisher :time
|
425
457
|
|
426
458
|
every 86400 do
|
@@ -429,10 +461,12 @@ via AMQP:
|
|
429
461
|
end
|
430
462
|
|
431
463
|
describe PublishTimerAgent do
|
464
|
+
let(:agent) { described_class.new({}) }
|
465
|
+
|
432
466
|
it "publishes to schedule" do
|
433
|
-
expect(
|
467
|
+
expect(agent.time).to receive(:publish).with("One day more!")
|
434
468
|
|
435
|
-
|
469
|
+
agent.trigger(86400)
|
436
470
|
end
|
437
471
|
end
|
438
472
|
|
@@ -440,6 +474,8 @@ Testing what happens when a particular message gets received isn't much
|
|
440
474
|
trickier:
|
441
475
|
|
442
476
|
class ReceiverAgent < Brown::Agent
|
477
|
+
include Brown::Agent::AMQP
|
478
|
+
|
443
479
|
amqp_listener "some_exchange" do |msg|
|
444
480
|
$stderr.puts "Message: #{msg.payload}"
|
445
481
|
msg.ack
|
@@ -447,10 +483,12 @@ trickier:
|
|
447
483
|
end
|
448
484
|
|
449
485
|
describe ReceiverAgent do
|
486
|
+
let(:agent) { described_class.new({}) }
|
487
|
+
|
450
488
|
it "receives the message OK" do
|
451
489
|
expect($stderr).to receive(:puts).with("Message: ohai!")
|
452
490
|
|
453
|
-
was_acked =
|
491
|
+
was_acked = agent.amqp_receive("some_exchange", "ohai!")
|
454
492
|
expect(was_acked).to be(true)
|
455
493
|
end
|
456
494
|
end
|