vx-common-amqp 0.2.6

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 (38) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +14 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +276 -0
  7. data/README.md +29 -0
  8. data/Rakefile +6 -0
  9. data/bin/vx-consumers +12 -0
  10. data/lib/vx/common/amqp/cli.rb +88 -0
  11. data/lib/vx/common/amqp/config.rb +74 -0
  12. data/lib/vx/common/amqp/consumer/ack.rb +19 -0
  13. data/lib/vx/common/amqp/consumer/configuration.rb +119 -0
  14. data/lib/vx/common/amqp/consumer/publish.rb +32 -0
  15. data/lib/vx/common/amqp/consumer/subscribe.rb +67 -0
  16. data/lib/vx/common/amqp/consumer.rb +70 -0
  17. data/lib/vx/common/amqp/formatter.rb +105 -0
  18. data/lib/vx/common/amqp/mixins/callbacks.rb +35 -0
  19. data/lib/vx/common/amqp/mixins/logger.rb +17 -0
  20. data/lib/vx/common/amqp/session.rb +154 -0
  21. data/lib/vx/common/amqp/supervisor/threaded.rb +171 -0
  22. data/lib/vx/common/amqp/testing.rb +54 -0
  23. data/lib/vx/common/amqp/version.rb +7 -0
  24. data/lib/vx/common/amqp.rb +68 -0
  25. data/spec/integration/multi_threaded_spec.rb +89 -0
  26. data/spec/integration/threaded_supervisor_spec.rb +85 -0
  27. data/spec/lib/amqp/config_spec.rb +32 -0
  28. data/spec/lib/amqp/consumer_spec.rb +316 -0
  29. data/spec/lib/amqp/formatter_spec.rb +47 -0
  30. data/spec/lib/amqp/mixins/callbacks_spec.rb +26 -0
  31. data/spec/lib/amqp/session_spec.rb +144 -0
  32. data/spec/lib/amqp/supervisor/threaded_spec.rb +124 -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. data/vx-common-amqp.gemspec +30 -0
  38. metadata +178 -0
