brown 2.2.2 → 2.2.2.25.g85ddf08
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.
- 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
|