msgr 1.2.0 → 1.3.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/.editorconfig +8 -0
- data/.github/workflows/build.yml +52 -0
- data/.github/workflows/lint.yml +20 -0
- data/.rubocop.yml +9 -48
- data/.travis.yml +21 -35
- data/Appraisals +18 -0
- data/CHANGELOG.md +11 -1
- data/Gemfile +8 -15
- data/README.md +8 -20
- data/Rakefile +5 -5
- data/bin/msgr +1 -0
- data/gemfiles/rails_5.2.gemfile +14 -0
- data/gemfiles/rails_6.0.gemfile +14 -0
- data/gemfiles/rails_6.1.gemfile +14 -0
- data/gemfiles/rails_master.gemfile +14 -0
- data/lib/msgr.rb +1 -0
- data/lib/msgr/binding.rb +13 -8
- data/lib/msgr/channel.rb +5 -3
- data/lib/msgr/cli.rb +18 -11
- data/lib/msgr/client.rb +17 -20
- data/lib/msgr/connection.rb +13 -1
- data/lib/msgr/consumer.rb +2 -3
- data/lib/msgr/dispatcher.rb +7 -9
- data/lib/msgr/logging.rb +2 -0
- data/lib/msgr/message.rb +1 -2
- data/lib/msgr/railtie.rb +14 -69
- data/lib/msgr/route.rb +1 -4
- data/lib/msgr/routes.rb +2 -0
- data/lib/msgr/tasks/msgr/drain.rake +11 -0
- data/lib/msgr/test_pool.rb +1 -3
- data/lib/msgr/version.rb +1 -1
- data/msgr.gemspec +2 -6
- data/scripts/simple_test.rb +2 -3
- data/spec/fixtures/{msgr-routes-test-1.rb → msgr_routes_test_1.rb} +0 -0
- data/spec/integration/dummy/Rakefile +1 -1
- data/spec/{msgr/support/.keep → integration/dummy/app/assets/config/manifest.js} +0 -0
- data/spec/integration/dummy/bin/bundle +1 -1
- data/spec/integration/dummy/bin/rails +1 -1
- data/spec/integration/dummy/config/application.rb +1 -1
- data/spec/integration/dummy/config/boot.rb +2 -2
- data/spec/integration/dummy/config/environment.rb +1 -1
- data/spec/integration/dummy/config/rabbitmq.yml +1 -1
- data/spec/integration/msgr/dispatcher_spec.rb +28 -12
- data/spec/integration/msgr/railtie_spec.rb +10 -120
- data/spec/integration/spec_helper.rb +2 -3
- data/spec/integration/{msgr_spec.rb → test_controller_spec.rb} +1 -1
- data/spec/unit/msgr/client_spec.rb +88 -0
- data/spec/{msgr → unit}/msgr/connection_spec.rb +1 -1
- data/spec/{msgr → unit}/msgr/consumer_spec.rb +0 -0
- data/spec/unit/msgr/dispatcher_spec.rb +45 -0
- data/spec/{msgr → unit}/msgr/route_spec.rb +15 -14
- data/spec/{msgr → unit}/msgr/routes_spec.rb +32 -35
- data/spec/{msgr → unit}/msgr_spec.rb +25 -16
- data/spec/{msgr → unit}/spec_helper.rb +1 -1
- data/spec/unit/support/.keep +0 -0
- metadata +37 -33
- data/gemfiles/Gemfile.rails-4-2 +0 -7
- data/gemfiles/Gemfile.rails-5-0 +0 -7
- data/gemfiles/Gemfile.rails-5-1 +0 -7
- data/gemfiles/Gemfile.rails-5-2 +0 -7
- data/gemfiles/Gemfile.rails-master +0 -14
- data/spec/msgr/msgr/client_spec.rb +0 -60
- data/spec/msgr/msgr/dispatcher_spec.rb +0 -44
- data/spec/support/setup.rb +0 -29
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Msgr::Client do
|
6
|
+
subject(:client) { described_class.new config }
|
7
|
+
|
8
|
+
let(:config) { {} }
|
9
|
+
|
10
|
+
describe '#uri' do
|
11
|
+
subject(:uri) { client.uri.to_s }
|
12
|
+
|
13
|
+
context 'with default config' do
|
14
|
+
it 'uses the default config' do
|
15
|
+
expect(uri).to eq 'amqp://127.0.0.1'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'without vhost' do
|
20
|
+
let(:config) { {uri: 'amqp://rabbit'} }
|
21
|
+
|
22
|
+
it 'does not specify a vhost' do
|
23
|
+
expect(uri).to eq 'amqp://rabbit'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'with empty vhost' do
|
28
|
+
let(:config) { {uri: 'amqp://rabbit/'} }
|
29
|
+
|
30
|
+
it 'does not specify a vhost' do
|
31
|
+
expect(uri).to eq 'amqp://rabbit'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'with explicit vhost' do
|
36
|
+
let(:config) { {uri: 'amqp://rabbit/some_vhost'} }
|
37
|
+
|
38
|
+
# This behavior is due to legacy parsing in Msgr's config.
|
39
|
+
# We interpret the entire path (incl. the leading slash)
|
40
|
+
# as vhost. As per AMQP rules, this means the leading slash
|
41
|
+
# is part of the vhost, which means it has to be URL encoded.
|
42
|
+
# This will likely change with the next major release.
|
43
|
+
it 'uses the entire path as vhost' do
|
44
|
+
expect(uri).to eq 'amqp://rabbit/%2Fsome_vhost'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'with URI and vhost' do
|
49
|
+
let(:config) { {uri: 'amqp://rabbit/some_vhost', vhost: 'real_vhost'} }
|
50
|
+
|
51
|
+
# This is currently the only way to specify a vhost without
|
52
|
+
# leading slash (as a vhost in the :uri config would have
|
53
|
+
# an extra URL encoded leading slash).
|
54
|
+
it 'uses the explicit vhost' do
|
55
|
+
expect(uri).to eq 'amqp://rabbit/real_vhost'
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe 'drain' do
|
61
|
+
subject(:drain) { client.drain }
|
62
|
+
|
63
|
+
let(:channel_stub) { instance_double('Msgr::Channel', prefetch: true) }
|
64
|
+
let(:queue_stub) { instance_double('Bunny::Queue', purge: true) }
|
65
|
+
|
66
|
+
before do
|
67
|
+
client.routes.configure do
|
68
|
+
route 'abc', to: 'consumer1#action1'
|
69
|
+
route 'def', to: 'consumer1#action2'
|
70
|
+
route 'ghi', to: 'consumer2#action1'
|
71
|
+
end
|
72
|
+
|
73
|
+
allow(Msgr::Channel).to receive(:new).and_return(channel_stub)
|
74
|
+
allow(channel_stub).to receive(:queue).and_return(queue_stub).at_most(3).times
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'requests purges for all configured routes' do
|
78
|
+
drain
|
79
|
+
|
80
|
+
expect(Msgr::Channel).to have_received(:new).exactly(3).times
|
81
|
+
expect(channel_stub).to have_received(:queue).with('msgr.consumer.Consumer1Consumer.action1', passive: true).once
|
82
|
+
expect(channel_stub).to have_received(:queue).with('msgr.consumer.Consumer1Consumer.action2', passive: true).once
|
83
|
+
expect(channel_stub).to have_received(:queue).with('msgr.consumer.Consumer2Consumer.action1', passive: true).once
|
84
|
+
|
85
|
+
expect(queue_stub).to have_received(:purge).exactly(3).times
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -6,7 +6,7 @@ describe Msgr::Connection do
|
|
6
6
|
describe '#rebind' do
|
7
7
|
let(:conn) { double }
|
8
8
|
let(:routes) { Msgr::Routes.new }
|
9
|
-
let(:connection) {
|
9
|
+
let(:connection) { described_class.new conn, routes, dispatcher }
|
10
10
|
|
11
11
|
pending 'some tests missing -> only lets written'
|
12
12
|
end
|
File without changes
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
class MsgrAutoAckConsumer < Msgr::Consumer
|
6
|
+
self.auto_ack = true
|
7
|
+
|
8
|
+
def index; end
|
9
|
+
end
|
10
|
+
|
11
|
+
class MsgrManualAckConsumer < Msgr::Consumer
|
12
|
+
self.auto_ack = false
|
13
|
+
|
14
|
+
def index; end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe Msgr::Dispatcher do
|
18
|
+
subject { dispatcher }
|
19
|
+
|
20
|
+
let(:config) { {} }
|
21
|
+
let(:args) { [config] }
|
22
|
+
let(:dispatcher) { described_class.new(*args) }
|
23
|
+
|
24
|
+
describe 'dispatch' do
|
25
|
+
it 'acks messages automatically if auto_ack is enabled' do
|
26
|
+
route_db = instance_double('Msgr::Route', consumer: 'MsgrAutoAckConsumer', action: :index)
|
27
|
+
msg_db = instance_spy('Msgr::Message', route: route_db, acked?: false)
|
28
|
+
|
29
|
+
dispatcher.dispatch(msg_db)
|
30
|
+
|
31
|
+
expect(msg_db).to have_received(:ack)
|
32
|
+
expect(msg_db).not_to have_received(:nack)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'does not ack messages if auto_ack is disabled' do
|
36
|
+
route_db = instance_double('Msgr::Route', consumer: 'MsgrManualAckConsumer', action: :index)
|
37
|
+
msg_db = instance_spy('Msgr::Message', route: route_db, acked?: false)
|
38
|
+
|
39
|
+
dispatcher.dispatch(msg_db)
|
40
|
+
|
41
|
+
expect(msg_db).not_to have_received(:ack)
|
42
|
+
expect(msg_db).not_to have_received(:nack)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -3,61 +3,62 @@
|
|
3
3
|
require 'spec_helper'
|
4
4
|
|
5
5
|
describe Msgr::Route do
|
6
|
+
subject { route }
|
7
|
+
|
6
8
|
let(:routing_key) { 'routing.key.#' }
|
7
9
|
let(:options) { {to: 'test#index'} }
|
8
10
|
let(:args) { [routing_key, options] }
|
9
|
-
let(:route) {
|
10
|
-
subject { route }
|
11
|
+
let(:route) { described_class.new(*args) }
|
11
12
|
|
12
13
|
describe '#initialize' do
|
13
|
-
it '
|
14
|
+
it 'requires `to` option' do
|
14
15
|
expect do
|
15
|
-
|
16
|
+
described_class.new(routing_key, {})
|
16
17
|
end.to raise_error(ArgumentError)
|
17
18
|
end
|
18
19
|
|
19
|
-
it '
|
20
|
+
it 'requires routing_key' do
|
20
21
|
expect do
|
21
|
-
|
22
|
+
described_class.new nil, options
|
22
23
|
end.to raise_error(ArgumentError)
|
23
24
|
end
|
24
25
|
|
25
|
-
it '
|
26
|
+
it 'requires not empty routing_key' do
|
26
27
|
expect do
|
27
|
-
|
28
|
+
described_class.new '', options
|
28
29
|
end.to raise_error ArgumentError, /routing key required/i
|
29
30
|
end
|
30
31
|
|
31
|
-
it '
|
32
|
+
it 'requires `to: "consumer#action` format' do
|
32
33
|
expect do
|
33
|
-
|
34
|
+
described_class.new routing_key, to: 'abc'
|
34
35
|
end.to raise_error ArgumentError, /invalid consumer format/i
|
35
36
|
end
|
36
37
|
end
|
37
38
|
|
38
39
|
describe '#consumer' do
|
39
|
-
it '
|
40
|
+
it 'returns consumer class name' do
|
40
41
|
expect(route.consumer).to eq 'TestConsumer'
|
41
42
|
end
|
42
43
|
|
43
44
|
context 'with underscore consumer name' do
|
44
45
|
let(:options) { super().merge to: 'test_resource_foo#index' }
|
45
46
|
|
46
|
-
it '
|
47
|
+
it 'returns camelized method name' do
|
47
48
|
expect(route.consumer).to eq 'TestResourceFooConsumer'
|
48
49
|
end
|
49
50
|
end
|
50
51
|
end
|
51
52
|
|
52
53
|
describe '#action' do
|
53
|
-
it '
|
54
|
+
it 'returns action method name' do
|
54
55
|
expect(route.action).to eq 'index'
|
55
56
|
end
|
56
57
|
|
57
58
|
context 'with camelCase action name' do
|
58
59
|
let(:options) { super().merge to: 'test#myActionMethod' }
|
59
60
|
|
60
|
-
it '
|
61
|
+
it 'returns underscore method name' do
|
61
62
|
expect(route.action).to eq 'my_action_method'
|
62
63
|
end
|
63
64
|
end
|
@@ -3,25 +3,15 @@
|
|
3
3
|
require 'spec_helper'
|
4
4
|
|
5
5
|
describe Msgr::Routes do
|
6
|
-
let(:routes) {
|
6
|
+
let(:routes) { described_class.new }
|
7
7
|
|
8
8
|
describe '#configure' do
|
9
|
-
|
10
|
-
|
11
|
-
it 'should evaluate given block within instance context' do
|
12
|
-
expect(routes).to receive(:instance_eval) do |&p|
|
13
|
-
expect(p).to be block
|
14
|
-
end
|
15
|
-
|
16
|
-
routes.configure(&block)
|
17
|
-
end
|
18
|
-
|
19
|
-
it 'should allow to call instance method in gven block' do
|
20
|
-
expect(routes).to receive(:test_instance_method).with(:abc)
|
21
|
-
|
9
|
+
it 'allows to configure the instance in given block' do
|
22
10
|
routes.configure do
|
23
|
-
|
11
|
+
route 'abc', to: 'consumer#action'
|
24
12
|
end
|
13
|
+
|
14
|
+
expect(routes.routes.first.keys).to eq ['abc']
|
25
15
|
end
|
26
16
|
end
|
27
17
|
|
@@ -35,7 +25,7 @@ describe Msgr::Routes do
|
|
35
25
|
|
36
26
|
let(:each) { routes.each }
|
37
27
|
|
38
|
-
it '
|
28
|
+
it 'iterates over configured routes' do
|
39
29
|
expect(each.size).to eq 2
|
40
30
|
|
41
31
|
expect(each.map(&:keys)).to eq [%w[abc.#], %w[edf.#]]
|
@@ -45,15 +35,16 @@ describe Msgr::Routes do
|
|
45
35
|
end
|
46
36
|
|
47
37
|
describe '#route' do
|
48
|
-
|
38
|
+
subject(:route) { -> { routes.route 'routing.key', to: 'test2#index2' } }
|
39
|
+
|
49
40
|
let(:last_route) { routes.routes.last }
|
50
41
|
|
51
|
-
it '
|
52
|
-
expect {
|
42
|
+
it 'adds a new route' do
|
43
|
+
expect { route.call }.to change { routes.routes.size }.from(0).to(1)
|
53
44
|
end
|
54
45
|
|
55
|
-
it '
|
56
|
-
|
46
|
+
it 'adds given route' do
|
47
|
+
route.call
|
57
48
|
|
58
49
|
expect(last_route.keys).to eq %w[routing.key]
|
59
50
|
expect(last_route.consumer).to eq 'Test2Consumer'
|
@@ -61,26 +52,26 @@ describe Msgr::Routes do
|
|
61
52
|
end
|
62
53
|
|
63
54
|
context 'with same target' do
|
64
|
-
|
55
|
+
subject(:route) do
|
65
56
|
lambda do
|
66
57
|
routes.route 'routing.key', to: 'test#index'
|
67
58
|
routes.route 'another.routing.key', to: 'test#index'
|
68
59
|
end
|
69
60
|
end
|
70
61
|
|
71
|
-
it '
|
72
|
-
expect {
|
62
|
+
it 'onlies add one new route' do
|
63
|
+
expect { route.call }.to change { routes.routes.size }.from(0).to(1)
|
73
64
|
end
|
74
65
|
|
75
|
-
it '
|
76
|
-
|
66
|
+
it 'adds second binding to first route' do
|
67
|
+
route.call
|
77
68
|
expect(routes.routes.first.keys).to eq %w[routing.key another.routing.key]
|
78
69
|
end
|
79
70
|
end
|
80
71
|
end
|
81
72
|
|
82
73
|
describe '#files' do
|
83
|
-
it '
|
74
|
+
it 'allows to add route paths' do
|
84
75
|
routes.files << 'abc.rb'
|
85
76
|
routes.files += %w[cde.rb edf.rb]
|
86
77
|
|
@@ -89,16 +80,20 @@ describe Msgr::Routes do
|
|
89
80
|
end
|
90
81
|
|
91
82
|
describe 'reload' do
|
92
|
-
before
|
83
|
+
before do
|
84
|
+
allow(File).to receive(:exist?).and_return(true)
|
85
|
+
allow(routes).to receive(:load)
|
86
|
+
end
|
93
87
|
|
94
|
-
it '
|
95
|
-
expect(routes).to receive(:load).with('cde.rb').ordered
|
96
|
-
expect(routes).to receive(:load).with('edf.rb').ordered
|
88
|
+
it 'triggers load for all files' do
|
97
89
|
routes.files += %w[cde.rb edf.rb]
|
98
90
|
routes.reload
|
91
|
+
|
92
|
+
expect(routes).to have_received(:load).with('cde.rb').ordered
|
93
|
+
expect(routes).to have_received(:load).with('edf.rb').ordered
|
99
94
|
end
|
100
95
|
|
101
|
-
it '
|
96
|
+
it 'clears old routes before reloading' do
|
102
97
|
routes.route 'abc', to: 'abc#test'
|
103
98
|
routes.reload
|
104
99
|
expect(routes.each.size).to eq 0
|
@@ -106,11 +101,13 @@ describe Msgr::Routes do
|
|
106
101
|
end
|
107
102
|
|
108
103
|
describe 'load' do
|
109
|
-
let(:file) { 'spec/fixtures/
|
104
|
+
let(:file) { 'spec/fixtures/msgr_routes_test_1.rb' }
|
110
105
|
|
111
|
-
it '
|
112
|
-
expect(routes).to receive(:route).with('abc.#', to: 'test#index')
|
106
|
+
it 'evals given file within routes context' do
|
113
107
|
routes.load file
|
108
|
+
expect(routes.routes.count).to eq 1
|
109
|
+
expect(routes.routes.first.keys).to eq ['abc.#']
|
110
|
+
expect(routes.routes.first.name).to eq 'msgr.consumer.TestConsumer.index'
|
114
111
|
end
|
115
112
|
end
|
116
113
|
end
|
@@ -5,7 +5,6 @@ require 'spec_helper'
|
|
5
5
|
class Receiver
|
6
6
|
end
|
7
7
|
|
8
|
-
#
|
9
8
|
class MsgrTestConsumer < Msgr::Consumer
|
10
9
|
def index
|
11
10
|
Receiver.index
|
@@ -26,7 +25,7 @@ end
|
|
26
25
|
|
27
26
|
describe Msgr do
|
28
27
|
let(:queue) { Queue.new }
|
29
|
-
let(:client) { Msgr::Client.new size: 1, prefix: SecureRandom.hex(2) }
|
28
|
+
let(:client) { Msgr::Client.new size: 1, prefix: SecureRandom.hex(2), uri: ENV['AMQP_SERVER'] }
|
30
29
|
|
31
30
|
before do
|
32
31
|
client.routes.configure do
|
@@ -36,43 +35,51 @@ describe Msgr do
|
|
36
35
|
end
|
37
36
|
|
38
37
|
client.start
|
38
|
+
|
39
|
+
allow(Receiver).to receive(:index) { queue << :end }
|
40
|
+
allow(Receiver).to receive(:batch) {|msg| queue << msg }
|
41
|
+
|
42
|
+
error_count = 0
|
43
|
+
allow(Receiver).to receive(:error) do
|
44
|
+
error_count += 1
|
45
|
+
raise 'error' if error_count == 1
|
46
|
+
|
47
|
+
queue << :end
|
48
|
+
end
|
39
49
|
end
|
40
50
|
|
41
51
|
after do
|
42
52
|
client.stop delete: true
|
43
53
|
end
|
44
54
|
|
45
|
-
it '
|
46
|
-
expect(Receiver).to receive(:index) { queue << :end }
|
47
|
-
|
55
|
+
it 'dispatches published methods to consumer' do
|
48
56
|
client.publish 'Payload', to: 'test.index'
|
49
57
|
|
50
58
|
Timeout.timeout(4) { queue.pop }
|
51
|
-
end
|
52
59
|
|
53
|
-
|
54
|
-
|
55
|
-
expect(Receiver).to receive(:error).ordered { queue << :end }
|
60
|
+
expect(Receiver).to have_received(:index).exactly(1).time
|
61
|
+
end
|
56
62
|
|
63
|
+
it 'redelivers failed messages' do
|
57
64
|
client.publish 'Payload', to: 'test.error'
|
58
65
|
|
59
66
|
Timeout.timeout(4) { queue.pop }
|
60
|
-
end
|
61
67
|
|
62
|
-
|
63
|
-
|
68
|
+
expect(Receiver).to have_received(:error).exactly(2).times
|
69
|
+
end
|
64
70
|
|
71
|
+
it 'receives 2 messages when prefetch is set to 2' do
|
65
72
|
2.times { client.publish 'Payload', to: 'test.batch' }
|
66
73
|
|
67
74
|
2.times { Timeout.timeout(4) { queue.pop } }
|
68
|
-
end
|
69
75
|
|
70
|
-
|
71
|
-
|
76
|
+
expect(Receiver).to have_received(:batch).exactly(2).times
|
77
|
+
end
|
72
78
|
|
79
|
+
it 'does not bulk ack all unacknowledged messages when acknowledging the last one' do
|
73
80
|
2.times { client.publish 'Payload', to: 'test.batch' }
|
74
81
|
|
75
|
-
messages = 2
|
82
|
+
messages = Array.new(2) { Timeout.timeout(4) { queue.pop } }
|
76
83
|
messages[1].ack
|
77
84
|
messages[0].nack
|
78
85
|
|
@@ -80,5 +87,7 @@ describe Msgr do
|
|
80
87
|
message = Timeout.timeout(4) { queue.pop }
|
81
88
|
expect(message.payload).to eq(messages[0].payload)
|
82
89
|
expect(message.delivery_info.redelivered).to eq(true)
|
90
|
+
|
91
|
+
expect(Receiver).to have_received(:batch).exactly(3).times
|
83
92
|
end
|
84
93
|
end
|