mercury_amqp 0.3.0 → 0.4.0

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
2
  SHA1:
3
- metadata.gz: 6ce3a4a09a924946d240cfbabeea944dffba4ec7
4
- data.tar.gz: 772194f0c84be6d336fc16b2821f0e728e8b1f4b
3
+ metadata.gz: 459b852ab72eefefec630bac89fde701f7fc2018
4
+ data.tar.gz: a0ae43b454314876e4dc1be0e6ccf9480d8a9ed6
5
5
  SHA512:
6
- metadata.gz: ab8452d14bdb64949877c748bd8eafbe37c5bd5ff652772950025f63b70f3db9d0754397606a9d223292d33142c2561d88f410cf4b9c9dd7acc416e0a7b1290b
7
- data.tar.gz: b63c33292ee6299d6b5832d2c1157fc6a229b281266e6fc7489dec4fed6f8126b4d023fce49abe2f40132816022c3411230d1ec078547b7fe910a8b8837a1db0
6
+ metadata.gz: 296ef00ec39d0419c4f55b185528a4d0ed905d236014ad0f8a432a310a01033cf4105c6c2c19a82d02cec850c4de3e8af2a96eba8f0e6d8f4761002a1ef971df
7
+ data.tar.gz: 7fcbe0aa189f7d5924312210ddaf7eb1fb6111ff0f4f74e38fddc1a7410f26a8820fd871cac8daaf0c93b363bee153cb61b1b85ca4ee760bc8c6240048758ca3
data/.gitignore CHANGED
@@ -1,5 +1,5 @@
1
+ *.png
1
2
  .idea
2
- .bundle
3
3
  Gemfile.lock
4
- *~
5
4
  *.gem
5
+ *~
data/README.md CHANGED
@@ -1,26 +1,140 @@
1
1
  mercury
2
2
  =======
3
3
 
4
- Mercury is a messaging layer intended to hide complexity for typical
5
- messaging scenarios. It is backed by the AMQP gem and consequently
6
- runs in an EventMachine reactor and has an asynchronous API. Mercury
7
- consists of _sources_, _work queues_, and _listeners_. A message is
8
- published to a source, to which one or more work queues and/or
9
- listeners are attached. These map roughly to AMQP constructs:
4
+ Mercury is a messaging layer that hides complexity for typical messaging scenarios.
10
5
 
11
- - **source**: topic exchange
12
- - **work queue**: durable named queue
13
- - **listener**: temporary anonymous queue
14
- - **tag**: routing key
6
+ - backed by [AMQP][amqp]
7
+ - runs in an [EventMachine][em] reactor
8
+ - asynchronous API
9
+ - serializes messages as JSON
10
+ - optional monadic interface makes testing easier
11
+ - simulator allows tests to run without a RabbitMQ server
15
12
 
16
- At the moment, mercury is backed by AMQP and serializes messages as
17
- JSON. In the future, additional transports and message formats may be
18
- supported.
13
+ ### Constructs
19
14
 
