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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 02ab3890a3b264186f6ad050332e0e13f897ec3a
4
- data.tar.gz: d9e120a8aa6478c5551c21fc9868e2b2a892a60e
2
+ SHA256:
3
+ metadata.gz: 9a28012ee7ec217616a14b03b2fc61827d7f813b34d50e4a847ac6ab7a0e0303
4
+ data.tar.gz: a8671a1393a9e9c89a00eb8f949ebb901b6607fae694fe697dac0a771c4cff98
5
5
  SHA512:
6
- metadata.gz: 781654a0fbf0643dbae1b2a481a14b3ca21dd3d58ae3b8ec5c3555c2f2f7bcc4cc9865e9ea4417a125ceba391366052f196f3dd72713a7254a4cddf7496774c0
7
- data.tar.gz: 10e4dca65cd6f577ac9f604e3a0678584d9281f0a23863702c03c5013aa67c4f73b8241c26f97c5f7775e836da151948d03853a2d34754b2519e23de0798766b
6
+ metadata.gz: 3d375959226721589ad39fbca4087bbe360a1394739a1689746b5715c34403b5594d6a3270f47a557dc46d58ba407be0f9917a71d9f015a3140db5cf9f3acd6e
7
+ data.tar.gz: ac06d3e484d71111faa20d4ca761ba97f0525e68d5ba25769725e8a6d9db2efd108a59a38e52242da3e9e62b959a894ecf2f61865c6bffcbc018c544b575603d
data/.gitignore CHANGED
@@ -3,3 +3,4 @@ Gemfile.lock
3
3
  /pkg
4
4
  /tmp
5
5
  /.yardoc
6
+ /.bundle
data/Dockerfile ADDED
@@ -0,0 +1,10 @@
1
+ FROM ruby:2.6-slim
2
+
3
+ ARG GEM_VERSION
4
+
5
+ COPY pkg/brown-$GEM_VERSION.gem /tmp/brown.gem
6
+
7
+ RUN gem install /tmp/brown.gem \
8
+ && rm -f /tmp/brown.gem
9
+
10
+ ENTRYPOINT ["/usr/local/bundle/bin/brown"]
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 build; gem install pkg/brown-<whatever>.gem
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 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.
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
- ## Agent-wide common variables
92
+ ## Sharing variables via memos
94
93
 
95
- There is some state you will want to keep across the entire agent. For
96
- this, Brown provides the concept of "memos". These are persistent objects,
97
- which you access via a class or instance method. To declare them, you
98
- simply do:
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 a method (both class and
105
- instance) which, the first time you run it, runs the provided block to
106
- create the memo object. Thereafter, that cached object is provided to the
107
- caller of the memo method.
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
- Because of Brown's multi-threaded nature, memos come with a built-in mutex
110
- to prevent concurrent usage. That means that every time you want to access
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 they are written in the class
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 if it is later in the
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(7) do
164
- # This is THE WRONG WAY AROUND. DO NOT DO THIS!
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
- Firstly, to publish a message, you need to declare a publisher, and then use
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 can send messages through it:
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. Hopefully
288
- you can come up with some more practical uses for this functionality.
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 send to the exchange `foo`,
305
- and then simply logs every message it receives. Note the `msg.ack` call;
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
- Simply pass a list of files which contain subclasses of `Brown::Agent`, and
319
- those classes will be run in individual threads, with automatic restarting.
320
- Convenient, huh?
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 unit test all of your agents. Since agents
326
- simply receive stimuli and act on them, testing is quite simple in
327
- principle, but the parallelism inherent in agents can make them hard to test
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
- subject.foo
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(MemoAgent).to have_memo(:blargh)
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, because
383
- memos are accessable at the class level:
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(MemoAgent.blargh(:test)).to eq("ohai")
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) { MemoAgent.blargh(:test) }
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 10 do
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).to receive(:info).with("Tick tock")
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(PublishTimerAgent.time).to receive(:publish).with("One day more!")
467
+ expect(agent.time).to receive(:publish).with("One day more!")
434
468
 
435
- PublishTimerAgent.trigger(86400)
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 = ReceiverAgent.amqp_receive("some_exchange", "ohai!")
491
+ was_acked = agent.amqp_receive("some_exchange", "ohai!")
454
492
  expect(was_acked).to be(true)
455
493
  end
456
494
  end