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.
@@ -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