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