vx-common-amqp 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
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
+