vx-lib-consumer 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +22 -0
- data/README.md +30 -0
- data/Rakefile +1 -0
- data/lib/vx/lib/consumer/ack.rb +44 -0
- data/lib/vx/lib/consumer/configuration.rb +69 -0
- data/lib/vx/lib/consumer/error.rb +10 -0
- data/lib/vx/lib/consumer/instrument.rb +27 -0
- data/lib/vx/lib/consumer/params.rb +80 -0
- data/lib/vx/lib/consumer/publish.rb +47 -0
- data/lib/vx/lib/consumer/rpc.rb +217 -0
- data/lib/vx/lib/consumer/serializer.rb +90 -0
- data/lib/vx/lib/consumer/session.rb +168 -0
- data/lib/vx/lib/consumer/subscribe.rb +105 -0
- data/lib/vx/lib/consumer/subscriber.rb +84 -0
- data/lib/vx/lib/consumer/testing.rb +49 -0
- data/lib/vx/lib/consumer/version.rb +7 -0
- data/lib/vx/lib/consumer.rb +138 -0
- data/spec/lib/consumer_spec.rb +210 -0
- data/spec/lib/rpc_spec.rb +67 -0
- data/spec/lib/serializer_spec.rb +43 -0
- data/spec/lib/session_spec.rb +29 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/support/beefcake_test_message.rb +8 -0
- data/spec/support/test_consumers.rb +59 -0
- data/vx-lib-consumer.gemspec +27 -0
- metadata +150 -0
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'bunny'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
module Vx
|
5
|
+
module Lib
|
6
|
+
module Consumer
|
7
|
+
class Session
|
8
|
+
|
9
|
+
include Instrument
|
10
|
+
|
11
|
+
@@session_lock = Mutex.new
|
12
|
+
|
13
|
+
@@shutdown_lock = Mutex.new
|
14
|
+
@@shutdown = ConditionVariable.new
|
15
|
+
@@live = false
|
16
|
+
|
17
|
+
attr_reader :conn
|
18
|
+
|
19
|
+
def shutdown
|
20
|
+
@@shutdown_lock.synchronize do
|
21
|
+
@@live = false
|
22
|
+
@@shutdown.broadcast
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def live?
|
27
|
+
@@live
|
28
|
+
end
|
29
|
+
|
30
|
+
def resume
|
31
|
+
@@live = true
|
32
|
+
end
|
33
|
+
|
34
|
+
def wait_shutdown(timeout = nil)
|
35
|
+
@@shutdown_lock.synchronize do
|
36
|
+
@@shutdown.wait(@@shutdown_lock, timeout)
|
37
|
+
not live?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def close
|
42
|
+
if open?
|
43
|
+
@@session_lock.synchronize do
|
44
|
+
instrument("closing_connection", info: conn_info)
|
45
|
+
|
46
|
+
instrument("close_connection", info: conn_info) do
|
47
|
+
begin
|
48
|
+
conn.close
|
49
|
+
while conn.status != :closed
|
50
|
+
sleep 0.01
|
51
|
+
end
|
52
|
+
rescue Bunny::ChannelError, Bunny::ClientTimeout => e
|
53
|
+
Consumer.exception_handler(e, {})
|
54
|
+
end
|
55
|
+
end
|
56
|
+
@conn = nil
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def open(options = {})
|
62
|
+
return self if open?
|
63
|
+
|
64
|
+
@@session_lock.synchronize do
|
65
|
+
unless open?
|
66
|
+
resume
|
67
|
+
|
68
|
+
@conn ||= Bunny.new(
|
69
|
+
nil, # from ENV['RABBITMQ_URL']
|
70
|
+
heartbeat: Consumer.configuration.heartbeat,
|
71
|
+
automatically_recover: false
|
72
|
+
)
|
73
|
+
|
74
|
+
instrumentation = { info: conn_info }.merge(options)
|
75
|
+
|
76
|
+
instrument("start_connecting", instrumentation)
|
77
|
+
|
78
|
+
instrument("connect", instrumentation) do
|
79
|
+
conn.start
|
80
|
+
while conn.connecting?
|
81
|
+
sleep 0.01
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
self
|
88
|
+
end
|
89
|
+
|
90
|
+
def open?
|
91
|
+
conn && conn.open? && conn.status == :open
|
92
|
+
end
|
93
|
+
|
94
|
+
def conn_info
|
95
|
+
if conn
|
96
|
+
"amqp://#{conn.user}@#{conn.host}:#{conn.port}/#{conn.vhost}"
|
97
|
+
else
|
98
|
+
"not connected"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def with_pub_channel
|
103
|
+
key = :vx_consumer_session_pub_channel
|
104
|
+
if ch = Thread.current[key]
|
105
|
+
yield ch
|
106
|
+
else
|
107
|
+
conn.with_channel do |c|
|
108
|
+
yield c
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def allocate_pub_channel
|
114
|
+
assert_connection_is_open
|
115
|
+
|
116
|
+
key = :vx_consumer_session_pub_channel
|
117
|
+
|
118
|
+
if Thread.current[key]
|
119
|
+
yield
|
120
|
+
else
|
121
|
+
ch = conn.create_channel
|
122
|
+
assign_error_handlers_to_channel(ch)
|
123
|
+
Thread.current[key] = ch
|
124
|
+
begin
|
125
|
+
yield
|
126
|
+
ensure
|
127
|
+
ch = Thread.current[key]
|
128
|
+
ch.close if ch.open?
|
129
|
+
Thread.current[key] = nil
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def declare_exchange(ch, name, options = nil)
|
135
|
+
assert_connection_is_open
|
136
|
+
|
137
|
+
options ||= {}
|
138
|
+
options = {} if name == ''.freeze
|
139
|
+
|
140
|
+
ch.exchange name, options
|
141
|
+
end
|
142
|
+
|
143
|
+
def declare_queue(ch, name, options = nil)
|
144
|
+
assert_connection_is_open
|
145
|
+
|
146
|
+
options ||= {}
|
147
|
+
ch.queue name, options
|
148
|
+
end
|
149
|
+
|
150
|
+
def assign_error_handlers_to_channel(ch)
|
151
|
+
ch.on_uncaught_exception {|e, c| ::Vx::Lib::Consumer.exception_handler(e, consumer: c) }
|
152
|
+
ch.on_error {|e, c| ::Vx::Lib::Consumer.exception_handler(e, consumer: c) }
|
153
|
+
end
|
154
|
+
|
155
|
+
private
|
156
|
+
|
157
|
+
def assert_connection_is_open
|
158
|
+
open? || raise(ConnectionDoesNotExistError)
|
159
|
+
end
|
160
|
+
|
161
|
+
def config
|
162
|
+
Consumer.configuration
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module Vx
|
2
|
+
module Lib
|
3
|
+
module Consumer
|
4
|
+
module Subscribe
|
5
|
+
|
6
|
+
def subscribe(options = {})
|
7
|
+
ch, q = bind(options)
|
8
|
+
|
9
|
+
subscriber = Subscriber.new(
|
10
|
+
ch,
|
11
|
+
q,
|
12
|
+
ch.generate_consumer_tag,
|
13
|
+
!params.ack
|
14
|
+
)
|
15
|
+
subscriber.vx_consumer_name = params.consumer_name
|
16
|
+
subscriber.queue_name = q.name
|
17
|
+
|
18
|
+
subscriber.on_delivery do |delivery_info, properties, payload|
|
19
|
+
handle_delivery ch, delivery_info, properties, payload
|
20
|
+
end
|
21
|
+
|
22
|
+
q.subscribe_with(subscriber)
|
23
|
+
end
|
24
|
+
|
25
|
+
def handle_delivery(channel, delivery_info, properties, payload)
|
26
|
+
payload = decode_payload properties, payload
|
27
|
+
|
28
|
+
instrumentation = {
|
29
|
+
consumer: params.consumer_name,
|
30
|
+
payload: payload,
|
31
|
+
properties: properties,
|
32
|
+
channel: channel.id
|
33
|
+
}
|
34
|
+
|
35
|
+
with_middlewares :sub, instrumentation do
|
36
|
+
instrument("start_processing", instrumentation)
|
37
|
+
instrument("process", instrumentation) do
|
38
|
+
allocate_pub_channel do
|
39
|
+
run_instance delivery_info, properties, payload, channel
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def run_instance(delivery_info, properties, payload, channel)
|
46
|
+
new.tap do |inst|
|
47
|
+
inst.properties = properties
|
48
|
+
inst.delivery_info = delivery_info
|
49
|
+
inst._channel = channel
|
50
|
+
end.perform payload
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def decode_payload(properties, payload)
|
56
|
+
Serializer.unpack(properties[:content_type], payload, params.model)
|
57
|
+
end
|
58
|
+
|
59
|
+
def bind(options = {})
|
60
|
+
qname = options[:queue] || params.queue_name
|
61
|
+
|
62
|
+
instrumentation = {
|
63
|
+
consumer: params.consumer_name
|
64
|
+
}
|
65
|
+
|
66
|
+
session.open
|
67
|
+
|
68
|
+
ch = session.conn.create_channel
|
69
|
+
session.assign_error_handlers_to_channel(ch)
|
70
|
+
ch.prefetch configuration.prefetch
|
71
|
+
|
72
|
+
x_name = ''
|
73
|
+
|
74
|
+
if params.exchange_name != ''
|
75
|
+
x = session.declare_exchange ch, params.exchange_name, params.exchange_options
|
76
|
+
x_name = x.name
|
77
|
+
end
|
78
|
+
|
79
|
+
q = session.declare_queue ch, qname, params.queue_options
|
80
|
+
|
81
|
+
instrumentation.merge!(
|
82
|
+
exchange: x_name,
|
83
|
+
queue: q.name,
|
84
|
+
queue_options: params.queue_options,
|
85
|
+
)
|
86
|
+
|
87
|
+
if x_name != ''
|
88
|
+
instrumentation.merge!(
|
89
|
+
exchange_options: params.exchange_options,
|
90
|
+
bind: params.bind_options
|
91
|
+
)
|
92
|
+
instrument("bind_queue", instrumentation) do
|
93
|
+
q.bind(x, params.bind_options)
|
94
|
+
end
|
95
|
+
else
|
96
|
+
instrument("using_default_exchange", instrumentation)
|
97
|
+
end
|
98
|
+
|
99
|
+
[ch, q]
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'bunny/consumer'
|
3
|
+
|
4
|
+
module Vx
|
5
|
+
module Lib
|
6
|
+
module Consumer
|
7
|
+
class Subscriber < Bunny::Consumer
|
8
|
+
|
9
|
+
include Instrument
|
10
|
+
|
11
|
+
attr_accessor :vx_consumer_name, :queue_name
|
12
|
+
|
13
|
+
def initialize(*args)
|
14
|
+
super(*args)
|
15
|
+
@lock = Mutex.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def graceful_shutdown
|
19
|
+
instrument('try_graceful_shutdown_consumer', consumer: vx_consumer_name)
|
20
|
+
in_progress do
|
21
|
+
cancel
|
22
|
+
instrument('graceful_shutdown_consumer', consumer: vx_consumer_name)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def try_graceful_shutdown
|
27
|
+
if @lock.try_lock
|
28
|
+
begin
|
29
|
+
instrument('graceful_shutdown_consumer', consumer: vx_consumer_name)
|
30
|
+
cancel
|
31
|
+
ensure
|
32
|
+
@lock.unlock
|
33
|
+
end
|
34
|
+
true
|
35
|
+
else
|
36
|
+
false
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def in_progress
|
41
|
+
@lock.synchronize do
|
42
|
+
yield
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def running?
|
47
|
+
@lock.locked?
|
48
|
+
end
|
49
|
+
|
50
|
+
def call(*args)
|
51
|
+
in_progress do
|
52
|
+
@on_delivery.call(*args) if @on_delivery
|
53
|
+
sleep 0
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def cancel
|
58
|
+
instrument('cancel_consumer', consumer: vx_consumer_name, channel: channel.id)
|
59
|
+
unless closed?
|
60
|
+
super
|
61
|
+
channel.close unless closed?
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def closed?
|
66
|
+
channel.closed?
|
67
|
+
end
|
68
|
+
|
69
|
+
def join
|
70
|
+
channel.work_pool.join
|
71
|
+
end
|
72
|
+
|
73
|
+
def wait_shutdown
|
74
|
+
Thread.new do
|
75
|
+
Thread.current.abort_on_exception = true
|
76
|
+
Consumer.wait_shutdown
|
77
|
+
cancel
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require File.expand_path("../../consumer", __FILE__)
|
2
|
+
|
3
|
+
module Vx
|
4
|
+
module Lib
|
5
|
+
module Consumer
|
6
|
+
|
7
|
+
module Testing
|
8
|
+
|
9
|
+
extend self
|
10
|
+
|
11
|
+
@@messages = Hash.new { |h,k| h[k] = [] }
|
12
|
+
@@messages_and_options = Hash.new { |h,k| h[k] = [] }
|
13
|
+
|
14
|
+
def messages
|
15
|
+
@@messages
|
16
|
+
end
|
17
|
+
|
18
|
+
def messages_and_options
|
19
|
+
@@messages_and_options
|
20
|
+
end
|
21
|
+
|
22
|
+
def clear
|
23
|
+
messages.clear
|
24
|
+
messages_and_options.clear
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
module Consumer::Publish
|
29
|
+
|
30
|
+
def publish(message, options = nil)
|
31
|
+
options ||= {}
|
32
|
+
Testing.messages[params.exchange_name] << message
|
33
|
+
Testing.messages_and_options[params.exchange_name] << [message, options]
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
def messages
|
38
|
+
Testing.messages[params.exchange_name]
|
39
|
+
end
|
40
|
+
|
41
|
+
def messages_and_options
|
42
|
+
Testing.messages_and_options[params.exchange_name]
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
%w{
|
2
|
+
version
|
3
|
+
error
|
4
|
+
configuration
|
5
|
+
instrument
|
6
|
+
session
|
7
|
+
params
|
8
|
+
serializer
|
9
|
+
subscriber
|
10
|
+
publish
|
11
|
+
subscribe
|
12
|
+
ack
|
13
|
+
rpc
|
14
|
+
}.each do |f|
|
15
|
+
require File.expand_path("../consumer/#{f}", __FILE__)
|
16
|
+
end
|
17
|
+
|
18
|
+
module Vx
|
19
|
+
module Lib
|
20
|
+
module Consumer
|
21
|
+
|
22
|
+
attr_accessor :properties
|
23
|
+
attr_accessor :delivery_info
|
24
|
+
attr_accessor :_channel
|
25
|
+
|
26
|
+
def self.included(base)
|
27
|
+
base.extend ClassMethods
|
28
|
+
base.extend Instrument
|
29
|
+
base.extend Publish
|
30
|
+
base.extend Subscribe
|
31
|
+
base.extend Rpc
|
32
|
+
base.send :include, Ack
|
33
|
+
base.send :include, Instrument
|
34
|
+
end
|
35
|
+
|
36
|
+
module ClassMethods
|
37
|
+
def params
|
38
|
+
@params ||= Params.new(self.name)
|
39
|
+
end
|
40
|
+
|
41
|
+
def exchange(*args)
|
42
|
+
params.exchange_options = args.last.is_a?(Hash) ? args.pop : nil
|
43
|
+
params.exchange_name = args.first
|
44
|
+
end
|
45
|
+
|
46
|
+
def fanout
|
47
|
+
params.exchange_type = :fanout
|
48
|
+
end
|
49
|
+
|
50
|
+
def topic
|
51
|
+
params.exchange_type = :topic
|
52
|
+
end
|
53
|
+
|
54
|
+
def direct
|
55
|
+
params.exchange_type = :direct
|
56
|
+
end
|
57
|
+
|
58
|
+
def queue(*args)
|
59
|
+
params.queue_options = args.last.is_a?(Hash) ? args.pop : nil
|
60
|
+
params.queue_name = args.first
|
61
|
+
end
|
62
|
+
|
63
|
+
def routing_key(name)
|
64
|
+
params.routing_key = name
|
65
|
+
end
|
66
|
+
|
67
|
+
def headers(value)
|
68
|
+
params.headers = value
|
69
|
+
end
|
70
|
+
|
71
|
+
def content_type(value)
|
72
|
+
params.content_type = value
|
73
|
+
end
|
74
|
+
|
75
|
+
def ack
|
76
|
+
params.ack = true
|
77
|
+
end
|
78
|
+
|
79
|
+
def model(value)
|
80
|
+
params.model = value
|
81
|
+
end
|
82
|
+
|
83
|
+
def session
|
84
|
+
Consumer.session
|
85
|
+
end
|
86
|
+
|
87
|
+
def allocate_pub_channel
|
88
|
+
Consumer.session.allocate_pub_channel { yield }
|
89
|
+
end
|
90
|
+
|
91
|
+
def configuration
|
92
|
+
Consumer.configuration
|
93
|
+
end
|
94
|
+
|
95
|
+
def with_middlewares(name, env, &block)
|
96
|
+
Consumer.configuration.builders[name].to_app(block).call(env)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
extend self
|
101
|
+
|
102
|
+
@@session = Session.new
|
103
|
+
@@configuration = Configuration.new
|
104
|
+
|
105
|
+
def shutdown
|
106
|
+
session.shutdown
|
107
|
+
end
|
108
|
+
|
109
|
+
def live?
|
110
|
+
session.live?
|
111
|
+
end
|
112
|
+
|
113
|
+
def wait_shutdown(timeout = nil)
|
114
|
+
session.wait_shutdown(timeout)
|
115
|
+
end
|
116
|
+
|
117
|
+
def configure
|
118
|
+
yield configuration
|
119
|
+
end
|
120
|
+
|
121
|
+
def configuration
|
122
|
+
@@configuration
|
123
|
+
end
|
124
|
+
|
125
|
+
def session
|
126
|
+
@@session
|
127
|
+
end
|
128
|
+
|
129
|
+
def exception_handler(e, env)
|
130
|
+
unless env.is_a?(Hash)
|
131
|
+
env = {env: env}
|
132
|
+
end
|
133
|
+
configuration.on_error.call(e, env)
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|