evrone-common-amqp 0.0.1

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 (37) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +8 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +29 -0
  8. data/Rakefile +6 -0
  9. data/bin/amqp_consumers +12 -0
  10. data/evrone-common-amqp.gemspec +30 -0
  11. data/lib/evrone/common/amqp.rb +68 -0
  12. data/lib/evrone/common/amqp/cli.rb +88 -0
  13. data/lib/evrone/common/amqp/config.rb +74 -0
  14. data/lib/evrone/common/amqp/consumer.rb +70 -0
  15. data/lib/evrone/common/amqp/consumer/ack.rb +19 -0
  16. data/lib/evrone/common/amqp/consumer/configuration.rb +93 -0
  17. data/lib/evrone/common/amqp/consumer/publish.rb +32 -0
  18. data/lib/evrone/common/amqp/consumer/subscribe.rb +67 -0
  19. data/lib/evrone/common/amqp/formatter.rb +109 -0
  20. data/lib/evrone/common/amqp/mixins/logger.rb +17 -0
  21. data/lib/evrone/common/amqp/mixins/with_middleware.rb +16 -0
  22. data/lib/evrone/common/amqp/session.rb +154 -0
  23. data/lib/evrone/common/amqp/supervisor/threaded.rb +170 -0
  24. data/lib/evrone/common/amqp/testing.rb +46 -0
  25. data/lib/evrone/common/amqp/version.rb +7 -0
  26. data/spec/integration/multi_threaded_spec.rb +83 -0
  27. data/spec/integration/threaded_supervisor_spec.rb +85 -0
  28. data/spec/lib/amqp/consumer_spec.rb +281 -0
  29. data/spec/lib/amqp/formatter_spec.rb +47 -0
  30. data/spec/lib/amqp/mixins/with_middleware_spec.rb +32 -0
  31. data/spec/lib/amqp/session_spec.rb +144 -0
  32. data/spec/lib/amqp/supervisor/threaded_spec.rb +123 -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. metadata +175 -0
