vx-lib-consumer 0.2.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.
- 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
|