vx-common-amqp 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
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