20
- Mercury writes string messages directly without encoding; this allows
21
- a client to pre-encode a message using an arbitrary encoding. The
22
- receiving client receives the encoded bytes as the message content
23
- (assuming the encoded message fails to parse as JSON).
15
+ ![Constructs diagram](constructs.png)
16
+
17
+ In the above example, two publishers independently publish messages to the same source.
18
+ One of them chooses to attach the tag `foo.success` to the messages it publishes.
19
+ When the source receives a message, it immediately broadcasts the message to all attached
20
+ queues.
21
+
22
+ Two independent listeners receive messages from the source. A listener has a private
23
+ queue that is automatically removed when the listener disconnects from the server (i.e.,
24
+ any queued messages the listener would have received are lost).
25
+
26
+ A worker pool "A" has two workers handling messages. Messages in the queue are dealt
27
+ to the workers as the workers acknowledge handling prior messages. If a worker disconnects
28
+ without acknowledging completion, the message(s) it was working on are automatically replaced at the
29
+ head of the queue. Worker queues persist even if all workers disconnect. Each worker pool
30
+ has its own queue. Consequently, workers within a pool compete for messages, whereas workers
31
+ across pools do not compete for messages.
32
+
33
+ Worker pool "B" has just one worker and has specified a tag filter.
34
+
35
+ Listeners and worker pools can specify _tag filters_ which limit the messages they receive
36
+ to only those with tags matching a particular pattern. A _tag_ is zero or more words delimited
37
+ by periods. A _word_ is a string of alphanumeric characters. A _filter_ is a string of zero or more
38
+ words or wildcards. A _wildcard_ is either `*` or `#`.
39
+
40
+ - `*` matches exactly one word.
41
+ - `#` matches zero or more words.
42
+
43
+ _Note:_ A filter with zero words (`''`) matches no messages.
44
+
45
+ _Note:_ Wildcards match only entire words. The filter `f*.success` does not match the tag `foo.success`.
46
+
47
+ A typical scenario is to have a single worker pool processing messages.
48
+
49
+ ![Typical scenario diagram](typical.png)
50
+
51
+ The publishers are often instances of the same service: either some
52
+ other mercury worker pool or a web service.
53
+
54
+
55
+ ### Operations
56
+
57
+ **open**(_parallelism_)
58
+
59
+ Creates a mercury instance. _paralellism_ is a natural number that indicates
60
+ the maximum total number of outstanding (unacknowledged) messages allowed
61
+ for this instance's workers. See the code for additional parameters.
62
+
63
+ **publish**(_source_name_, _msg_, _tag_)
64
+
65
+ Publishes a message to a source, with an optional tag.
66
+
67
+ **start_worker**(_worker_pool_name_, _source_name_, _tag_filter_, _msg_handler_)
68
+
69
+ Starts a worker in the specified pool subscribing to the specified
70
+ source with the specified filter (optional). Incoming messages are
71
+ passed to the message handler procedure.
72
+
73
+ **start_listener**(_source_name_, _tag_filter_, _msg_handler_)
74
+
75
+ Starts a listener.
76
+
77
+ **ack**(_msg_)
78
+
79
+ Indicates message handling succeeded. The message is removed from the queue.
80
+
81
+ **nack**(_msg_)
82
+
83
+ Indicates message handling failed, with the assumption that it might succeed at
84
+ a later time. The message is returned to the queue.
85
+
86
+ **reject**(_msg_)
87
+
88
+ Indicates message handling failed, with the assumption that it can never succeed.
89
+ The message is removed from the queue.
90
+
91
+ _Note:_ All operations create the referenced constructs if they do not already exist.
92
+
93
+ ### Serialization
94
+
95
+ If a message is a hash, mercury serializes it to JSON upon sending and deserializes
96
+ it upon receiving.
97
+
98
+ If a message is a string, mercury writes it directly without any
99
+ serialization; this allows a client to pre-encode a message using an
100
+ arbitrary encoding. The receiving client receives the exact same
101
+ string as the message content (assuming the serialized message fails to
102
+ parse as JSON).
103
+
104
+ ### Logatron integration
105
+
106
+ Mercury depends on the
107
+ [`logatron`][logatron] gem and propagates
108
+ logatron's request ID (`Logatron.msg_id`) through the AMQP header
109
+ `X-Ascent-Log-Id`. This enables a log aggregation service to find the
110
+ logs associated with a particular incoming request, even though the
111
+ log entries may be scattered across various services.
112
+
113
+ ### Thread safety
114
+
115
+ Mercury is not threadsafe. All calls to a particular instance must be made from the
116
+ thread in which the instance was created.
117
+
118
+ ### Error handling
119
+
120
+ If there is a communication error (e.g., the connection to the server breaks)
121
+ mercury raises an error. This behavior can be overridden.
122
+
123
+ ### Long running operations
124
+
125
+ The AMQP gem uses EventMachine timers to send heartbeats to the
126
+ server. Message handling blocks the event loop, so each message must
127
+ be handled within a few seconds, otherwise the server will miss a
128
+ heartbeat and disconnect the client. Long-running message handlers
129
+ should move their work to a thread pool to avoid blocking the event
130
+ loop.
131
+ [`EventMachine.defer`][em_defer]
132
+ and
133
+ [`fiber_defer`][fiber_defer]
134
+ are two facilities for accomplishing this.
135
+
136
+
137
+ ### Example Usage
24
138
 