@@ -0,0 +1,170 @@
1
+ require 'thread'
2
+
3
+ module Evrone
4
+ module Common
5
+ module AMQP
6
+ class Supervisor::Threaded
7
+
8
+ include Common::AMQP::Logger
9
+
10
+ POOL_INTERVAL = 0.5
11
+
12
+ Task = Struct.new(:object, :method, :id) do
13
+
14
+ attr_accessor :thread, :attempt, :start_at
15
+
16
+ def alive?
17
+ !!(thread && thread.alive?)
18
+ end
19
+
20
+ def inspect
21
+ %{#<Task
22
+ object=#{object.to_s}
23
+ method=#{method.inspect}
24
+ id=#{id.inspect}
25
+ alive=#{alive?}
26
+ attempt=#{attempt}
27
+ start_at=#{start_at}> }.gsub("\n", ' ').gsub(/ +/, ' ').strip
28
+ end
29
+ end
30
+
31
+ class SpawnAttemptsLimitReached < ::Exception ; end
32
+
33
+ class << self
34
+
35
+ @@shutdown = false
36
+
37
+ def build(tasks)
38
+ supervisor = new
39
+ tasks.each_pair do |k,v|
40
+ v.times do |n|
41
+ supervisor.add k, :subscribe, n
42
+ end
43
+ end
44
+ supervisor
45
+ end
46
+
47
+ def resume
48
+ @@shutdown = false
49
+ end
50
+
51
+ def shutdown?
52
+ @@shutdown
53
+ end
54
+
55
+ def shutdown
56
+ @@shutdown = true
57
+ end
58
+
59
+ end
60
+
61
+ def initialize
62
+ self.class.resume
63
+ @tasks = Array.new
64
+ end
65
+
66
+ def add(object, method, id)
67
+ @tasks.push Task.new(object, method, id).freeze
68
+ end
69
+
70
+ def size
71
+ @tasks.size
72
+ end
73
+
74
+ def shutdown?
75
+ self.class.shutdown?
76
+ end
77
+
78
+ def shutdown
79
+ self.class.shutdown
80
+ end
81
+
82
+ def run_async
83
+ Thread.new { run }.tap{|t| t.abort_on_exception = true }
84
+ end
85
+
86
+ def run
87
+ start_all_threads
88
+
89
+ loop do
90
+ task = @tasks.shift
91
+ break unless task
92
+
93
+ case
94
+ when shutdown?
95
+ log_thread_error task
96
+ when task.alive?
97
+ @tasks.push task
98
+ else
99
+ process_fail task
100
+ end
101
+
102
+ sleep POOL_INTERVAL unless shutdown?
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ def process_fail(task)
109
+ log_thread_error task
110
+ if check_attempt task
111
+ @tasks.push create_thread(task, task.attempt + 1)
112
+ else
113
+ raise SpawnAttemptsLimitReached
114
+ end
115
+ end
116
+
117
+ def start_all_threads
118
+ started_tasks = Array.new
119
+ while task = @tasks.shift
120
+ started_tasks.push create_thread(task, 0)
121
+ end
122
+ while task = started_tasks.shift
123
+ @tasks.push task
124
+ end
125
+ end
126
+
127
+ def create_thread(task, attempt)
128
+ attempt = 0 if reset_attempt?(task)
129
+ task.dup.tap do |new_task|
130
+ new_task.thread = Thread.new(new_task) do |t|
131
+ Thread.current[:consumer_id] = t.id
132
+ Thread.current[:consumer_name] = t.object.to_s
133
+ t.object.send t.method
134
+ end
135
+ new_task.thread.abort_on_exception = false
136
+ new_task.attempt = attempt
137
+ new_task.start_at = Time.now
138
+ new_task.freeze
139
+ debug "spawn #{new_task.inspect}"
140
+ end
141
+ end
142
+
143
+ def log_thread_error(task)
144
+ return unless task.thread
145
+
146
+ begin
147
+ task.thread.value
148
+ nil
149
+ rescue Exception => e
150
+ STDERR.puts "#{e.inspect} in #{task.inspect}"
151
+ STDERR.puts e.backtrace.join("\n")
152
+ e
153
+ end
154
+ end
155
+
156
+ def reset_attempt?(task)
157
+ return true unless task.start_at
158
+
159
+ interval = 60
160
+ (task.start_at + interval) < Time.now
161
+ end
162
+
163
+ def check_attempt(task)
164
+ task.attempt.to_i <= Common::AMQP.config.spawn_attempts.to_i
165
+ end
166
+
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,46 @@
1
+ require File.expand_path("../../amqp", __FILE__)
2
+
3
+ module Evrone
4
+ module Common
5
+ module AMQP
6
+ module Testing
7
+
8
+ extend self
9
+
10
+ @@messages = []
11
+ @@exchange_messages = Hash.new { |h,k| h[k] = [] }
12
+
13
+ def messages
14
+ @@messages
15
+ end
16
+
17
+ def exchange_messages
18
+ @@exchange_messages
19
+ end
20
+
21
+ def clear
22
+ messages.clear
23
+ exchange_messages.clear
24
+ end
25
+ end
26
+
27
+ module Consumer::Publish
28
+ alias_method :real_publish, :publish
29
+
30
+ def publish(message, options = {})
31
+ Testing.exchange_messages[exchange_name] << message
32
+ Testing.messages << message
33
+ self
34
+ end
35
+ end
36
+
37
+ module Consumer
38
+ module ClassMethods
39
+ def messages
40
+ Testing.exchange_messages[exchange_name]
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,7 @@
1
+ module Evrone
2
+ module Common
3
+ module AMQP
4
+ VERSION = "0.0.1"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,83 @@
1
+ require 'spec_helper'
2
+ require 'thread'
3
+ require 'timeout'
4
+
5
+ class Evrone::BobThread
6
+ include Evrone::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 Evrone::AliceThread
22
+ include Evrone::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
+ Evrone::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) { Evrone::AliceThread }
38
+ let(:bob) { Evrone::BobThread }
39
+ let(:sess) { Evrone::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
+ klass.subscribe
56
+ end
57
+ end
58
+ ths.each{|t| t.abort_on_exception = true }
59
+ sleep 0.5
60
+
61
+ num_messages.times do |n|
62
+ alice.publish "n#{n}", content_type: "text/plain"
63
+ end
64
+
65
+ Timeout.timeout(60) do
66
+ loop do
67
+ stop = false
68
+ $mtest_mutex.synchronize do
69
+ puts $mtest_collected.size
70
+ stop = true if $mtest_collected.size >= num_messages
71
+ end
72
+ break if stop
73
+ sleep 2
74
+ end
75
+ end
76
+
77
+ Evrone::Common::AMQP.shutdown
78
+ Timeout.timeout(10) { ths.map{|i| i.join } }
79
+
80
+ expect($mtest_collected.sort).to eq (0...num_messages).map{|i| "n#{i}" }.sort
81
+ end
82
+
83
+ end
@@ -0,0 +1,85 @@
1
+ require 'spec_helper'
2
+ require 'thread'
3
+ require 'timeout'
4
+
5
+ class Evrone::BobThreadWithSupervisor
6
+ include Evrone::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 Evrone::AliceThreadWithSupervisor
25
+ include Evrone::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
+ Evrone::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) { Evrone::AliceThreadWithSupervisor }
41
+ let(:bob) { Evrone::BobThreadWithSupervisor }
42
+ let(:sess) { Evrone::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 = Evrone::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
+ Evrone::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,281 @@
1
+ require 'spec_helper'
2
+ require 'timeout'
3
+ require 'json'
4
+
5
+ class Evrone::TestConsumer
6
+ include Evrone::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 Evrone::Common::AMQP::Consumer do
20
+
21
+ let(:consumer) { Evrone::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(:consumer_name) { should eq 'evrone.test' }
33
+ its(:config) { should be_an_instance_of(Evrone::Common::AMQP::Config) }
34
+
35
+ context "model" do
36
+ subject { consumer_class.model }
37
+
38
+ it "by default should be nil" do
39
+ expect(subject).to be_nil
40
+ end
41
+
42
+ it 'when set model should be' do
43
+ consumer_class.model Hash
44
+ expect(subject).to eq Hash
45
+ end
46
+ end
47
+
48
+ context "content_type" do
49
+
50
+ subject { consumer_class.content_type }
51
+
52
+ it "by default should be nil" do
53
+ expect(subject).to be_nil
54
+ end
55
+
56
+ it 'when set content type should be' do
57
+ consumer_class.content_type 'foo'
58
+ expect(subject).to eq 'foo'
59
+ end
60
+ end
61
+
62
+ context "bind_options" do
63
+ subject { consumer_class.bind_options }
64
+
65
+ context "by default should eq {}" do
66
+ it { should eq ({}) }
67
+ end
68
+
69
+ context "set routing_key" do
70
+ before { consumer_class.routing_key 'key' }
71
+ it { should eq({routing_key: 'key'}) }
72
+ end
73
+
74
+ context "set headers" do
75
+ before { consumer_class.headers 'key' }
76
+ it { should eq({headers: 'key'}) }
77
+ end
78
+ end
79
+
80
+ context "ack" do
81
+ subject { consumer_class.ack }
82
+
83
+ it "by default should be false" do
84
+ expect(subject).to be_false
85
+ end
86
+
87
+ it "when set to true should be true" do
88
+ consumer_class.ack true
89
+ expect(subject).to be_true
90
+ end
91
+ end
92
+
93
+ context "exchange_name" do
94
+ subject { consumer_class.exchange_name }
95
+
96
+ it 'by default should eq consumer_name' do
97
+ expect(subject).to eq consumer_class.consumer_name
98
+ end
99
+
100
+ it "when set name should be" do
101
+ consumer_class.exchange :foo
102
+ expect(subject).to eq :foo
103
+ end
104
+ end
105
+
106
+ context "queue_name" do
107
+ subject{ consumer_class.queue_name }
108
+ it 'by default should eq consumer_name' do
109
+ expect(subject).to eq consumer_class.consumer_name
110
+ end
111
+
112
+ it "when set name should be" do
113
+ consumer_class.queue :bar
114
+ expect(subject).to eq :bar
115
+ end
116
+ end
117
+
118
+ %w{ queue exchange }.each do |m|
119
+ context "#{m}_options" do
120
+ subject { consumer_class.send "#{m}_options" }
121
+ it 'by default should eq {}' do
122
+ expect(subject).to eq({})
123
+ end
124
+
125
+ it "when set #{m} options should be" do
126
+ consumer_class.send(m, durable: true)
127
+ expect(subject).to eq(durable: true)
128
+ end
129
+ end
130
+ end
131
+
132
+ %w{ routing_key headers }.each do |m|
133
+ context m do
134
+ subject { consumer_class.send m }
135
+
136
+ it 'by default should be nil' do
137
+ expect(subject).to be_nil
138
+ end
139
+
140
+ it "when set #{m} should be" do
141
+ consumer_class.send(m, key: :value)
142
+ expect(subject).to eq(key: :value)
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ context "(publish)" do
149
+
150
+ context "options" do
151
+ let(:message) { {"foo" => 1, "bar" => 2} }
152
+ let(:expected_options) { {} }
153
+ let(:options) { {} }
154
+ let(:x) { OpenStruct.new name: "name" }
155
+
156
+ subject{ consumer_class.publish message, options }
157
+
158
+ before do
159
+ mock(consumer_class).declare_exchange { x }
160
+ mock(x).publish(message.to_json, expected_options)
161
+ end
162
+
163
+ context "routing_key" do
164
+ context "by default" do
165
+ it { should be }
166
+ end
167
+
168
+ context "when exists in configuration" do
169
+ let(:expected_options) { { routing_key: 'routing.key' } }
170
+ before do
171
+ consumer_class.routing_key 'routing.key'
172
+ end
173
+ it { should be }
174
+ end
175
+
176
+ context "when exists in options" do
177
+ let(:expected_options) { { routing_key: 'routing.key' } }
178
+ let(:options) { { routing_key: 'routing.key' } }
179
+ it { should be }
180
+ end
181
+
182
+ context "when exists in options and configuration" do
183
+ let(:expected_options) { { routing_key: 'options.key' } }
184
+ let(:options) { { routing_key: 'options.key' } }
185
+ before do
186
+ consumer_class.routing_key 'configuration.key'
187
+ end
188
+ it { should be }
189
+ end
190
+ end
191
+
192
+ context "headers" do
193
+ context "by default" do
194
+ it { should be }
195
+ end
196
+
197
+ context "when exists in configuration" do
198
+ let(:expected_options) { { headers: 'key' } }
199
+ before do
200
+ consumer_class.headers 'key'
201
+ end
202
+ it { should be }
203
+ end
204
+
205
+ context "when exists in options" do
206
+ let(:expected_options) { { headers: 'key' } }
207
+ let(:options) { { headers: 'key' } }
208
+ it { should be }
209
+ end
210
+
211
+ context "when exists in options and configuration" do
212
+ let(:expected_options) { { headers: 'options' } }
213
+ let(:options) { { headers: 'options' } }
214
+ before do
215
+ consumer_class.headers 'configuration'
216
+ end
217
+ it { should be }
218
+ end
219
+ end
220
+ end
221
+
222
+ context "real run" do
223
+ let(:x_name) { consumer_class.exchange_name }
224
+ let(:q_name) { consumer_class.queue_name }
225
+ let(:sess) { consumer_class.session.open }
226
+ let(:ch) { sess.conn.create_channel }
227
+ let(:q) { sess.declare_queue q_name, channel: ch }
228
+ let(:x) { sess.declare_exchange x_name, channel: ch }
229
+ let(:message) { { 'key' => 'value' } }
230
+
231
+ after do
232
+ delete_queue q
233
+ delete_exchange x
234
+ sess.close
235
+ end
236
+
237
+ before do
238
+ q.bind x
239
+ end
240
+
241
+ it "should publish message to exchange using settings from consumer" do
242
+ consumer_class.publish message
243
+ sleep 0.25
244
+ expect(q.message_count).to eq 1
245
+ _, _, expected = q.pop
246
+ expect(expected).to eq message.to_json
247
+ end
248
+ end
249
+ end
250
+
251
+ context '(subscribe)' do
252
+ let(:x_name) { consumer_class.exchange_name }
253
+ let(:q_name) { consumer_class.queue_name }
254
+ let(:sess) { consumer_class.session.open }
255
+ let(:ch) { sess.conn.create_channel }
256
+ let(:q) { sess.declare_queue q_name, channel: ch }
257
+ let(:x) { sess.declare_exchange x_name, channel: ch }
258
+
259
+ after do
260
+ delete_queue q
261
+ delete_exchange x
262
+ sess.close
263
+ end
264
+
265
+ before do
266
+ consumer_class.ack true
267
+ q.bind(x)
268
+ 3.times { |n| x.publish({"n" => n}.to_json, content_type: "application/json") }
269
+ end
270
+
271
+ subject { Thread.current[:collected] }
272
+
273
+ it "should receive messages" do
274
+ Timeout.timeout(3) do
275
+ consumer_class.subscribe
276
+ end
277
+ expect(subject).to have(3).items
278
+ expect(subject.map(&:values).flatten).to eq [0,1,2]
279
+ end
280
+ end
281
+ end