mercury_amqp 0.1.9 → 0.2.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/Gemfile +0 -1
- data/lib/mercury/mercury.rb +80 -10
- data/lib/mercury/sync.rb +6 -3
- data/lib/mercury/test_utils.rb +2 -2
- data/lib/mercury/version.rb +1 -1
- data/spec/lib/mercury/mercury_spec.rb +77 -4
- data/spec/lib/mercury/monadic_spec.rb +12 -12
- data/spec/lib/mercury/sync_spec.rb +16 -13
- data/spec/spec_helper.rb +11 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2fd7b0dd970c7160af30546770764c5fc2725bc6
|
4
|
+
data.tar.gz: a702ad714aba8bd83308c19905841f0cec380430
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ef2d9c3392044008379014cfe5a12d22849c5f36a0852a3f36dccc6a6677f8b1d65f3c51143dacf9607077164abfb10874f369dc0f62246edc5a2b04fb5c757c
|
7
|
+
data.tar.gz: 687e95ee42e0e32aaf9290669ed11ba2e68f3b1b1acf43195990158a2724e285f708028e1ab43d20cfd5d5953c5bd1fbae4a98c75c31a33bc34f0c2123d64c98
|
data/Gemfile
CHANGED
data/lib/mercury/mercury.rb
CHANGED
@@ -26,16 +26,22 @@ class Mercury
|
|
26
26
|
password: 'guest',
|
27
27
|
parallelism: 1,
|
28
28
|
on_error: nil,
|
29
|
+
wait_for_publisher_confirms: true,
|
29
30
|
&k)
|
30
31
|
@on_error = on_error
|
31
32
|
AMQP.connect(host: host, port: port, vhost: vhost, username: username, password: password,
|
32
33
|
on_tcp_connection_failure: server_down_error_handler) do |amqp|
|
33
34
|
@amqp = amqp
|
34
35
|
@channel = AMQP::Channel.new(amqp, prefetch: parallelism) do
|
35
|
-
@channel.confirm_select
|
36
36
|
install_channel_error_handler
|
37
37
|
install_lost_connection_error_handler
|
38
|
-
|
38
|
+
if wait_for_publisher_confirms
|
39
|
+
enable_publisher_confirms do
|
40
|
+
k.call(self)
|
41
|
+
end
|
42
|
+
else
|
43
|
+
k.call(self)
|
44
|
+
end
|
39
45
|
end
|
40
46
|
end
|
41
47
|
end
|
@@ -44,10 +50,14 @@ class Mercury
|
|
44
50
|
def publish(source_name, msg, tag: '', headers: {}, &k)
|
45
51
|
# The amqp gem caches exchange objects, so it's fine to
|
46
52
|
# redeclare the exchange every time we publish.
|
47
|
-
# TODO: wait for publish confirmations (@channel.on_ack)
|
48
53
|
with_source(source_name) do |exchange|
|
49
|
-
|
50
|
-
|
54
|
+
payload = write(msg)
|
55
|
+
pub_opts = Mercury.publish_opts(tag, headers)
|
56
|
+
if publisher_confirms_enabled
|
57
|
+
expect_publisher_confirm(k)
|
58
|
+
exchange.publish(payload, **pub_opts)
|
59
|
+
else
|
60
|
+
exchange.publish(payload, **pub_opts, &k)
|
51
61
|
end
|
52
62
|
end
|
53
63
|
end
|
@@ -112,6 +122,58 @@ class Mercury
|
|
112
122
|
|
113
123
|
private
|
114
124
|
|
125
|
+
# In AMQP, queue consumers ack requests after handling them. Unacked messages
|
126
|
+
# are automatically returned to the queue, guaranteeing they are eventually handled.
|
127
|
+
# Services often ack one request while publishing related messages. Ideally, these
|
128
|
+
# operations would be transactional. If the ack succeeds but the publish does not,
|
129
|
+
# the line of processing is abandoned, resulting in processing getting "stuck".
|
130
|
+
# The best we can do in AMQP is to use "publisher confirms" to confirm that the publish
|
131
|
+
# succeeded before acking the originating request. Since the ack can still fail in this
|
132
|
+
# scenario, the system should employ idempotent design, which makes request redelivery
|
133
|
+
# harmless.
|
134
|
+
#
|
135
|
+
# see https://www.rabbitmq.com/confirms.html
|
136
|
+
# see http://rubyamqp.info/articles/durability/
|
137
|
+
def enable_publisher_confirms(&k)
|
138
|
+
@confirm_handlers = {}
|
139
|
+
@channel.confirm_select do
|
140
|
+
@last_published_delivery_tag = 0
|
141
|
+
@channel.on_ack do |basic_ack|
|
142
|
+
tag = basic_ack.delivery_tag
|
143
|
+
if @confirm_handlers.keys.exclude?(tag)
|
144
|
+
raise "Got an unexpected publish confirmation ACK for delivery-tag: #{tag}. Was expecting one of: #{@confirm_handlers.keys.inspect}"
|
145
|
+
end
|
146
|
+
dispatch_publisher_confirm(basic_ack)
|
147
|
+
end
|
148
|
+
@channel.on_nack do |basic_nack|
|
149
|
+
raise "Delivery failed for message with delivery-tag: #{basic_nack.delivery_tag}"
|
150
|
+
end
|
151
|
+
k.call
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def publisher_confirms_enabled
|
156
|
+
@confirm_handlers.is_a?(Hash)
|
157
|
+
end
|
158
|
+
|
159
|
+
def expect_publisher_confirm(k)
|
160
|
+
expected_delivery_tag = (@last_published_delivery_tag += 1)
|
161
|
+
@confirm_handlers[expected_delivery_tag] = k
|
162
|
+
expected_delivery_tag
|
163
|
+
end
|
164
|
+
|
165
|
+
def dispatch_publisher_confirm(basic_ack)
|
166
|
+
confirmed_tags =
|
167
|
+
if basic_ack.multiple
|
168
|
+
@confirm_handlers.keys.select { |tag| tag <= basic_ack.delivery_tag }.sort # sort just to be deterministic
|
169
|
+
else
|
170
|
+
[basic_ack.delivery_tag]
|
171
|
+
end
|
172
|
+
confirmed_tags.each do |tag|
|
173
|
+
@confirm_handlers.delete(tag).call
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
115
177
|
def make_received_message(payload, metadata, is_ackable)
|
116
178
|
msg = ReceivedMessage.new(read(payload), metadata, is_ackable: is_ackable)
|
117
179
|
Logatron.msg_id = msg.headers['X-Ascent-Log-Id']
|
@@ -157,11 +219,19 @@ class Mercury
|
|
157
219
|
|
158
220
|
def make_error_handler(msg)
|
159
221
|
proc do
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
222
|
+
# If an error is already being raised, don't interfere with it.
|
223
|
+
# This is actually essential since some versions of EventMachine (notably 1.2.0.1)
|
224
|
+
# fail to clean up properly if an error is raised during the `ensure` clean up
|
225
|
+
# phase (in EventMachine::run), which zombifies subsequent reactors. (AMQP connection
|
226
|
+
# failure handlers are invoked from EventMachine's `ensure`.)
|
227
|
+
current_exception = $!
|
228
|
+
unless current_exception
|
229
|
+
Logatron.error(msg)
|
230
|
+
if @on_error.respond_to?(:call)
|
231
|
+
@on_error.call(msg)
|
232
|
+
else
|
233
|
+
raise msg
|
234
|
+
end
|
165
235
|
end
|
166
236
|
end
|
167
237
|
end
|
data/lib/mercury/sync.rb
CHANGED
@@ -4,14 +4,17 @@ require 'bunny'
|
|
4
4
|
class Mercury
|
5
5
|
class Sync
|
6
6
|
class << self
|
7
|
-
def publish(source_name, msg, tag: '', amqp_opts: {})
|
7
|
+
def publish(source_name, msg, tag: '', amqp_opts: {}, wait_for_publisher_confirms: true)
|
8
8
|
conn = Bunny.new(amqp_opts)
|
9
9
|
conn.start
|
10
10
|
ch = conn.create_channel
|
11
|
-
|
11
|
+
|
12
|
+
ch.confirm_select if wait_for_publisher_confirms # see http://rubybunny.info/articles/extensions.html and Mercury#enable_publisher_confirms
|
12
13
|
ex = ch.topic(source_name, Mercury.source_opts)
|
13
14
|
ex.publish(WireSerializer.new.write(msg), **Mercury.publish_opts(tag, {}))
|
14
|
-
|
15
|
+
if wait_for_publisher_confirms
|
16
|
+
ch.wait_for_confirms or raise 'failed to confirm publication'
|
17
|
+
end
|
15
18
|
ensure
|
16
19
|
conn.close
|
17
20
|
end
|
data/lib/mercury/test_utils.rb
CHANGED
@@ -6,9 +6,9 @@ class Mercury
|
|
6
6
|
module TestUtils
|
7
7
|
include Cps::Methods
|
8
8
|
|
9
|
-
def em
|
9
|
+
def em(timeout_seconds: 3)
|
10
10
|
EM.run do
|
11
|
-
EM.add_timer(in_debug_mode? ? 999999 :
|
11
|
+
EM.add_timer(in_debug_mode? ? 999999 : timeout_seconds) { raise 'EM spec timed out' }
|
12
12
|
yield
|
13
13
|
end
|
14
14
|
end
|
data/lib/mercury/version.rb
CHANGED
@@ -75,12 +75,85 @@ describe Mercury do
|
|
75
75
|
end
|
76
76
|
end
|
77
77
|
|
78
|
-
|
78
|
+
# Commented out until this gets fixed: https://github.com/eventmachine/eventmachine/issues/670
|
79
|
+
# it 'raises an error when a connection cannot be established' do
|
80
|
+
# expect { em { Mercury.open(port: 31999) { done } } }.to raise_error /Failed to establish connection/
|
81
|
+
# expect(EM.reactor_running?).to be false
|
82
|
+
# end
|
83
|
+
|
84
|
+
it 'raises an error when the connection breaks' do
|
85
|
+
expect { em { Mercury.open { done } } }.to raise_error /Lost connection/
|
86
|
+
expect(EM.reactor_running?).to be false # make sure we're not triggering EventMachine cleanup bugs
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'does not obscure exceptions thrown inside the reactor' do
|
90
|
+
expect { em { Mercury.open { raise 'oops' } } }.to raise_error 'oops'
|
91
|
+
expect(EM.reactor_running?).to be false # make sure we're not triggering EventMachine cleanup bugs
|
92
|
+
end
|
93
|
+
|
94
|
+
describe '#publish' do
|
95
|
+
context 'docker assumptions' do
|
96
|
+
after(:each) { start_rabbitmq_server }
|
97
|
+
# This test is commented out because it make assumptions about, and manipulates, a local docker orchestration.
|
98
|
+
# Uncomment and run it in a suitable environment to do semi-automated testing on publisher confirms.
|
99
|
+
# it 'waits to invoke its continuation until after the message is confirmed' do
|
100
|
+
# got_confirmation = false
|
101
|
+
# expect {
|
102
|
+
# with_mercury(timeout_seconds: 15) do |m|
|
103
|
+
# m.publish(source, 'hello') do # cause `source` to be declared so subsequent publishes simply publish
|
104
|
+
# stop_rabbitmq_server
|
105
|
+
# m.publish(source, 'hello') do
|
106
|
+
# got_confirmation = true
|
107
|
+
# m.close { done }
|
108
|
+
# end
|
109
|
+
# end
|
110
|
+
# end
|
111
|
+
# }.to raise_error /Lost connection to AMQP server/
|
112
|
+
# expect(got_confirmation).to be false
|
113
|
+
# end
|
114
|
+
end
|
115
|
+
it 'allows multiple outstanding confirmations' do
|
116
|
+
log = []
|
117
|
+
with_mercury do |m|
|
118
|
+
m.publish(source, 'hello') do # cause `source` to be declared so subsequent publishes simply publish
|
119
|
+
publish_and_confirm(m, 'a', log)
|
120
|
+
publish_and_confirm(m, 'b', log)
|
121
|
+
em_wait_until(proc { log.size == 4 }) do
|
122
|
+
expect(log).to eql ['publish a', 'publish b', 'confirm a', 'confirm b']
|
123
|
+
m.close { done }
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def publish_and_confirm(m, name, log)
|
130
|
+
m.publish(source, name) do
|
131
|
+
log << "confirm #{name}"
|
132
|
+
end
|
133
|
+
log << "publish #{name}"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def start_rabbitmq_server
|
138
|
+
Dir.chdir(File.expand_path('~/git/docker/local_orchestration')) do
|
139
|
+
system('docker-compose start rabbitmq')
|
140
|
+
end
|
141
|
+
puts 'done.'
|
142
|
+
end
|
143
|
+
|
144
|
+
def stop_rabbitmq_server
|
145
|
+
Dir.chdir(File.expand_path('~/git/docker/local_orchestration')) do
|
146
|
+
system('docker-compose stop rabbitmq')
|
147
|
+
end
|
148
|
+
puts 'done.'
|
149
|
+
end
|
150
|
+
|
151
|
+
def with_mercury(timeout_seconds: 3, &block)
|
79
152
|
sources = [source]
|
80
153
|
queues = [queue]
|
81
|
-
em { delete_sources_and_queues_cps(sources, queues).run{done} }
|
82
|
-
em { Mercury.open(&block) }
|
83
|
-
em { delete_sources_and_queues_cps(sources, queues).run{done} }
|
154
|
+
em(timeout_seconds: timeout_seconds) { delete_sources_and_queues_cps(sources, queues).run{done} }
|
155
|
+
em(timeout_seconds: timeout_seconds) { Mercury.open(&block) }
|
156
|
+
em(timeout_seconds: timeout_seconds) { delete_sources_and_queues_cps(sources, queues).run{done} }
|
84
157
|
end
|
85
158
|
|
86
159
|
end
|
@@ -78,8 +78,8 @@ describe Mercury::Monadic do
|
|
78
78
|
end
|
79
79
|
seql do
|
80
80
|
and_then { m.start_worker('worker1', source1, handle_msg) }
|
81
|
-
and_then { m.publish(source1, {'id' => 1, 'sleep_seconds' => 0.
|
82
|
-
and_then { m.publish(source1, {'id' => 2, 'sleep_seconds' => 0.
|
81
|
+
and_then { m.publish(source1, {'id' => 1, 'sleep_seconds' => 0.1}) }
|
82
|
+
and_then { m.publish(source1, {'id' => 2, 'sleep_seconds' => 0.1}) }
|
83
83
|
and_then { wait_until { events.size == 4 } }
|
84
84
|
and_lift do
|
85
85
|
expect(events).to eql ['received 1', 'received 2', 'finished 1', 'finished 2']
|
@@ -128,8 +128,8 @@ describe Mercury::Monadic do
|
|
128
128
|
msgs = []
|
129
129
|
seql do
|
130
130
|
and_then { m.start_listener(source, &msgs.method(:push)) }
|
131
|
+
and_lift { EM.next_tick { Logatron.msg_id = 'fake_msg_id' } } # we want this to happen right after publishing but before getting the response
|
131
132
|
and_then { m.publish(source, msg) }
|
132
|
-
and_lift { Logatron.msg_id = 'fake_msg_id' }
|
133
133
|
and_then { wait_until { msgs.size == 1 } }
|
134
134
|
and_lift do
|
135
135
|
expect(msgs[0].headers['X-Ascent-Log-Id']).to eql real_msg_id
|
@@ -188,14 +188,21 @@ describe Mercury::Monadic do
|
|
188
188
|
end
|
189
189
|
end
|
190
190
|
|
191
|
+
def push_and_ack(array)
|
192
|
+
proc do |msg|
|
193
|
+
array.push(msg)
|
194
|
+
msg.ack
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
191
198
|
itt 'workers can specify tag filters' do
|
192
199
|
test_with_mercury do |m|
|
193
200
|
seql do
|
194
201
|
let(:m2) { Mercury::Monadic.open }
|
195
202
|
work1 = []
|
196
203
|
work2 = []
|
197
|
-
and_then { m.start_worker(worker, source, tag_filter: 'success', &
|
198
|
-
and_then { m2.start_worker(worker, source, tag_filter: 'failure', &
|
204
|
+
and_then { m.start_worker(worker, source, tag_filter: 'success', &work1.method(:push)) }
|
205
|
+
and_then { m2.start_worker(worker, source, tag_filter: 'failure', &work2.method(:push)) }
|
199
206
|
and_then { m.publish(source, msg1, tag: 'success') }
|
200
207
|
and_then { m.publish(source, msg2, tag: 'failure') }
|
201
208
|
and_then { wait_until { work1.size == 1 && work2.size == 1 } }
|
@@ -208,13 +215,6 @@ describe Mercury::Monadic do
|
|
208
215
|
end
|
209
216
|
end
|
210
217
|
|
211
|
-
def push_and_ack(array)
|
212
|
-
proc do |msg|
|
213
|
-
array.push(msg)
|
214
|
-
msg.ack
|
215
|
-
end
|
216
|
-
end
|
217
|
-
|
218
218
|
itt 'a worker must ack before receiving another message' do
|
219
219
|
test_with_mercury do |m|
|
220
220
|
msgs = []
|
@@ -7,17 +7,20 @@ describe Mercury::Sync do
|
|
7
7
|
let!(:source) { 'test-exchange1' }
|
8
8
|
let!(:queue) { 'test-queue1' }
|
9
9
|
describe '::publish' do
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
10
|
+
%w{with without}.each do |w|
|
11
|
+
it "publishes synchronously (#{w} publisher confirms)" do
|
12
|
+
use_publisher_confirms = w == 'with'
|
13
|
+
sent = {'a' => 1}
|
14
|
+
received = []
|
15
|
+
test_with_mercury(wait_for_publisher_confirms: use_publisher_confirms) do |m|
|
16
|
+
seql do
|
17
|
+
and_then { m.start_listener(source, received.method(:push)) }
|
18
|
+
and_lift { Mercury::Sync.publish(source, sent) }
|
19
|
+
and_then { wait_until { received.any? } }
|
20
|
+
and_lift do
|
21
|
+
expect(received.size).to eql 1
|
22
|
+
expect(received[0].content).to eql sent
|
23
|
+
end
|
21
24
|
end
|
22
25
|
end
|
23
26
|
end
|
@@ -25,9 +28,9 @@ describe Mercury::Sync do
|
|
25
28
|
end
|
26
29
|
|
27
30
|
# the block must return a Cps
|
28
|
-
def test_with_mercury(&block)
|
31
|
+
def test_with_mercury(**kws, &block)
|
29
32
|
sources = [source]
|
30
33
|
queues = [queue]
|
31
|
-
test_with_mercury_cps(sources, queues, &block)
|
34
|
+
test_with_mercury_cps(sources, queues, **kws, &block)
|
32
35
|
end
|
33
36
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -4,10 +4,10 @@ require 'mercury/fake'
|
|
4
4
|
include Mercury::TestUtils
|
5
5
|
|
6
6
|
# the block must return a Cps
|
7
|
-
def test_with_mercury_cps(sources, queues,
|
7
|
+
def test_with_mercury_cps(sources, queues, **kws, &block)
|
8
8
|
em do
|
9
9
|
seql do
|
10
|
-
let(:m) { Mercury::Monadic.open(
|
10
|
+
let(:m) { Mercury::Monadic.open(**kws) }
|
11
11
|
and_then { delete_sources_and_queues_cps(sources, queues) }
|
12
12
|
and_then { block.call(m) }
|
13
13
|
and_then { delete_sources_and_queues_cps(sources, queues) }
|
@@ -26,6 +26,15 @@ module MercuryFakeSpec
|
|
26
26
|
# runs a test once with real mercury and once with Mercury::Fake
|
27
27
|
def itt(name, &block)
|
28
28
|
it(name, &block)
|
29
|
+
context 'without publisher confirms' do
|
30
|
+
before :each do
|
31
|
+
real_open = Mercury.method(:open)
|
32
|
+
allow(Mercury).to receive(:open) do |**kws, &k|
|
33
|
+
real_open.call(**kws.merge(wait_for_publisher_confirms: false), &k)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
it(name, &block)
|
37
|
+
end
|
29
38
|
context 'with Mercury::Fake' do
|
30
39
|
before :each do
|
31
40
|
allow(Mercury).to receive(:open) do |parallelism:1, &k|
|
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.2.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-
|
11
|
+
date: 2016-04-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|