vx-common-amqp 0.2.6

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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +14 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +276 -0
  7. data/README.md +29 -0
  8. data/Rakefile +6 -0
  9. data/bin/vx-consumers +12 -0
  10. data/lib/vx/common/amqp/cli.rb +88 -0
  11. data/lib/vx/common/amqp/config.rb +74 -0
  12. data/lib/vx/common/amqp/consumer/ack.rb +19 -0
  13. data/lib/vx/common/amqp/consumer/configuration.rb +119 -0
  14. data/lib/vx/common/amqp/consumer/publish.rb +32 -0
  15. data/lib/vx/common/amqp/consumer/subscribe.rb +67 -0
  16. data/lib/vx/common/amqp/consumer.rb +70 -0
  17. data/lib/vx/common/amqp/formatter.rb +105 -0
  18. data/lib/vx/common/amqp/mixins/callbacks.rb +35 -0
  19. data/lib/vx/common/amqp/mixins/logger.rb +17 -0
  20. data/lib/vx/common/amqp/session.rb +154 -0
  21. data/lib/vx/common/amqp/supervisor/threaded.rb +171 -0
  22. data/lib/vx/common/amqp/testing.rb +54 -0
  23. data/lib/vx/common/amqp/version.rb +7 -0
  24. data/lib/vx/common/amqp.rb +68 -0
  25. data/spec/integration/multi_threaded_spec.rb +89 -0
  26. data/spec/integration/threaded_supervisor_spec.rb +85 -0
  27. data/spec/lib/amqp/config_spec.rb +32 -0
  28. data/spec/lib/amqp/consumer_spec.rb +316 -0
  29. data/spec/lib/amqp/formatter_spec.rb +47 -0
  30. data/spec/lib/amqp/mixins/callbacks_spec.rb +26 -0
  31. data/spec/lib/amqp/session_spec.rb +144 -0
  32. data/spec/lib/amqp/supervisor/threaded_spec.rb +124 -0
  33. data/spec/lib/amqp_spec.rb +9 -0
  34. data/spec/spec_helper.rb +13 -0
  35. data/spec/support/amqp.rb +15 -0
  36. data/spec/support/ignore_me_error.rb +1 -0
  37. data/vx-common-amqp.gemspec +30 -0
  38. metadata +178 -0
