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