evrone-common-amqp 0.0.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 +8 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +6 -0
- data/bin/amqp_consumers +12 -0
- data/evrone-common-amqp.gemspec +30 -0
- data/lib/evrone/common/amqp.rb +68 -0
- data/lib/evrone/common/amqp/cli.rb +88 -0
- data/lib/evrone/common/amqp/config.rb +74 -0
- data/lib/evrone/common/amqp/consumer.rb +70 -0
- data/lib/evrone/common/amqp/consumer/ack.rb +19 -0
- data/lib/evrone/common/amqp/consumer/configuration.rb +93 -0
- data/lib/evrone/common/amqp/consumer/publish.rb +32 -0
- data/lib/evrone/common/amqp/consumer/subscribe.rb +67 -0
- data/lib/evrone/common/amqp/formatter.rb +109 -0
- data/lib/evrone/common/amqp/mixins/logger.rb +17 -0
- data/lib/evrone/common/amqp/mixins/with_middleware.rb +16 -0
- data/lib/evrone/common/amqp/session.rb +154 -0
- data/lib/evrone/common/amqp/supervisor/threaded.rb +170 -0
- data/lib/evrone/common/amqp/testing.rb +46 -0
- data/lib/evrone/common/amqp/version.rb +7 -0
- data/spec/integration/multi_threaded_spec.rb +83 -0
- data/spec/integration/threaded_supervisor_spec.rb +85 -0
- data/spec/lib/amqp/consumer_spec.rb +281 -0
- data/spec/lib/amqp/formatter_spec.rb +47 -0
- data/spec/lib/amqp/mixins/with_middleware_spec.rb +32 -0
- data/spec/lib/amqp/session_spec.rb +144 -0
- data/spec/lib/amqp/supervisor/threaded_spec.rb +123 -0
- data/spec/lib/amqp_spec.rb +9 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/support/amqp.rb +15 -0
- data/spec/support/ignore_me_error.rb +1 -0
- metadata +175 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
module Evrone
|
2
|
+
module Common
|
3
|
+
module AMQP
|
4
|
+
module Consumer::Ack
|
5
|
+
|
6
|
+
def ack!(multiple = false)
|
7
|
+
self.class.session.channel.ack delivery_info.delivery_tag, multiple
|
8
|
+
debug "commit ##{delivery_info.delivery_tag.to_i}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def nack!(multiple = false, requeue = false)
|
12
|
+
self.class.session.channel.ack delivery_info.delivery_tag, multiple, requeue
|
13
|
+
debug "reject ##{delivery_info.delivery_tag.to_i}"
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
module Evrone
|
5
|
+
module Common
|
6
|
+
module AMQP
|
7
|
+
module Consumer::Configuration
|
8
|
+
|
9
|
+
@@consumer_configuration_lock = Mutex.new
|
10
|
+
|
11
|
+
def consumer_configuration
|
12
|
+
@consumer_configuration || reset_consumer_configuration!
|
13
|
+
end
|
14
|
+
|
15
|
+
def reset_consumer_configuration!
|
16
|
+
@@consumer_configuration_lock.synchronize do
|
17
|
+
@consumer_configuration =
|
18
|
+
OpenStruct.new(exchange: OpenStruct.new(options: {}),
|
19
|
+
queue: OpenStruct.new(options: {}),
|
20
|
+
consumer_name: make_consumer_name,
|
21
|
+
ack: false,
|
22
|
+
content_type: nil)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
%w{ exchange queue }.each do |m|
|
27
|
+
define_method m do |*name|
|
28
|
+
options = name.last.is_a?(Hash) ? name.pop : {}
|
29
|
+
consumer_configuration.__send__(m).name = name.first
|
30
|
+
consumer_configuration.__send__(m).options = options
|
31
|
+
end
|
32
|
+
|
33
|
+
define_method "#{m}_name" do
|
34
|
+
consumer_configuration.__send__(m).name || consumer_name
|
35
|
+
end
|
36
|
+
|
37
|
+
define_method "#{m}_options" do
|
38
|
+
consumer_configuration.__send__(m).options
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def routing_key(name = nil)
|
43
|
+
consumer_configuration.routing_key = name if name
|
44
|
+
consumer_configuration.routing_key
|
45
|
+
end
|
46
|
+
|
47
|
+
def headers(values = nil)
|
48
|
+
consumer_configuration.headers = values unless values == nil
|
49
|
+
consumer_configuration.headers
|
50
|
+
end
|
51
|
+
|
52
|
+
def model(value = nil)
|
53
|
+
consumer_configuration.model = value unless value == nil
|
54
|
+
consumer_configuration.model
|
55
|
+
end
|
56
|
+
|
57
|
+
def content_type(value = nil)
|
58
|
+
consumer_configuration.content_type = value if value
|
59
|
+
consumer_configuration.content_type
|
60
|
+
end
|
61
|
+
|
62
|
+
def ack(value = nil)
|
63
|
+
consumer_configuration.ack = value unless value == nil
|
64
|
+
consumer_configuration.ack
|
65
|
+
end
|
66
|
+
|
67
|
+
def consumer_name
|
68
|
+
consumer_configuration.consumer_name
|
69
|
+
end
|
70
|
+
|
71
|
+
def bind_options
|
72
|
+
consumer_configuration.bind_options ||
|
73
|
+
@@consumer_configuration_lock.synchronize do
|
74
|
+
opts = {}
|
75
|
+
opts[:routing_key] = routing_key if routing_key
|
76
|
+
opts[:headers] = headers if headers
|
77
|
+
consumer_configuration.bind_options = opts
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def make_consumer_name
|
84
|
+
to_s.scan(/[A-Z][a-z]*/).join("_")
|
85
|
+
.downcase
|
86
|
+
.gsub(/_/, '.')
|
87
|
+
.gsub(/\.consumer$/, '')
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Evrone
|
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
|
+
with_middleware :publishing, message: message, exchange: x do |opts|
|
17
|
+
m = serialize_message opts[: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 Evrone
|
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
|
+
with_middleware(:subscribing, exchange: x, queue: q) 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
|
+
with_middleware(:recieving, payload: payload) do |opts|
|
51
|
+
new.tap do |inst|
|
52
|
+
inst.properties = properties
|
53
|
+
inst.delivery_info = delivery_info
|
54
|
+
end.perform opts[: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,109 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
module Evrone
|
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 |body|
|
91
|
+
StringIO.open do |io|
|
92
|
+
body.serialize(io)
|
93
|
+
io.rewind
|
94
|
+
io.read
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
unpack do |payload, model|
|
99
|
+
raise ModelDoesNotExists unless model
|
100
|
+
m.parse payload
|
101
|
+
end
|
102
|
+
|
103
|
+
class ModelDoesNotExists < Exception ; end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Evrone
|
2
|
+
module Common
|
3
|
+
module AMQP
|
4
|
+
module WithMiddleware
|
5
|
+
def with_middleware(name, env, &block)
|
6
|
+
builder = Common::AMQP.config.public_send("#{name}_builder")
|
7
|
+
if builder
|
8
|
+
builder.to_app(block).call env
|
9
|
+
else
|
10
|
+
yield env
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'bunny'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
module Evrone
|
5
|
+
module Common
|
6
|
+
module AMQP
|
7
|
+
class Session
|
8
|
+
|
9
|
+
include Common::AMQP::Logger
|
10
|
+
|
11
|
+
CHANNEL_KEY = :evrone_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
|