@@ -0,0 +1,32 @@
1
+ module Vx
2
+ module Common
3
+ module AMQP
4
+ module Consumer::Publish
5
+
6
+ def publish(message, options = nil)
7
+ session.open
8
+
9
+ options ||= {}
10
+ options[:routing_key] = routing_key if routing_key && !options.key?(:routing_key)
11
+ options[:headers] = headers if headers && !options.key?(:headers)
12
+ options[:content_type] ||= content_type || config.content_type
13
+
14
+ x = declare_exchange
15
+
16
+ run_callbacks(:publish, message: message, exchange: x, name: consumer_id) do
17
+ m = serialize_message message, options[:content_type]
18
+ x.publish m, options
19
+ end
20
+
21
+ debug "published #{message.inspect} to #{x.name}"
22
+ self
23
+ end
24
+
25
+ def serialize_message(message, content_type)
26
+ Common::AMQP::Formatter.pack(content_type, message)
27
+ end
28
+
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,67 @@
1
+ module Vx
2
+ module Common
3
+ module AMQP
4
+ module Consumer::Subscribe
5
+
6
+ def subscribe
7
+ session.open
8
+
9
+ session.with_channel do
10
+ x = declare_exchange
11
+ q = declare_queue
12
+
13
+ run_callbacks(:subscribe, exchange: x, queue: q, name: consumer_id) do
14
+ debug "subscribing to #{q.name}:#{x.name} using #{bind_options.inspect}"
15
+ q.bind(x, bind_options)
16
+ debug "successfuly subscribed to #{q.name}:#{x.name}"
17
+
18
+ subscription_loop q
19
+ end
20
+
21
+ debug "shutdown"
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def subscription_loop(q)
28
+ loop do
29
+ break if shutdown?
30
+
31
+ delivery_info, properties, payload = q.pop(ack: ack)
32
+
33
+ if payload
34
+ result = nil
35
+
36
+ debug "recieve ##{delivery_info.delivery_tag.to_i} #{payload.inspect}"
37
+ result = run_instance delivery_info, properties, payload
38
+ debug "done ##{delivery_info.delivery_tag.to_i}"
39
+
40
+ break if result == :shutdown
41
+ else
42
+ sleep config.pool_timeout
43
+ end
44
+ end
45
+ end
46
+
47
+ def run_instance(delivery_info, properties, payload)
48
+ payload = deserialize_message properties, payload
49
+
50
+ run_callbacks :recieve, payload: payload, name: consumer_id do
51
+ new.tap do |inst|
52
+ inst.properties = properties
53
+ inst.delivery_info = delivery_info
54
+ end.perform payload
55
+ end
56
+ end
57
+
58
+ def deserialize_message(properties, payload)
59
+ Common::AMQP::Formatter.unpack properties[:content_type],
60
+ model,
61
+ payload
62
+ end
63
+
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,70 @@
1
+ require 'ostruct'
2
+
3
+ module Vx
4
+ module Common
5
+ module AMQP
6
+ module Consumer
7
+
8
+ autoload :Configuration, File.expand_path("../consumer/configuration", __FILE__)
9
+ autoload :Publish, File.expand_path("../consumer/publish", __FILE__)
10
+ autoload :Subscribe, File.expand_path("../consumer/subscribe", __FILE__)
11
+ autoload :Sleep, File.expand_path("../consumer/sleep", __FILE__)
12
+ autoload :Ack, File.expand_path("../consumer/ack", __FILE__)
13
+
14
+ include Common::AMQP::Consumer::Ack
15
+ include Common::AMQP::Logger
16
+
17
+ attr_accessor :delivery_info
18
+ attr_accessor :properties
19
+ attr_accessor :channel
20
+
21
+ @@classes = []
22
+
23
+ def self.included(base)
24
+ base.extend ClassMethods
25
+ @@classes << base.to_s
26
+ end
27
+
28
+ def self.classes
29
+ @@classes
30
+ end
31
+
32
+ module ClassMethods
33
+
34
+ include Common::AMQP::Consumer::Configuration
35
+ include Common::AMQP::Consumer::Publish
36
+ include Common::AMQP::Consumer::Subscribe
37
+ include Common::AMQP::Logger
38
+ include Common::AMQP::Callbacks
39
+
40
+ def shutdown?
41
+ Common::AMQP.shutdown?
42
+ end
43
+
44
+ def shutdown
45
+ Common::AMQP.shutdown
46
+ end
47
+
48
+ def session
49
+ Common::AMQP.session
50
+ end
51
+
52
+ def config
53
+ Common::AMQP.config
54
+ end
55
+
56
+ private
57
+
58
+ def declare_exchange
59
+ session.declare_exchange(exchange_name, exchange_options)
60
+ end
61
+
62
+ def declare_queue
63
+ session.declare_queue(queue_name, queue_options)
64
+ end
65
+
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,105 @@
1
+ require 'json'
2
+ require 'stringio'
3
+
4
+ module Vx
5
+ module Common
6
+ module AMQP
7
+
8
+ class Formatter
9
+
10
+ @@formats = {}
11
+
12
+ class Format
13
+
14
+ attr_reader :content_type
15
+
16
+ def initialize(content_type)
17
+ @content_type = content_type
18
+ end
19
+
20
+ def pack(&block)
21
+ @pack = block if block_given?
22
+ @pack
23
+ end
24
+
25
+ def unpack(&block)
26
+ @unpack = block if block_given?
27
+ @unpack
28
+ end
29
+
30
+ end
31
+
32
+ class << self
33
+
34
+ def formats
35
+ @@formats
36
+ end
37
+
38
+ def define(content_type, &block)
39
+ fmt = Format.new content_type
40
+ fmt.instance_eval(&block)
41
+ formats.merge! content_type => fmt
42
+ end
43
+
44
+ def lookup(content_type)
45
+ formats[content_type]
46
+ end
47
+
48
+ def pack(content_type, body)
49
+ if fmt = lookup(content_type)
50
+ fmt.pack.call(body)
51
+ end
52
+ end
53
+
54
+ def unpack(content_type, model, body)
55
+ if fmt = lookup(content_type)
56
+ fmt.unpack.call(body, model)
57
+ end
58
+ end
59
+
60
+ end
61
+
62
+ define 'text/plain' do
63
+
64
+ pack do |body|
65
+ body.to_s
66
+ end
67
+
68
+ unpack do |body, _|
69
+ body
70
+ end
71
+ end
72
+
73
+ define 'application/json' do
74
+
75
+ pack do |body|
76
+ body.to_json
77
+ end
78
+
79
+ unpack do |payload, model|
80
+ if model && model.respond_to?(:from_json)
81
+ model.from_json payload
82
+ else
83
+ JSON.parse(payload)
84
+ end
85
+ end
86
+ end
87
+
88
+ define 'application/x-protobuf' do
89
+
90
+ pack do |object|
91
+ object.encode.to_s
92
+ end
93
+
94
+ unpack do |payload, model|
95
+ raise ModelDoesNotExists unless model
96
+ model.decode payload
97
+ end
98
+
99
+ class ModelDoesNotExists < Exception ; end
100
+ end
101
+ end
102
+
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,35 @@
1
+ module Vx
2
+ module Common
3
+ module AMQP
4
+ module Callbacks
5
+
6
+ def run_callbacks(name, *args)
7
+ before = "before_#{name}".to_sym
8
+ after = "after_#{name}".to_sym
9
+ if f = Common::AMQP.config.callbacks[before]
10
+ f.call(*args)
11
+ end
12
+
13
+ rs = yield if block_given?
14
+
15
+ if f = Common::AMQP.config.callbacks[after]
16
+ f.call(*args)
17
+ end
18
+
19
+ rs
20
+ end
21
+
22
+ def run_on_error_callback(e)
23
+ if f = Common::AMQP.config.callbacks[:on_error]
24
+ begin
25
+ f.call e
26
+ rescue Exception => e
27
+ $stderr.puts "ERROR on error callback: #{e.inspect}"
28
+ end
29
+ end
30
+ end
31
+
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,17 @@
1
+ module Vx
2
+ module Common
3
+ module AMQP
4
+ module Logger
5
+
6
+ %w{ debug info warn }.each do |m|
7
+ define_method m do |msg|
8
+ if log = Common::AMQP.logger
9
+ log.send(m, "[AMQP] #{msg}")
10
+ end
11
+ end
12
+ end
13
+
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,154 @@
1
+ require 'bunny'
2
+ require 'thread'
3
+
4
+ module Vx
5
+ module Common
6
+ module AMQP
7
+ class Session
8
+
9
+ include Common::AMQP::Logger
10
+
11
+ CHANNEL_KEY = :vx_amqp_channel
12
+
13
+ @@session_lock = Mutex.new
14
+
15
+ attr_reader :conn
16
+
17
+ class << self
18
+ def shutdown
19
+ @shutdown = true
20
+ end
21
+
22
+ def shutdown?
23
+ @shutdown == true
24
+ end
25
+
26
+ def resume
27
+ @shutdown = false
28
+ end
29
+ end
30
+
31
+ def close
32
+ if open?
33
+ @@session_lock.synchronize do
34
+ info "closing connection"
35
+ begin
36
+ conn.close
37
+ rescue Bunny::ChannelError => e
38
+ warn e
39
+ end
40
+ info "wait..."
41
+ while conn.status != :closed
42
+ sleep 0.01
43
+ end
44
+ @conn = nil
45
+ info "connection closed"
46
+ end
47
+ end
48
+ end
49
+
50
+ def open
51
+ return self if open?
52
+
53
+ @@session_lock.synchronize do
54
+ self.class.resume
55
+
56
+ @conn ||= Bunny.new config.url, heartbeat: :server
57
+
58
+ unless conn.open?
59
+ info "connecting to #{conn_info}"
60
+ conn.start
61
+ info "wait connection to #{conn_info}"
62
+ while conn.connecting?
63
+ sleep 0.01
64
+ end
65
+ info "connected successfuly (#{server_name})"
66
+ end
67
+ end
68
+
69
+ self
70
+ end
71
+
72
+ def open?
73
+ conn && conn.open? && conn.status == :open
74
+ end
75
+
76
+ def declare_exchange(name, options = nil)
77
+ assert_connection_is_open
78
+
79
+ options ||= {}
80
+ name ||= config.default_exchange_name
81
+ ch = options.delete(:channel) || channel
82
+ type, opts = get_exchange_type_and_options options
83
+ ch.exchange name, opts.merge(type: type)
84
+ end
85
+
86
+ def declare_queue(name, options = nil)
87
+ assert_connection_is_open
88
+
89
+ options ||= {}
90
+ ch = options.delete(:channel) || channel
91
+ name, opts = get_queue_name_and_options(name, options)
92
+ ch.queue name, opts
93
+ end
94
+
95
+ def channel
96
+ assert_connection_is_open
97
+
98
+ Thread.current[CHANNEL_KEY] || conn.default_channel
99
+ end
100
+
101
+ def with_channel
102
+ assert_connection_is_open
103
+
104
+ old,new = nil
105
+ begin
106
+ old,new = Thread.current[CHANNEL_KEY], conn.create_channel
107
+ Thread.current[CHANNEL_KEY] = new
108
+ yield
109
+ ensure
110
+ Thread.current[CHANNEL_KEY] = old
111
+ new.close if new && new.open?
112
+ end
113
+ end
114
+
115
+ def conn_info
116
+ if conn
117
+ "#{conn.user}:#{conn.host}:#{conn.port}/#{conn.vhost}"
118
+ end
119
+ end
120
+
121
+ def server_name
122
+ if conn
123
+ p = conn.server_properties || {}
124
+ "#{p["product"]}/#{p["version"]}"
125
+ end
126
+ end
127
+
128
+ def config
129
+ Common::AMQP.config
130
+ end
131
+
132
+ private
133
+
134
+ def get_exchange_type_and_options(options)
135
+ options = config.default_exchange_options.merge(options || {})
136
+ type = options.delete(:type) || config.default_exchange_type
137
+ [type, options]
138
+ end
139
+
140
+ def get_queue_name_and_options(name, options)
141
+ name ||= AMQ::Protocol::EMPTY_STRING
142
+ [name, config.default_queue_options.merge(options || {})]
143
+ end
144
+
145
+ def assert_connection_is_open
146
+ open? || raise(ConnectionDoesNotExist.new "you need to run #{to_s}#open")
147
+ end
148
+
149
+ class ConnectionDoesNotExist < ::Exception ; end
150
+
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,171 @@
1
+ require 'thread'
2
+
3
+ module Vx
4
+ module Common
5
+ module AMQP
6
+ class Supervisor::Threaded
7
+
8
+ include Common::AMQP::Logger
9
+ include Common::AMQP::Callbacks
10
+
11
+ POOL_INTERVAL = 0.5
12
+
13
+ Task = Struct.new(:object, :method, :id) do
14
+
15
+ attr_accessor :thread, :attempt, :start_at
16
+
17
+ def alive?
18
+ !!(thread && thread.alive?)
19
+ end
20
+
21
+ def inspect
22
+ %{#<Task
23
+ object=#{object.to_s}
24
+ method=#{method.inspect}
25
+ id=#{id.inspect}
26
+ alive=#{alive?}
27
+ attempt=#{attempt}
28
+ start_at=#{start_at}> }.gsub("\n", ' ').gsub(/ +/, ' ').strip
29
+ end
30
+ end
31
+
32
+ class SpawnAttemptsLimitReached < ::Exception ; end
33
+
34
+ class << self
35
+
36
+ @@shutdown = false
37
+
38
+ def build(tasks)
39
+ supervisor = new
40
+ tasks.each_pair do |k,v|
41
+ v.to_i.times do |n|
42
+ supervisor.add k, :subscribe, n
43
+ end
44
+ end
45
+ supervisor
46
+ end
47
+
48
+ def resume
49
+ @@shutdown = false
50
+ end
51
+
52
+ def shutdown?
53
+ @@shutdown
54
+ end
55
+
56
+ def shutdown
57
+ @@shutdown = true
58
+ end
59
+
60
+ end
61
+
62
+ def initialize
63
+ self.class.resume
64
+ @tasks = Array.new
65
+ end
66
+
67
+ def add(object, method, id)
68
+ @tasks.push Task.new(object, method, id).freeze
69
+ end
70
+
71
+ def size
72
+ @tasks.size
73
+ end
74
+
75
+ def shutdown?
76
+ self.class.shutdown?
77
+ end
78
+
79
+ def shutdown
80
+ self.class.shutdown
81
+ end
82
+
83
+ def run_async
84
+ Thread.new { run }.tap{|t| t.abort_on_exception = true }
85
+ end
86
+
87
+ def run
88
+ start_all_threads
89
+
90
+ loop do
91
+ task = @tasks.shift
92
+ break unless task
93
+
94
+ case
95
+ when shutdown?
96
+ log_thread_error task
97
+ when task.alive?
98
+ @tasks.push task
99
+ else
100
+ process_fail task
101
+ end
102
+
103
+ sleep POOL_INTERVAL unless shutdown?
104
+ end
105
+ end
106
+
107
+ private
108
+
109
+ def process_fail(task)
110
+ log_thread_error task
111
+ if check_attempt task
112
+ @tasks.push create_thread(task, task.attempt + 1)
113
+ else
114
+ raise SpawnAttemptsLimitReached
115
+ end
116
+ end
117
+
118
+ def start_all_threads
119
+ started_tasks = Array.new
120
+ while task = @tasks.shift
121
+ started_tasks.push create_thread(task, 0)
122
+ end
123
+ while task = started_tasks.shift
124
+ @tasks.push task
125
+ end
126
+ end
127
+
128
+ def create_thread(task, attempt)
129
+ attempt = 0 if reset_attempt?(task)
130
+ task.dup.tap do |new_task|
131
+ new_task.thread = Thread.new(new_task) do |t|
132
+ Thread.current[:vx_amqp_consumer_id] = t.id
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
+ run_on_error_callback(e)
153
+ e
154
+ end
155
+ end
156
+
157
+ def reset_attempt?(task)
158
+ return true unless task.start_at
159
+
160
+ interval = 60
161
+ (task.start_at + interval) < Time.now
162
+ end
163
+
164
+ def check_attempt(task)
165
+ task.attempt.to_i <= Common::AMQP.config.spawn_attempts.to_i
166
+ end
167
+
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,54 @@
1
+ require File.expand_path("../../amqp", __FILE__)
2
+
3
+ module Vx
4
+ module Common
5
+ module AMQP
6
+ module Testing
7
+
8
+ extend self
9
+
10
+ @@messages = Hash.new { |h,k| h[k] = [] }
11
+ @@messages_and_options = Hash.new { |h,k| h[k] = [] }
12
+
13
+ def messages
14
+ @@messages
15
+ end
16
+
17
+ def messages_and_options
18
+ @@messages_and_options
19
+ end
20
+
21
+ def clear
22
+ messages.clear
23
+ messages_and_options.clear
24
+ end
25
+ end
26
+
27
+ module Consumer::Publish
28
+ alias_method :real_publish, :publish
29
+
30
+ def publish(message, options = nil)
31
+ options ||= {}
32
+ Testing.messages[exchange_name] << message
33
+ Testing.messages_and_options[exchange_name] << [message, options]
34
+ self
35
+ end
36
+
37
+ end
38
+
39
+ module Consumer
40
+ module ClassMethods
41
+
42
+ def messages
43
+ Testing.messages[exchange_name]
44
+ end
45
+
46
+ def messages_and_options
47
+ Testing.messages_and_options[exchange_name]
48
+ end
49
+
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,7 @@
1
+ module Vx
2
+ module Common
3
+ module AMQP
4
+ VERSION = "0.2.6"
5
+ end
6
+ end
7
+ end