@@ -0,0 +1,68 @@
1
+ require File.expand_path("../amqp/version", __FILE__)
2
+
3
+ module Vx
4
+ module Common
5
+ module AMQP
6
+
7
+ autoload :Config, File.expand_path("../amqp/config", __FILE__)
8
+ autoload :Session, File.expand_path("../amqp/session", __FILE__)
9
+ autoload :Consumer, File.expand_path("../amqp/consumer", __FILE__)
10
+ autoload :CLI, File.expand_path("../amqp/cli", __FILE__)
11
+ autoload :Formatter, File.expand_path("../amqp/formatter", __FILE__)
12
+
13
+ module Supervisor
14
+ autoload :Threaded, File.expand_path("../amqp/supervisor/threaded", __FILE__)
15
+ end
16
+
17
+ autoload :Logger, File.expand_path("../amqp/mixins/logger", __FILE__)
18
+ autoload :Callbacks, File.expand_path("../amqp/mixins/callbacks", __FILE__)
19
+
20
+ extend self
21
+
22
+ @@config = Common::AMQP::Config.new
23
+ @@session = Common::AMQP::Session.new
24
+
25
+ def configure
26
+ yield config
27
+ end
28
+
29
+ def config
30
+ @@config
31
+ end
32
+
33
+ def session
34
+ @@session
35
+ end
36
+
37
+ def open
38
+ session.open
39
+ end
40
+
41
+ def open?
42
+ session.open?
43
+ end
44
+
45
+ def close
46
+ session.close
47
+ end
48
+
49
+ def logger
50
+ config.logger
51
+ end
52
+
53
+ def logger=(val)
54
+ config.logger = val
55
+ end
56
+
57
+ def shutdown
58
+ Common::AMQP::Session.shutdown
59
+ Vx::Common::AMQP::Supervisor::Threaded.shutdown
60
+ end
61
+
62
+ def shutdown?
63
+ Common::AMQP::Session.shutdown?
64
+ end
65
+
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,89 @@
1
+ require 'spec_helper'
2
+ require 'thread'
3
+ require 'timeout'
4
+
5
+ class Vx::BobThread
6
+ include Vx::Common::AMQP::Consumer
7
+
8
+ queue exclusive: true, durable: false
9
+ exchange auto_delete: true, durable: false
10
+ ack true
11
+
12
+ def perform(payload)
13
+ $mtest_mutex.synchronize do
14
+ $mtest_collected << payload
15
+ ack!
16
+ sleep 0.1
17
+ end
18
+ end
19
+ end
20
+
21
+ class Vx::AliceThread
22
+ include Vx::Common::AMQP::Consumer
23
+
24
+ queue exclusive: true, durable: false
25
+ exchange auto_delete: true, durable: false
26
+ ack true
27
+
28
+ def perform(payload)
29
+ Vx::BobThread.publish payload, content_type: properties[:content_type]
30
+ ack!
31
+ sleep 0.1
32
+ end
33
+ end
34
+
35
+ describe "Run in multithread environment", slow: true, jruby: true do
36
+ let(:num_messages) { 100 }
37
+ let(:alice) { Vx::AliceThread }
38
+ let(:bob) { Vx::BobThread }
39
+ let(:sess) { Vx::Common::AMQP.open }
40
+ let(:ch) { sess.conn.create_channel }
41
+
42
+ before do
43
+ $mtest_mutex = Mutex.new
44
+ $mtest_collected = []
45
+ end
46
+
47
+ after do
48
+ sess.close
49
+ end
50
+
51
+ it "should be successfuly" do
52
+ ths = (0..12).map do |i|
53
+ klass = (i % 2 == 0) ? alice : bob
54
+ Thread.new do
55
+ begin
56
+ klass.subscribe
57
+ rescue Exception => e
58
+ puts "ERROR: #{e.inspect}"
59
+ raise e
60
+ end
61
+ end.tap do |th|
62
+ th.abort_on_exception = true
63
+ end
64
+ end
65
+ sleep 0.5
66
+
67
+ num_messages.times do |n|
68
+ alice.publish "n#{n}", content_type: "text/plain"
69
+ end
70
+
71
+ Timeout.timeout(60) do
72
+ loop do
73
+ stop = false
74
+ $mtest_mutex.synchronize do
75
+ puts $mtest_collected.size
76
+ stop = true if $mtest_collected.size >= num_messages
77
+ end
78
+ break if stop
79
+ sleep 2
80
+ end
81
+ end
82
+
83
+ Vx::Common::AMQP.shutdown
84
+ Timeout.timeout(10) { ths.map{|i| i.join } }
85
+
86
+ expect($mtest_collected.sort).to eq (0...num_messages).map{|i| "n#{i}" }.sort
87
+ end
88
+
89
+ end
@@ -0,0 +1,85 @@
1
+ require 'spec_helper'
2
+ require 'thread'
3
+ require 'timeout'
4
+
5
+ class Vx::BobThreadWithSupervisor
6
+ include Vx::Common::AMQP::Consumer
7
+
8
+ class ErrorSimulation < ::Exception ; end
9
+
10
+ queue exclusive: true, durable: false
11
+ exchange auto_delete: true, durable: false
12
+ ack true
13
+
14
+ def perform(payload)
15
+ $mtest_mutex.synchronize do
16
+ raise IgnoreMeError if Random.new(delivery_info.delivery_tag.to_i).rand < 0.2
17
+ $mtest_collected << payload
18
+ ack!
19
+ sleep 0.1
20
+ end
21
+ end
22
+ end
23
+
24
+ class Vx::AliceThreadWithSupervisor
25
+ include Vx::Common::AMQP::Consumer
26
+
27
+ queue exclusive: true, durable: false
28
+ exchange auto_delete: true, durable: false
29
+ ack true
30
+
31
+ def perform(payload)
32
+ Vx::BobThreadWithSupervisor.publish payload, content_type: properties[:content_type]
33
+ ack!
34
+ sleep 0.1
35
+ end
36
+ end
37
+
38
+ describe "Run in multithread environment", slow: true, jruby: true do
39
+ let(:num_messages) { 100 }
40
+ let(:alice) { Vx::AliceThreadWithSupervisor }
41
+ let(:bob) { Vx::BobThreadWithSupervisor }
42
+ let(:sess) { Vx::Common::AMQP.open }
43
+ let(:ch) { sess.conn.create_channel }
44
+
45
+ before do
46
+ $mtest_mutex = Mutex.new
47
+ $mtest_collected = []
48
+ end
49
+
50
+ after do
51
+ sess.close
52
+ end
53
+
54
+ it "should be successfuly" do
55
+
56
+ supervisor = Vx::Common::AMQP::Supervisor::Threaded.build alice => 6, bob => 6
57
+
58
+ supervisor_thread = supervisor.run_async
59
+
60
+ sleep 0.5
61
+
62
+ num_messages.times do |n|
63
+ alice.publish "n#{n}", content_type: "text/plain"
64
+ end
65
+
66
+ Timeout.timeout(60) do
67
+ loop do
68
+ stop = false
69
+ $mtest_mutex.synchronize do
70
+ puts $mtest_collected.size
71
+ stop = true if $mtest_collected.size >= num_messages
72
+ end
73
+ break if stop
74
+ sleep 2
75
+ end
76
+ end
77
+
78
+ Vx::Common::AMQP.shutdown
79
+ supervisor.shutdown
80
+ Timeout.timeout(10) { supervisor_thread.join }
81
+
82
+ expect($mtest_collected.sort).to eq (0...num_messages).map{|i| "n#{i}" }.sort
83
+ end
84
+
85
+ end
@@ -0,0 +1,32 @@
1
+ require 'spec_helper'
2
+
3
+ describe Vx::Common::AMQP::Config do
4
+ let(:config) { described_class.new }
5
+
6
+ context '(callbacks)' do
7
+ %w{ before after }.each do |p|
8
+ %w{ subscribe recieve publish }.each do |m|
9
+ name = "#{p}_#{m}"
10
+ it name do
11
+ config.public_send name do |value|
12
+ value
13
+ end
14
+
15
+ val = config.callbacks[name.to_sym].call("value")
16
+ expect(val).to eq 'value'
17
+ end
18
+ end
19
+ end
20
+
21
+ context "on_error" do
22
+ it "should be success" do
23
+ config.on_error do |e|
24
+ e
25
+ end
26
+ val = config.callbacks[:on_error].call "value"
27
+ expect(val).to eq 'value'
28
+ end
29
+ end
30
+ end
31
+
32
+ end
@@ -0,0 +1,316 @@
1
+ require 'spec_helper'
2
+ require 'timeout'
3
+ require 'json'
4
+
5
+ class Vx::TestConsumer
6
+ include Vx::Common::AMQP::Consumer
7
+
8
+ ack true
9
+
10
+ def perform(payload)
11
+ Thread.current[:collected] ||= []
12
+ Thread.current[:collected] << payload
13
+ ack!
14
+
15
+ :shutdown if Thread.current[:collected].size == 3
16
+ end
17
+ end
18
+
19
+ describe Vx::Common::AMQP::Consumer do
20
+
21
+ let(:consumer) { Vx::TestConsumer.new }
22
+ let(:consumer_class) { consumer.class }
23
+
24
+ subject { consumer }
25
+
26
+ before { consumer_class.reset_consumer_configuration! }
27
+
28
+ context '(configuration)' do
29
+
30
+ subject { consumer_class }
31
+
32
+ its(:config) { should be_an_instance_of(Vx::Common::AMQP::Config) }
33
+ its(:consumer_name) { should eq 'test_consumer' }
34
+
35
+ context "consumer_id" do
36
+
37
+ it "should be success" do
38
+ expect(consumer_class.consumer_id).to eq 'test_consumer'
39
+ th = Thread.new do
40
+ Thread.current[:vx_amqp_consumer_id] = '99'
41
+ consumer_class.consumer_id
42
+ end
43
+ expect(th.value).to eq 'test_consumer.99'
44
+ end
45
+ end
46
+
47
+
48
+ context "model" do
49
+ subject { consumer_class.model }
50
+
51
+ it "by default should be nil" do
52
+ expect(subject).to be_nil
53
+ end
54
+
55
+ it 'when set model should be' do
56
+ consumer_class.model Hash
57
+ expect(subject).to eq Hash
58
+ end
59
+ end
60
+
61
+ context "content_type" do
62
+
63
+ subject { consumer_class.content_type }
64
+
65
+ it "by default should be nil" do
66
+ expect(subject).to be_nil
67
+ end
68
+
69
+ it 'when set content type should be' do
70
+ consumer_class.content_type 'foo'
71
+ expect(subject).to eq 'foo'
72
+ end
73
+ end
74
+
75
+ context "bind_options" do
76
+ subject { consumer_class.bind_options }
77
+
78
+ context "by default should eq {}" do
79
+ it { should eq ({}) }
80
+ end
81
+
82
+ context "set routing_key" do
83
+ before { consumer_class.routing_key 'key' }
84
+ it { should eq(routing_key: 'key') }
85
+ end
86
+
87
+ context "set routing_key by block" do
88
+ before do
89
+ consumer_class.routing_key { 'key.block' }
90
+ end
91
+ it { should eq(routing_key: 'key.block') }
92
+ end
93
+
94
+ context "set headers" do
95
+ before { consumer_class.headers 'key' }
96
+ it { should eq({headers: 'key'}) }
97
+ end
98
+
99
+ context "set headers by block" do
100
+ before { consumer_class.headers { 'key.block' } }
101
+ it { should eq(headers: 'key.block') }
102
+ end
103
+ end
104
+
105
+ context "ack" do
106
+ subject { consumer_class.ack }
107
+
108
+ it "by default should be false" do
109
+ expect(subject).to be_false
110
+ end
111
+
112
+ it "when set to true should be true" do
113
+ consumer_class.ack true
114
+ expect(subject).to be_true
115
+ end
116
+ end
117
+
118
+ context "exchange_name" do
119
+ subject { consumer_class.exchange_name }
120
+
121
+ it 'by default should eq consumer_name' do
122
+ expect(subject).to eq consumer_class.consumer_name
123
+ end
124
+
125
+ it "when set name should be" do
126
+ consumer_class.exchange :foo
127
+ expect(subject).to eq :foo
128
+ end
129
+
130
+ it "when set by block should be" do
131
+ consumer_class.exchange { 'name.block' }
132
+ expect(subject).to eq 'name.block'
133
+ end
134
+ end
135
+
136
+ context "queue_name" do
137
+ subject{ consumer_class.queue_name }
138
+ it 'by default should eq consumer_name' do
139
+ expect(subject).to eq consumer_class.consumer_name
140
+ end
141
+
142
+ it "when set name should be" do
143
+ consumer_class.queue :bar
144
+ expect(subject).to eq :bar
145
+ end
146
+
147
+ it "when set by block should be" do
148
+ consumer_class.queue { 'name.block' }
149
+ expect(subject).to eq 'name.block'
150
+ end
151
+ end
152
+
153
+ %w{ queue exchange }.each do |m|
154
+ context "#{m}_options" do
155
+ subject { consumer_class.send "#{m}_options" }
156
+ it 'by default should eq {}' do
157
+ expect(subject).to eq({})
158
+ end
159
+
160
+ it "when set #{m} options should be" do
161
+ consumer_class.send(m, durable: true)
162
+ expect(subject).to eq(durable: true)
163
+ end
164
+ end
165
+ end
166
+
167
+ %w{ routing_key headers }.each do |m|
168
+ context m do
169
+ subject { consumer_class.send m }
170
+
171
+ it 'by default should be nil' do
172
+ expect(subject).to be_nil
173
+ end
174
+
175
+ it "when set #{m} should be" do
176
+ consumer_class.send(m, key: :value)
177
+ expect(subject).to eq(key: :value)
178
+ end
179
+ end
180
+ end
181
+ end
182
+
183
+ context "(publish)" do
184
+
185
+ context "options" do
186
+ let(:message) { {"foo" => 1, "bar" => 2} }
187
+ let(:expected_options) { {} }
188
+ let(:options) { {} }
189
+ let(:x) { OpenStruct.new name: "name" }
190
+
191
+ subject{ consumer_class.publish message, options }
192
+
193
+ before do
194
+ mock(consumer_class).declare_exchange { x }
195
+ mock(x).publish(message.to_json, expected_options)
196
+ end
197
+
198
+ context "routing_key" do
199
+ context "by default" do
200
+ it { should be }
201
+ end
202
+
203
+ context "when exists in configuration" do
204
+ let(:expected_options) { { routing_key: 'routing.key' } }
205
+ before do
206
+ consumer_class.routing_key 'routing.key'
207
+ end
208
+ it { should be }
209
+ end
210
+
211
+ context "when exists in options" do
212
+ let(:expected_options) { { routing_key: 'routing.key' } }
213
+ let(:options) { { routing_key: 'routing.key' } }
214
+ it { should be }
215
+ end
216
+
217
+ context "when exists in options and configuration" do
218
+ let(:expected_options) { { routing_key: 'options.key' } }
219
+ let(:options) { { routing_key: 'options.key' } }
220
+ before do
221
+ consumer_class.routing_key 'configuration.key'
222
+ end
223
+ it { should be }
224
+ end
225
+ end
226
+
227
+ context "headers" do
228
+ context "by default" do
229
+ it { should be }
230
+ end
231
+
232
+ context "when exists in configuration" do
233
+ let(:expected_options) { { headers: 'key' } }
234
+ before do
235
+ consumer_class.headers 'key'
236
+ end
237
+ it { should be }
238
+ end
239
+
240
+ context "when exists in options" do
241
+ let(:expected_options) { { headers: 'key' } }
242
+ let(:options) { { headers: 'key' } }
243
+ it { should be }
244
+ end
245
+
246
+ context "when exists in options and configuration" do
247
+ let(:expected_options) { { headers: 'options' } }
248
+ let(:options) { { headers: 'options' } }
249
+ before do
250
+ consumer_class.headers 'configuration'
251
+ end
252
+ it { should be }
253
+ end
254
+ end
255
+ end
256
+
257
+ context "real run" do
258
+ let(:x_name) { consumer_class.exchange_name }
259
+ let(:q_name) { consumer_class.queue_name }
260
+ let(:sess) { consumer_class.session.open }
261
+ let(:ch) { sess.conn.create_channel }
262
+ let(:q) { sess.declare_queue q_name, channel: ch }
263
+ let(:x) { sess.declare_exchange x_name, channel: ch }
264
+ let(:message) { { 'key' => 'value' } }
265
+
266
+ after do
267
+ delete_queue q
268
+ delete_exchange x
269
+ sess.close
270
+ end
271
+
272
+ before do
273
+ q.bind x
274
+ end
275
+
276
+ it "should publish message to exchange using settings from consumer" do
277
+ consumer_class.publish message
278
+ sleep 0.25
279
+ expect(q.message_count).to eq 1
280
+ _, _, expected = q.pop
281
+ expect(expected).to eq message.to_json
282
+ end
283
+ end
284
+ end
285
+
286
+ context '(subscribe)' do
287
+ let(:x_name) { consumer_class.exchange_name }
288
+ let(:q_name) { consumer_class.queue_name }
289
+ let(:sess) { consumer_class.session.open }
290
+ let(:ch) { sess.conn.create_channel }
291
+ let(:q) { sess.declare_queue q_name, channel: ch }
292
+ let(:x) { sess.declare_exchange x_name, channel: ch }
293
+
294
+ after do
295
+ delete_queue q
296
+ delete_exchange x
297
+ sess.close
298
+ end
299
+
300
+ before do
301
+ consumer_class.ack true
302
+ q.bind(x)
303
+ 3.times { |n| x.publish({"n" => n}.to_json, content_type: "application/json") }
304
+ end
305
+
306
+ subject { Thread.current[:collected] }
307
+
308
+ it "should receive messages" do
309
+ Timeout.timeout(3) do
310
+ consumer_class.subscribe
311
+ end
312
+ expect(subject).to have(3).items
313
+ expect(subject.map(&:values).flatten).to eq [0,1,2]
314
+ end
315
+ end
316
+ end
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+ require 'json'
3
+
4
+ class FormatterConsumerTest
5
+ include Vx::Common::AMQP::Consumer
6
+
7
+ model Hash
8
+ end
9
+
10
+ describe Vx::Common::AMQP::Formatter do
11
+ let(:consumer) { FormatterConsumerTest.new }
12
+ subject { described_class }
13
+
14
+ context "pack" do
15
+ let(:body) { { "a" => 1, "b" => 2 } }
16
+
17
+ it "should pack message" do
18
+ expect(subject.pack 'application/json', body).to eq body.to_json
19
+ expect(subject.pack :foo, body).to be_nil
20
+ end
21
+
22
+ end
23
+
24
+ context "unpack" do
25
+ let(:body) { { "a" => 1, "b" => 2 } }
26
+
27
+ before do
28
+ mock(Hash).from_json(body.to_json) { body }
29
+ end
30
+
31
+ it "should unpack message" do
32
+ expect(subject.unpack 'application/json', Hash, body.to_json).to eq body
33
+ expect(subject.unpack :foo, Hash, body.to_json).to be_nil
34
+ end
35
+
36
+ end
37
+
38
+ context "lookup" do
39
+
40
+ it "should find format by content type" do
41
+ expect(subject.lookup('application/json').content_type).to eq 'application/json'
42
+ expect(subject.lookup(:foo)).to be_nil
43
+ end
44
+
45
+ end
46
+
47
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ describe Vx::Common::AMQP::Callbacks do
4
+ let(:output) { [] }
5
+ let(:object) { Object.new.extend described_class }
6
+
7
+ before do
8
+ Vx::Common::AMQP.config.reset!
9
+ Vx::Common::AMQP.configure do |c|
10
+ c.before_publish { |v| output << "before:#{v}" }
11
+ c.after_publish { |v| output << "after:#{v}" }
12
+ end
13
+ end
14
+
15
+ after { Vx::Common::AMQP.config.reset! }
16
+
17
+ context 'run_callbacks' do
18
+ it "should be success" do
19
+ object.run_callbacks :publish, "call" do
20
+ output << "call"
21
+ end
22
+ expect(output).to eq %w{ before:call call after:call }
23
+ end
24
+ end
25
+ end
26
+