vx-lib-consumer 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,7 @@
1
+ module Vx
2
+ module Lib
3
+ module Consumer
4
+ VERSION = "0.2.1"
5
+ end
6
+ end
7
+ 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