mercury_amqp 0.3.0 → 0.4.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.
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