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 +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
|
+

|
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
|
+

|
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
|