evrone-common-amqp 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +8 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +29 -0
  8. data/Rakefile +6 -0
  9. data/bin/amqp_consumers +12 -0
  10. data/evrone-common-amqp.gemspec +30 -0
  11. data/lib/evrone/common/amqp.rb +68 -0
  12. data/lib/evrone/common/amqp/cli.rb +88 -0
  13. data/lib/evrone/common/amqp/config.rb +74 -0
  14. data/lib/evrone/common/amqp/consumer.rb +70 -0
  15. data/lib/evrone/common/amqp/consumer/ack.rb +19 -0
  16. data/lib/evrone/common/amqp/consumer/configuration.rb +93 -0
  17. data/lib/evrone/common/amqp/consumer/publish.rb +32 -0
  18. data/lib/evrone/common/amqp/consumer/subscribe.rb +67 -0
  19. data/lib/evrone/common/amqp/formatter.rb +109 -0
  20. data/lib/evrone/common/amqp/mixins/logger.rb +17 -0
  21. data/lib/evrone/common/amqp/mixins/with_middleware.rb +16 -0
  22. data/lib/evrone/common/amqp/session.rb +154 -0
  23. data/lib/evrone/common/amqp/supervisor/threaded.rb +170 -0
  24. data/lib/evrone/common/amqp/testing.rb +46 -0
  25. data/lib/evrone/common/amqp/version.rb +7 -0
  26. data/spec/integration/multi_threaded_spec.rb +83 -0
  27. data/spec/integration/threaded_supervisor_spec.rb +85 -0
  28. data/spec/lib/amqp/consumer_spec.rb +281 -0
  29. data/spec/lib/amqp/formatter_spec.rb +47 -0
  30. data/spec/lib/amqp/mixins/with_middleware_spec.rb +32 -0
  31. data/spec/lib/amqp/session_spec.rb +144 -0
  32. data/spec/lib/amqp/supervisor/threaded_spec.rb +123 -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. 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,17 @@
1
+ module Evrone
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,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