25
139
 
26
140
  ```ruby
@@ -30,6 +144,8 @@ def run
30
144
  EventMachine.run do
31
145
  Mercury.open do |m|
32
146
  m.start_worker('cooks', 'orders', method(:handle_message)) do
147
+ # When this continuation block is called, the worker is guaranteed
148
+ # to have been started, so we can start publishing orders.
33
149
  m.publish('orders', {'table' => 5, 'items' => ['salad', 'steak', 'cake']})
34
150
  end
35
151
  end
@@ -71,7 +187,7 @@ end
71
187
 
72
188
  require 'mercury/monadic'
73
189
 
74
- seq do
190
+ seql do
75
191
  let(:m) { Mercury::Monadic.open }
76
192
  and_then { m.start_listener(source) }
77
193
  let(:r1) { m.source_exists?(source) }
@@ -82,4 +198,21 @@ seq do
82
198
  and_then { m.close }
83
199
  and_lift { done }
84
200
  end
85
- ```
201
+ ```
202
+
203
+ Design Decisions
204
+ ----------------
205
+
206
+ #### Continuation blocks are required
207
+
208
+ It would be possible to make continuation blocks optional. The problem is that this allows the user
209
+ to make the mistake of treating the API as synchronous and only discover their error when tests fail,
210
+ probably intermittently. An empty block can be passed, but at least it indicates that the continuation
211
+ is being intentionally ignored.
212
+
213
+
214
+ [amqp]: https://github.com/ruby-amqp/amqp
215
+ [em]: https://github.com/eventmachine/eventmachine
216
+ [logatron]: https://github.com/indigobio/logatron
217
+ [em_defer]: http://www.rubydoc.info/github/eventmachine/eventmachine/EventMachine.defer
218
+ [fiber_defer]: https://github.com/indigobio/abstractivator/blob/master/lib/abstractivator/fiber_defer.rb
data/constructs.dot ADDED
@@ -0,0 +1,24 @@
1
+ digraph G {
2
+ rankdir=LR
3
+ p1 [label="publisher", shape=rect]
4
+ p2 [label="publisher", shape=rect]
5
+ s [label="source", shape=circle]
6
+ l1q [label="{|||||}", shape=record, fixedsize=true, height=0.3, width=1.5]
7
+ l2q [label="{|||||}", shape=record, fixedsize=true, height=0.3, width=1.5]
8
+ w1q [label="{|||||}", shape=record, fixedsize=true, height=0.3, width=1.5]
9
+ w2q [label="{|||||}", shape=record, fixedsize=true, height=0.3, width=1.5]
10
+ l1 [label="listener", shape=rect]
11
+ l2 [label="listener", shape=rect]
12
+ w1a1 [label="worker A", shape=rect]
13
+ w1a2 [label="worker A", shape=rect]
14
+ w2b [label="worker B", shape=rect]
15
+
16
+ p1 -> s [tailport=e]
17
+ p2 -> s [tailport=e, label="tag: foo.success", fontsize=8, fontname="mono"]
18
+ s -> {l1q, l2q, w1q} [headport=w]
19
+ s -> w2q [headport=w, label="tag_filter: *.success", fontsize=8, fontname="mono"]
20
+ l1q -> l1 [tailport=e, headport=w]
21
+ l2q -> l2 [tailport=e, headport=w]
22
+ w1q -> {w1a1, w1a2} [tailport=e, headport=w]
23
+ w2q -> w2b [tailport=e, headport=w]
24
+ }
data/constructs.png ADDED
Binary file
data/lib/mercury/cps.rb CHANGED
@@ -37,7 +37,7 @@ class Mercury
37
37
  Cps.new do |*args, &k|
38
38
  self.run(*args) do |*args2|
39
39
  next_cps = pm.call(*args2)
40
- next_cps.is_a?(Cps) or raise "'and_then' block did not return a Cps. Did you want 'and_lift'? at #{pm.source_location}"
40
+ next_cps.is_a?(Cps) or raise "'and_then' block did not return a Cps object. Did you want 'and_lift'? at #{pm.source_location}"
41
41
  next_cps.run(&k)
42
42
  end
43
43
  end
@@ -52,7 +52,14 @@ class Mercury
52
52
 
53
53
  # Returns a Cps for a non-CPS proc.
54
54
  def self.lift(&p)
55
- new { |*args, &k| k.call(p.call(*args)) }
55
+ new do |*args, &k|
56
+ value = p.call(*args)
57
+ if value.is_a?(Cps)
58
+ # This is technically valid, but 99% of the time it indicates a programming error.
59
+ raise "'lift' block returned a Cps object. Did you want 'and_then'? at #{p.source_location}"
60
+ end
61
+ k.call(value)
62
+ end
56
63
  end
57
64
 
58
65
  # The identity function as a Cps.
data/lib/mercury/fake.rb CHANGED
@@ -15,9 +15,21 @@ require 'mercury/fake/subscriber'
15
15
  # broken sockets, etc.
16
16
  class Mercury
17
17
  class Fake
18
- def initialize(domain=:default, parallelism: 1)
18
+ def self.install(rspec_context, domain=:default)
19
+ rspec_context.instance_exec do
20
+ allow(Mercury).to receive(:open) do |**kws, &k|
21
+ EM.next_tick { k.call(Mercury::Fake.new(domain, **kws)) } # EM.next_tick is required to emulate the real Mercury.open
22
+ end
23
+ end
24
+ end
25
+
26
+ def initialize(domain=:default, **kws)
19
27
  @domain = Fake.domains[domain]
20
- @parallelism = parallelism
28
+ @parallelism = kws.fetch(:parallelism, 1)
29
+ ignored_keys = kws.keys - [:parallelism]
30
+ if ignored_keys.any?
31
+ $stderr.puts "Warning: Mercury::Fake::new is ignoring keyword arguments: #{ignored_keys.join(', ')}"
32
+ end
21
33
  end
22
34
 
23
35
  def self.domains
@@ -30,21 +42,22 @@ class Mercury
30
42
  end
31
43
 
32
44
  def publish(source_name, msg, tag: '', headers: {}, &k)
33
- assert_not_closed
45
+ guard_public(k)
34
46
  queues.values.select{|q| q.binds?(source_name, tag)}.each{|q| q.enqueue(roundtrip(msg), tag, headers)}
35
47
  ret(k)
36
48
  end
37
49
 
38
- def start_listener(source_name, handler, tag_filter: '#', &k)
50
+ def start_listener(source_name, handler, tag_filter: nil, &k)
39
51
  start_worker_or_listener(source_name, handler, tag_filter, &k)
40
52
  end
41
53
 
42
- def start_worker(worker_group, source_name, handler, tag_filter: '#', &k)
54
+ def start_worker(worker_group, source_name, handler, tag_filter: nil, &k)
43
55
  start_worker_or_listener(source_name, handler, tag_filter, worker_group, &k)
44
56
  end
45
57
 
46
58
  def start_worker_or_listener(source_name, handler, tag_filter, worker_group=nil, &k)
47
- assert_not_closed
59
+ guard_public(k)
60
+ tag_filter ||= '#'
48
61
  q = ensure_queue(source_name, tag_filter, !!worker_group, worker_group)
49
62
  ret(k) # it's important we show the "start" operation finishing before delivery starts (in add_subscriber)
50
63
  q.add_subscriber(Subscriber.new(handler, @parallelism))
@@ -52,23 +65,25 @@ class Mercury
52
65
  private :start_worker_or_listener
53
66
 
54
67
  def delete_source(source_name, &k)
55
- assert_not_closed
68
+ guard_public(k)
56
69
  queues.delete_if{|_k, v| v.source == source_name}
57
70
  ret(k)
58
71
  end
59
72
 
60
73
  def delete_work_queue(worker_group, &k)
61
- assert_not_closed
74
+ guard_public(k)
62
75
  queues.delete_if{|_k, v| v.worker == worker_group}
63
76
  ret(k)
64
77
  end
65
78
 
66
79
  def source_exists?(source, &k)
80
+ guard_public(k)
67
81
  built_in_sources = %w(direct topic fanout headers match rabbitmq.log rabbitmq.trace).map{|x| "amq.#{x}"}
68
82
  ret(k, (queues.values.map(&:source) + built_in_sources).include?(source))
69
83
  end
70
84
 
71
85
  def queue_exists?(worker, &k)
86
+ guard_public(k)
72
87
  ret(k, queues.values.map(&:worker).include?(worker))
73
88
  end
74
89
 
@@ -98,8 +113,8 @@ class Mercury
98
113
  [source, tag_filter, worker].join('^')
99
114
  end
100
115
 
101
- def assert_not_closed
102
- raise 'connection is closed' if @closed
116
+ def guard_public(k, initializing: false)
117
+ Mercury.guard_public(@closed, k, initializing: initializing)
103
118
  end
104
119
  end
105
120
  end
@@ -13,8 +13,13 @@ class Mercury
13
13
  end
14
14
 
15
15
  def close(&k)
16
- @amqp.close do
17
- k.call
16
+ if @amqp
17
+ @amqp.close do
18
+ @amqp = nil
19
+ k.call
20
+ end
21
+ else
22
+ EM.next_tick(&k)
18
23
  end
19
24
  end
20
25
 
@@ -26,16 +31,18 @@ class Mercury
26
31
  parallelism: 1,
27
32
  on_error: nil,
28
33
  wait_for_publisher_confirms: true,
29
- logger: logger,
34
+ logger:,
30
35
  &k)
36
+ guard_public(k, initializing: true)
31
37
  @logger = logger
32
38
  @on_error = on_error
33
39
  AMQP.connect(host: host, port: port, vhost: vhost, username: username, password: password,
34
40
  on_tcp_connection_failure: server_down_error_handler) do |amqp|
35
41
  @amqp = amqp
36
- @channel = AMQP::Channel.new(amqp, prefetch: parallelism) do
42
+ install_lost_connection_error_handler
43
+ AMQP::Channel.new(amqp, prefetch: parallelism) do |channel|
44
+ @channel = channel
37
45
  install_channel_error_handler
38
- install_lost_connection_error_handler
39
46
  if wait_for_publisher_confirms
40
47
  enable_publisher_confirms do
41
48
  k.call(self)
@@ -49,6 +56,7 @@ class Mercury
49
56
  private_class_method :new
50
57
 
51
58
  def publish(source_name, msg, tag: '', headers: {}, &k)
59
+ guard_public(k)
52
60
  # The amqp gem caches exchange objects, so it's fine to
53
61
  # redeclare the exchange every time we publish.
54
62
  with_source(source_name) do |exchange|
@@ -67,7 +75,8 @@ class Mercury
67
75
  { routing_key: tag, persistent: true, headers: Logatron.http_headers.merge(headers) }
68
76
  end
69
77
 
70
- def start_listener(source_name, handler, tag_filter: '#', &k)
78
+ def start_listener(source_name, handler, tag_filter: nil, &k)
79
+ guard_public(k)
71
80
  with_source(source_name) do |exchange|
72
81
  with_listener_queue(exchange, tag_filter) do |queue|
73
82
  queue.subscribe(ack: false) do |metadata, payload|
@@ -78,7 +87,8 @@ class Mercury
78
87
  end
79
88
  end
80
89
 
81
- def start_worker(worker_group, source_name, handler, tag_filter: '#', &k)
90
+ def start_worker(worker_group, source_name, handler, tag_filter: nil, &k)
91
+ guard_public(k)
82
92
  with_source(source_name) do |exchange|
83
93
  with_work_queue(worker_group, exchange, tag_filter) do |queue|
84
94
  queue.subscribe(ack: true) do |metadata, payload|
@@ -90,6 +100,7 @@ class Mercury
90
100
  end
91
101
 
92
102
  def delete_source(source_name, &k)
103
+ guard_public(k)
93
104
  with_source(source_name) do |exchange|
94
105
  exchange.delete do
95
106
  k.call
@@ -98,6 +109,7 @@ class Mercury
98
109
  end
99
110
 
100
111
  def delete_work_queue(worker_group, &k)
112
+ guard_public(k)
101
113
  @channel.queue(worker_group, work_queue_opts) do |queue|
102
114
  queue.delete do
103
115
  k.call
@@ -106,6 +118,7 @@ class Mercury
106
118
  end
107
119
 
108
120
  def source_exists?(source_name, &k)
121
+ guard_public(k)
109
122
  existence_check(k) do |ch, &ret|
110
123
  with_source_no_cache(ch, source_name, passive: true) do
111
124
  ret.call(true)
@@ -114,6 +127,7 @@ class Mercury
114
127
  end
115
128
 
116
129
  def queue_exists?(queue_name, &k)
130
+ guard_public(k)
117
131
  existence_check(k) do |ch, &ret|
118
132
  ch.queue(queue_name, passive: true) do
119
133
  ret.call(true)
@@ -213,9 +227,7 @@ class Mercury
213
227
  end
214
228
 
215
229
  def handle_channel_error(_ch, info)
216
- @amqp.close do
217
- make_error_handler("An error occurred: #{info.reply_code} - #{info.reply_text}").call
218
- end
230
+ make_error_handler("An error occurred: #{info.reply_code} - #{info.reply_text}").call
219
231
  end
220
232
 
221
233
  def make_error_handler(msg)
@@ -228,10 +240,12 @@ class Mercury
228
240
  current_exception = $!
229
241
  unless current_exception
230
242
  @logger.error(msg)
231
- if @on_error.respond_to?(:call)
232
- @on_error.call(msg)
233
- else
234
- raise msg
243
+ close do
244
+ if @on_error.respond_to?(:call)
245
+ @on_error.call(msg)
246
+ else
247
+ raise msg
248
+ end
235
249
  end
236
250
  end
237
251
  end
@@ -274,10 +288,24 @@ class Mercury
274
288
  end
275
289
 
276
290
  def bind_queue(exchange, queue_name, tag_filter, opts, &k)
291
+ tag_filter ||= '#'
277
292
  queue = @channel.queue(queue_name, opts)
278
293
  queue.bind(exchange, routing_key: tag_filter) do
279
294
  k.call(queue)
280
295
  end
281
296
  end
282
297
 
298
+ def guard_public(k, initializing: false)
299
+ Mercury.guard_public(@amqp.nil?, k, initializing: initializing)
300
+ end
301
+
302
+ def self.guard_public(is_closed, k, initializing: false)
303
+ if is_closed && !initializing
304
+ raise 'This mercury instance is defunct. Either it was purposely closed or an error occurred.'
305
+ end
306
+ unless k
307
+ raise 'A continuation block is required but none was provided.'
308
+ end
309
+ end
310
+
283
311
  end
@@ -1,3 +1,3 @@
1
1
  class Mercury
2
- VERSION = '0.3.0'
2
+ VERSION = '0.4.0'
3
3
  end
@@ -12,6 +12,9 @@ describe Cps do
12
12
  it 'CPS-transforms a non-CPS proc' do
13
13
  expect(Cps.lift{rand}.run).to be_a Numeric
14
14
  end
15
+ it 'raises an error if the block returns a Cps object' do
16
+ expect{lift{lift{'cps'}}.run}.to raise_error /returned a Cps/
17
+ end
15
18
  end
16
19
 
17
20
  describe '::run' do
@@ -30,12 +33,18 @@ describe Cps do
30
33
  it 'composes two Cps instances' do
31
34
  expect(lift{a}.and_then{|x| to_string(x)}.run).to eql a.to_s
32
35
  end
36
+ it 'raises an error if the block does not return a Cps object' do
37
+ expect{lift{a}.and_then{'not-cps'}.run}.to raise_error /did not return a Cps/
38
+ end
33
39
  end
34
40
 
35
41
  describe '#and_lift' do
36
42
  it 'composes a Cps instance with a normal proc' do
37
43
  expect(lift{a}.and_lift{|x| x.to_s}.run).to eql a.to_s
38
44
  end
45
+ it 'raises an error if the block returns a Cps object' do
46
+ expect{lift{a}.and_lift{lift{'cps'}}.run}.to raise_error /returned a Cps/
47
+ end
39
48
  end
40
49
 
41
50
  describe '::concurrently' do
@@ -29,7 +29,7 @@ describe Mercury do
29
29
  em do
30
30
  Mercury.open do |m|
31
31
  m.close do
32
- expect { m.publish(queue, {'a' => 1}) }.to raise_error /connection is closed/
32
+ expect { m.publish(queue, {'a' => 1}) }.to raise_error /closed/
33
33
  done
34
34
  end
35
35
  end
@@ -134,6 +134,34 @@ describe Mercury do
134
134
  end
135
135
  end
136
136
 
137
+ it 'raises when an error occurs' do
138
+ expect do
139
+ em do
140
+ Mercury.open do |m|
141
+ ch = m.instance_variable_get(:@channel)
142
+ ch.acknowledge(42) # force a channel error
143
+ end
144
+ end
145
+ end.to raise_error 'An error occurred: 406 - PRECONDITION_FAILED - unknown delivery tag 42'
146
+ end
147
+
148
+ it 'raises a helpful exception if used after a custom error handler suppresses an error' do
149
+ expect do
150
+ em do
151
+ handler = proc do
152
+ EM.next_tick do
153
+ @mercury.publish(source, 'hello')
154
+ end
155
+ end
156
+ Mercury.open(on_error: handler) do |m|
157
+ @mercury = m
158
+ ch = m.instance_variable_get(:@channel)
159
+ ch.acknowledge(42) # force a channel error
160
+ end
161
+ end
162
+ end.to raise_error /defunct/
163
+ end
164
+
137
165
  def start_rabbitmq_server
138
166
  Dir.chdir(File.expand_path('~/git/docker/local_orchestration')) do
139
167
  system('docker-compose start rabbitmq')
@@ -145,16 +145,24 @@ describe Mercury::Monadic do
145
145
  failures = []
146
146
  bars = []
147
147
  everything = []
148
+ everything2 = []
149
+ everything3 = []
150
+ all_msgs_received = proc do
151
+ successes.size == 2 && failures.size == 2 && bars.size == 2 &&
152
+ everything.size == 4 && everything2.size == 4 && everything3.size == 4
153
+ end
148
154
  seql do
149
155
  and_then { m.start_listener(source, tag_filter: '*.success', &successes.method(:push)) }
150
156
  and_then { m.start_listener(source, tag_filter: '*.failure', &failures.method(:push)) }
151
157
  and_then { m.start_listener(source, tag_filter: 'bar.*', &bars.method(:push)) }
152
158
  and_then { m.start_listener(source, tag_filter: '#', &everything.method(:push)) }
159
+ and_then { m.start_listener(source, tag_filter: nil, &everything2.method(:push)) }
160
+ and_then { m.start_worker(worker, source, tag_filter: nil) { |msg| everything3.push(msg); msg.ack } }
153
161
  and_then { m.publish(source, msg1, tag: 'foo.success') }
154
162
  and_then { m.publish(source, msg2, tag: 'foo.failure') }
155
163
  and_then { m.publish(source, msg3, tag: 'bar.success') }
156
164
  and_then { m.publish(source, msg4, tag: 'bar.failure') }
157
- and_then { wait_until { successes.size == 2 && failures.size == 2 && bars.size == 2 && everything.size == 4 } }
165
+ and_then { wait_until(&all_msgs_received) }
158
166
  and_lift do
159
167
  expect(successes[0].content).to eql(msg1)
160
168
  expect(successes[1].content).to eql(msg3)
@@ -166,6 +174,8 @@ describe Mercury::Monadic do
166
174
  expect(everything[1].content).to eql(msg2)
167
175
  expect(everything[2].content).to eql(msg3)
168
176
  expect(everything[3].content).to eql(msg4)
177
+ expect(everything2.map(&:content)).to eql(everything.map(&:content))
178
+ expect(everything3.map(&:content)).to eql(everything.map(&:content))
169
179
  end
170
180
  end
171
181
  end
@@ -273,17 +283,6 @@ describe Mercury::Monadic do
273
283
  end
274
284
  end
275
285
 
276
- it 'raises when an error occurs' do
277
- expect do
278
- em do
279
- Mercury.open do |m|
280
- ch = m.instance_variable_get(:@channel)
281
- ch.acknowledge(42) # force a channel error
282
- end
283
- end
284
- end.to raise_error 'An error occurred: 406 - PRECONDITION_FAILED - unknown delivery tag 42'
285
- end
286
-
287
286
  describe '#delete_source' do
288
287
  itt 'deletes the source if it exists' do
289
288
  test_with_mercury do |m|
data/spec/spec_helper.rb CHANGED
@@ -36,11 +36,7 @@ module MercuryFakeSpec
36
36
  it(name, &block)
37
37
  end
38
38
  context 'with Mercury::Fake' do
39
- before :each do
40
- allow(Mercury).to receive(:open) do |parallelism:1, &k|
41
- EM.next_tick { k.call(Mercury::Fake.new(parallelism: parallelism)) }
42
- end
43
- end
39
+ before(:each) { Mercury::Fake.install(self) }
44
40
  it(name, &block)
45
41
  end
46
42
  end
data/typical.dot ADDED
@@ -0,0 +1,17 @@
1
+ digraph G {
2
+ rankdir=LR
3
+ p1 [label="publisher", shape=rect]
4
+ p2 [label="publisher", shape=rect]
5
+ p3 [label="publisher", shape=rect]
6
+ s [label="source", shape=circle]
7
+ wq [label="{|||||}", shape=record, fixedsize=true, height=0.3, width=1.5]
8
+ w1 [label="worker A", shape=rect]
9
+ w2 [label="worker A", shape=rect]
10
+ w3 [label="worker A", shape=rect]
11
+
12
+ p1 -> s [tailport=e]
13
+ p2 -> s [tailport=e]
14
+ p3 -> s [tailport=e]
15
+ s -> wq [headport=w]
16
+ wq -> {w1, w2, w3} [tailport=e, headport=w]
17
+ }
data/typical.png ADDED
Binary file
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mercury_amqp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Winton
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-04-22 00:00:00.000000000 Z
11
+ date: 2016-04-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -190,6 +190,8 @@ files:
190
190
  - Gemfile
191
191
  - README.md
192
192
  - Rakefile
193
+ - constructs.dot
194
+ - constructs.png
193
195
  - lib/mercury.rb
194
196
  - lib/mercury/cps.rb
195
197
  - lib/mercury/cps/methods.rb
@@ -218,6 +220,8 @@ files:
218
220
  - spec/lib/mercury/utils_spec.rb
219
221
  - spec/lib/mercury/wire_serializer_spec.rb
220
222
  - spec/spec_helper.rb
223
+ - typical.dot
224
+ - typical.png
221
225
  homepage: https://github.com/wintonpc/mercury_amqp
222
226
  licenses:
223
227
  - MIT