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 +4 -4
- data/.gitignore +2 -2
- data/README.md +152 -19
- data/constructs.dot +24 -0
- data/constructs.png +0 -0
- data/lib/mercury/cps.rb +9 -2
- data/lib/mercury/fake.rb +25 -10
- data/lib/mercury/mercury.rb +42 -14
- data/lib/mercury/version.rb +1 -1
- data/spec/lib/mercury/cps_spec.rb +9 -0
- data/spec/lib/mercury/mercury_spec.rb +29 -1
- data/spec/lib/mercury/monadic_spec.rb +11 -12
- data/spec/spec_helper.rb +1 -5
- data/typical.dot +17 -0
- data/typical.png +0 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 459b852ab72eefefec630bac89fde701f7fc2018
|
4
|
+
data.tar.gz: a0ae43b454314876e4dc1be0e6ccf9480d8a9ed6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 296ef00ec39d0419c4f55b185528a4d0ed905d236014ad0f8a432a310a01033cf4105c6c2c19a82d02cec850c4de3e8af2a96eba8f0e6d8f4761002a1ef971df
|
7
|
+
data.tar.gz: 7fcbe0aa189f7d5924312210ddaf7eb1fb6111ff0f4f74e38fddc1a7410f26a8820fd871cac8daaf0c93b363bee153cb61b1b85ca4ee760bc8c6240048758ca3
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -1,26 +1,140 @@
|
|
1
1
|
mercury
|
2
2
|
=======
|
3
3
|
|
4
|
-
Mercury is a messaging layer
|
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
|
-
-
|
12
|
-
-
|
13
|
-
-
|
14
|
-
-
|
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
|
-
|
17
|
-
JSON. In the future, additional transports and message formats may be
|
18
|
-
supported.
|
13
|
+
### Constructs
|
19
14
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
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
|
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
|
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
|
-
|
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:
|
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:
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
102
|
-
|
116
|
+
def guard_public(k, initializing: false)
|
117
|
+
Mercury.guard_public(@closed, k, initializing: initializing)
|
103
118
|
end
|
104
119
|
end
|
105
120
|
end
|
data/lib/mercury/mercury.rb
CHANGED
@@ -13,8 +13,13 @@ class Mercury
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def close(&k)
|
16
|
-
@amqp
|
17
|
-
|
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
|
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
|
-
|
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:
|
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:
|
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
|
-
|
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
|
-
|
232
|
-
@on_error.call
|
233
|
-
|
234
|
-
|
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
|
data/lib/mercury/version.rb
CHANGED
@@ -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 /
|
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
|
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
|
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.
|
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-
|
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
|