mimi-messaging 0.1.10
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 +9 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +11 -0
- data/Rakefile +6 -0
- data/bin/console +7 -0
- data/bin/setup +8 -0
- data/lib/mimi/messaging.rb +124 -0
- data/lib/mimi/messaging/connection.rb +181 -0
- data/lib/mimi/messaging/errors.rb +19 -0
- data/lib/mimi/messaging/listener.rb +72 -0
- data/lib/mimi/messaging/message.rb +74 -0
- data/lib/mimi/messaging/mock.rb +13 -0
- data/lib/mimi/messaging/mock/connection.rb +153 -0
- data/lib/mimi/messaging/mock/request.rb +18 -0
- data/lib/mimi/messaging/mock/request_processor.rb +92 -0
- data/lib/mimi/messaging/model.rb +27 -0
- data/lib/mimi/messaging/model_provider.rb +100 -0
- data/lib/mimi/messaging/msgpack/msgpack_ext.rb +14 -0
- data/lib/mimi/messaging/msgpack/type_packer.rb +104 -0
- data/lib/mimi/messaging/notification.rb +35 -0
- data/lib/mimi/messaging/provider.rb +48 -0
- data/lib/mimi/messaging/request.rb +56 -0
- data/lib/mimi/messaging/request_processor.rb +216 -0
- data/lib/mimi/messaging/request_processor/context.rb +39 -0
- data/lib/mimi/messaging/request_processor/dsl.rb +121 -0
- data/lib/mimi/messaging/version.rb +5 -0
- data/lib/tasks/console_ext.rake +6 -0
- data/lib/tasks/console_helpers.rb +116 -0
- data/mimi-messaging.gemspec +39 -0
- metadata +196 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
module Mimi
|
2
|
+
module Messaging
|
3
|
+
class ConnectionError < StandardError
|
4
|
+
end # class ConnectionError
|
5
|
+
|
6
|
+
class RequestError < StandardError
|
7
|
+
attr_accessor :params
|
8
|
+
|
9
|
+
def initialize(message = 'failed to process request', params = {})
|
10
|
+
@message = message
|
11
|
+
@params = params.dup
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
@message
|
16
|
+
end
|
17
|
+
end # class RequestError
|
18
|
+
end # module Messaging
|
19
|
+
end # module Mimi
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Mimi
|
2
|
+
module Messaging
|
3
|
+
class Listener < RequestProcessor
|
4
|
+
DEFAULT_DURABLE_QUEUE_OPTIONS = {
|
5
|
+
exclusive: false,
|
6
|
+
durable: true,
|
7
|
+
auto_delete: false
|
8
|
+
}
|
9
|
+
|
10
|
+
DEFAULT_TEMPORARY_QUEUE_OPTIONS = {
|
11
|
+
exclusive: true,
|
12
|
+
durable: false,
|
13
|
+
auto_delete: true
|
14
|
+
}
|
15
|
+
|
16
|
+
abstract!
|
17
|
+
|
18
|
+
queue_options DEFAULT_TEMPORARY_QUEUE_OPTIONS
|
19
|
+
|
20
|
+
def self.queue(name = nil, opts = {})
|
21
|
+
return super unless name
|
22
|
+
queue_name(name)
|
23
|
+
if name && name != ''
|
24
|
+
queue_options(DEFAULT_DURABLE_QUEUE_OPTIONS.merge(opts))
|
25
|
+
else
|
26
|
+
queue_options(DEFAULT_TEMPORARY_QUEUE_OPTIONS.merge(opts))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.default_queue_name
|
31
|
+
''
|
32
|
+
end
|
33
|
+
|
34
|
+
# Sets or gets notification resource name
|
35
|
+
#
|
36
|
+
def self.notification(name = nil, _opts = {})
|
37
|
+
notification_name name
|
38
|
+
true
|
39
|
+
end
|
40
|
+
|
41
|
+
# Sets or gets queue name
|
42
|
+
#
|
43
|
+
def self.notification_name(name = nil)
|
44
|
+
if name && @notification_name
|
45
|
+
raise "#{self} has already registered '#{@notification_name}' as notification name"
|
46
|
+
end
|
47
|
+
(@notification_name ||= name) || default_notification_name
|
48
|
+
end
|
49
|
+
|
50
|
+
# Default (inferred) notification name
|
51
|
+
#
|
52
|
+
def self.default_notification_name
|
53
|
+
class_name_to_resource_name(name, 'listener')
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.resource_name
|
57
|
+
notification_name
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.request_type(_d, _m, _p)
|
61
|
+
:broadcast
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.construct_queue
|
65
|
+
exchange = channel.fanout(resource_name)
|
66
|
+
q = channel.create_queue(queue_name, queue_options)
|
67
|
+
q.bind(exchange)
|
68
|
+
q
|
69
|
+
end
|
70
|
+
end # class Listener
|
71
|
+
end # module Messaging
|
72
|
+
end # module Mimi
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'msgpack'
|
3
|
+
|
4
|
+
module Mimi
|
5
|
+
module Messaging
|
6
|
+
class Message < Hashie::Mash
|
7
|
+
def self.queue(name, _opts = {})
|
8
|
+
@queue_name = name
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.queue_name
|
12
|
+
@queue_name || default_queue_name
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.default_queue_name
|
16
|
+
Mimi::Messaging::RequestProcessor.class_name_to_resource_name(self, 'message')
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.get(name, data = {}, opts = {})
|
20
|
+
headers = {
|
21
|
+
method_name: name.to_s,
|
22
|
+
Mimi::Messaging::CONTEXT_ID_KEY => Mimi::Messaging.logger.context_id
|
23
|
+
}
|
24
|
+
_d, _m, response = Mimi::Messaging.get(
|
25
|
+
queue_name, encode(data), opts.deep_merge(headers: headers)
|
26
|
+
)
|
27
|
+
raise Timeout::Error unless response
|
28
|
+
message = new(decode(response))
|
29
|
+
raise RequestError.new(message.error, Message.new(message.params)) if message.error?
|
30
|
+
message
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.post(name, data = {}, opts = {})
|
34
|
+
headers = {
|
35
|
+
method_name: name.to_s,
|
36
|
+
Mimi::Messaging::CONTEXT_ID_KEY => Mimi::Messaging.logger.context_id
|
37
|
+
}
|
38
|
+
Mimi::Messaging.post(
|
39
|
+
queue_name, encode(data), opts.deep_merge(headers: headers)
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.add_method(name, &block)
|
44
|
+
self.class.instance_eval do
|
45
|
+
define_method(name, &block)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.methods(*names)
|
50
|
+
names.each do |method_name|
|
51
|
+
add_method(method_name) do |*params|
|
52
|
+
get(method_name, *params)
|
53
|
+
end
|
54
|
+
add_method("#{method_name}!") do |*params|
|
55
|
+
post(method_name, *params)
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.encode(data)
|
62
|
+
MessagePack.pack(data) # data.to_json
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.decode(raw_message)
|
66
|
+
MessagePack.unpack(raw_message) # JSON.parse(raw_message)
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_s
|
70
|
+
to_hash.to_s
|
71
|
+
end
|
72
|
+
end # class Message
|
73
|
+
end # module Messaging
|
74
|
+
end # module Mimi
|
@@ -0,0 +1,13 @@
|
|
1
|
+
#
|
2
|
+
# Replaces real Connection and Channel with MockConnection and MockChannel
|
3
|
+
# that stubs connections to RabbitMQ.
|
4
|
+
#
|
5
|
+
# Replaces RequestProcessor with MockRequestProcessor
|
6
|
+
#
|
7
|
+
|
8
|
+
require_relative 'mock/connection'
|
9
|
+
require_relative 'mock/request_processor'
|
10
|
+
require_relative 'mock/request'
|
11
|
+
|
12
|
+
Mimi::Messaging.send :remove_const, :Connection
|
13
|
+
Mimi::Messaging::Connection = Mimi::Messaging::MockConnection
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'bunny'
|
2
|
+
|
3
|
+
module Mimi
|
4
|
+
module Messaging
|
5
|
+
class MockConnection
|
6
|
+
attr_reader :queue_prefix
|
7
|
+
|
8
|
+
# Creates a Connection with given connection params
|
9
|
+
#
|
10
|
+
# @param params [Hash] Connection params as accepted by Bunny
|
11
|
+
# @param params[:queue_prefix] [String] (optional) Use this connection for all communication
|
12
|
+
# related to queues, having names starting with given
|
13
|
+
# prefix
|
14
|
+
#
|
15
|
+
def initialize(params = {})
|
16
|
+
@queue_prefix = params[:queue_prefix]
|
17
|
+
@channel_pool = {}
|
18
|
+
bunny_params = {
|
19
|
+
host: params[:mq_host],
|
20
|
+
port: params[:mq_port],
|
21
|
+
username: params[:mq_username],
|
22
|
+
password: params[:mq_password],
|
23
|
+
vhost: params[:mq_vhost]
|
24
|
+
}
|
25
|
+
@connection = { mock_connection: bunny_params }
|
26
|
+
end
|
27
|
+
|
28
|
+
# Starts the connection, opening actual connection to RabbitMQ
|
29
|
+
#
|
30
|
+
def start
|
31
|
+
@started = true
|
32
|
+
end
|
33
|
+
|
34
|
+
# Stops the connection
|
35
|
+
#
|
36
|
+
def stop
|
37
|
+
@channel_pool = {}
|
38
|
+
@started = false
|
39
|
+
end
|
40
|
+
|
41
|
+
def started?
|
42
|
+
@started
|
43
|
+
end
|
44
|
+
|
45
|
+
def channel
|
46
|
+
raise ConnectionError unless started?
|
47
|
+
@channel_pool[Thread.current.object_id] ||= create_channel
|
48
|
+
end
|
49
|
+
|
50
|
+
def create_channel(opts = {})
|
51
|
+
MockChannel.new(@connection, opts)
|
52
|
+
end
|
53
|
+
|
54
|
+
def reply_queue
|
55
|
+
raise ConnectionError unless started?
|
56
|
+
channel.reply_queue
|
57
|
+
end
|
58
|
+
|
59
|
+
def post(queue_name, raw_message, params = {})
|
60
|
+
channel.post(queue_name, raw_message, params)
|
61
|
+
end
|
62
|
+
|
63
|
+
def get(queue_name, raw_message, params = {})
|
64
|
+
channel.get(queue_name, raw_message, params)
|
65
|
+
end
|
66
|
+
|
67
|
+
def broadcast(queue_name, raw_message, params = {})
|
68
|
+
channel.broadcast(queue_name, raw_message, params)
|
69
|
+
end
|
70
|
+
|
71
|
+
class MockQueue
|
72
|
+
def initialize(*)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class MockChannel
|
77
|
+
attr_reader :options, :connection
|
78
|
+
|
79
|
+
DEFAULT_OPTIONS = {
|
80
|
+
concurrency: 1
|
81
|
+
}
|
82
|
+
DEFAULT_GET_TIMEOUT = 60 # seconds
|
83
|
+
|
84
|
+
def initialize(connection, opts = {})
|
85
|
+
@connection = connection
|
86
|
+
@options = DEFAULT_OPTIONS.merge(opts)
|
87
|
+
@channel = { mock_channel: @connection, opts: options[:concurrency] }
|
88
|
+
@mutex = Mutex.new
|
89
|
+
end
|
90
|
+
|
91
|
+
def create_queue(name, opts = {})
|
92
|
+
MockQueue.new(name, opts)
|
93
|
+
end
|
94
|
+
|
95
|
+
def reply_queue
|
96
|
+
@reply_queue ||= create_queue('', exclusive: true)
|
97
|
+
end
|
98
|
+
|
99
|
+
def ack(tag)
|
100
|
+
raise "Not implemented"
|
101
|
+
end
|
102
|
+
|
103
|
+
def fanout(name)
|
104
|
+
raise "Not implemented"
|
105
|
+
end
|
106
|
+
|
107
|
+
def active?
|
108
|
+
true
|
109
|
+
end
|
110
|
+
|
111
|
+
# Sends a raw RabbitMQ message to a given direct exchange
|
112
|
+
#
|
113
|
+
# @param queue_name [String] Queue name to send the message to
|
114
|
+
# @param raw_message [String]
|
115
|
+
# @param params [Hash] Message params (metadata)
|
116
|
+
#
|
117
|
+
def post(queue_name, raw_message, params = {})
|
118
|
+
true
|
119
|
+
end
|
120
|
+
|
121
|
+
# Sends a raw RabbitMQ message to a given direct exchange and listens for response
|
122
|
+
#
|
123
|
+
# @param queue_name [String] Queue name to send the message to
|
124
|
+
# @param raw_message [String]
|
125
|
+
# @param params [Hash] Message params (metadata)
|
126
|
+
#
|
127
|
+
# @param params[:timeout] [Integer] (optional) Timeout in seconds
|
128
|
+
#
|
129
|
+
# @return [nil,Array]
|
130
|
+
#
|
131
|
+
def get(queue_name, raw_message, params = {})
|
132
|
+
nil
|
133
|
+
end
|
134
|
+
|
135
|
+
# Sends a raw RabbitMQ message to a given fanout exchange
|
136
|
+
#
|
137
|
+
# @param fanout_name [String] Fanout exchange name to send the message to
|
138
|
+
# @param raw_message [String]
|
139
|
+
# @param params [Hash] Message params (metadata)
|
140
|
+
#
|
141
|
+
def broadcast(fanout_name, raw_message, params = {})
|
142
|
+
true
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
def publish(exchange, raw_message, params = {})
|
148
|
+
true
|
149
|
+
end
|
150
|
+
end # class Channel
|
151
|
+
end # class Connection
|
152
|
+
end # module Messaging
|
153
|
+
end # module Mimi
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Mimi
|
2
|
+
module Messaging
|
3
|
+
class Request
|
4
|
+
attr_reader :response
|
5
|
+
|
6
|
+
def send_response(data = {})
|
7
|
+
return if !get? || replied?
|
8
|
+
raise ArgumentError, 'Invalid response format, Hash is expected' unless data.is_a?(Hash)
|
9
|
+
raw_message = Mimi::Messaging::Message.encode(data)
|
10
|
+
@response = Mimi::Messaging::Message.decode(raw_message)
|
11
|
+
request_processor.connection.post(
|
12
|
+
reply_to_queue_name, raw_message, correlation_id: metadata[:correlation_id]
|
13
|
+
)
|
14
|
+
@replied = true
|
15
|
+
end
|
16
|
+
end # class Request
|
17
|
+
end # module Messaging
|
18
|
+
end # module Mimi
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Mimi
|
2
|
+
module Messaging
|
3
|
+
class RequestProcessor
|
4
|
+
attr_reader :result, :request
|
5
|
+
|
6
|
+
def self.started?
|
7
|
+
!@consumer.nil?
|
8
|
+
end
|
9
|
+
|
10
|
+
# Mock start
|
11
|
+
#
|
12
|
+
def self.start
|
13
|
+
return if abstract?
|
14
|
+
raise "#{name} already started" if started?
|
15
|
+
logger.debug "#{self} starting to serve '#{resource_name}' (#{exposed_methods})"
|
16
|
+
@queue = construct_queue
|
17
|
+
@consumer_mutex = Mutex.new
|
18
|
+
@consumer_mutex.synchronize do
|
19
|
+
@consumer = true
|
20
|
+
end
|
21
|
+
# consumer created, mutex released
|
22
|
+
end
|
23
|
+
|
24
|
+
# Mock stop
|
25
|
+
#
|
26
|
+
def self.stop
|
27
|
+
return if abstract?
|
28
|
+
raise "#{name} already stopped" unless started?
|
29
|
+
@consumer_mutex.synchronize do
|
30
|
+
@consumer = nil
|
31
|
+
end
|
32
|
+
@consumer = nil
|
33
|
+
@queue = nil
|
34
|
+
@channel = nil
|
35
|
+
@connection = nil
|
36
|
+
end
|
37
|
+
|
38
|
+
# MockRequestProcessor helper GET methods
|
39
|
+
#
|
40
|
+
# @param method_name [Symbol]
|
41
|
+
# @param message [Hash]
|
42
|
+
#
|
43
|
+
# @return [Mimi::Messaging::Message]
|
44
|
+
#
|
45
|
+
def self.get(method_name, message)
|
46
|
+
metadata = Mimi::Messaging::Message.new(
|
47
|
+
correlation_id: 1,
|
48
|
+
reply_to: 'mock_client',
|
49
|
+
headers: {
|
50
|
+
'method_name' => method_name.to_s,
|
51
|
+
'c' => 'mock-context'
|
52
|
+
}
|
53
|
+
)
|
54
|
+
d = Mimi::Messaging::Message.new()
|
55
|
+
raw_message = Mimi::Messaging::Message.new(message).to_msgpack
|
56
|
+
request_processor = new(d, metadata, raw_message)
|
57
|
+
request_processor.request.response
|
58
|
+
end
|
59
|
+
|
60
|
+
# MockRequestProcessor helper POST method
|
61
|
+
#
|
62
|
+
# @param method_name [Symbol]
|
63
|
+
# @param message [Hash]
|
64
|
+
#
|
65
|
+
# @return [Mimi::Messaging::Message]
|
66
|
+
#
|
67
|
+
def self.post(method_name, message)
|
68
|
+
metadata = Mimi::Messaging::Message.new(
|
69
|
+
headers: {
|
70
|
+
'method_name' => method_name.to_s,
|
71
|
+
'c' => 'context-id'
|
72
|
+
}
|
73
|
+
)
|
74
|
+
d = Mimi::Messaging::Message.new()
|
75
|
+
raw_message = Mimi::Messaging::Message.new(message)
|
76
|
+
request_processor = new(d, metadata, raw_message)
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
|
80
|
+
# MockRequestProcessor helper BROADCAST method
|
81
|
+
#
|
82
|
+
# @param method_name [Symbol]
|
83
|
+
# @param message [Hash]
|
84
|
+
#
|
85
|
+
# @return [Mimi::Messaging::Message]
|
86
|
+
#
|
87
|
+
def self.broadcast(method_name, message)
|
88
|
+
post(method_name, message)
|
89
|
+
end
|
90
|
+
end # class RequestProcessor
|
91
|
+
end # module Messaging
|
92
|
+
end # module Mimi
|