mimi-messaging 0.1.9 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +66 -0
- data/README.md +68 -3
- data/TODO.md +8 -0
- data/docs/Messaging_Layer_Properties.md +141 -0
- data/docs/Why_HTTP_is_a_bad_choice.md +20 -0
- data/docs/diagrams/Pattern -- Command.drawio +1 -0
- data/docs/diagrams/Pattern -- Event direct.drawio +1 -0
- data/docs/diagrams/Pattern -- Event with Queue.drawio +1 -0
- data/docs/diagrams/Pattern -- Event.drawio +1 -0
- data/docs/diagrams/Pattern -- Query.drawio +1 -0
- data/docs/img/pattern--command.png +0 -0
- data/docs/img/pattern--event-direct.png +0 -0
- data/docs/img/pattern--event-using-queue.png +0 -0
- data/docs/img/pattern--event.png +0 -0
- data/docs/img/pattern--query.png +0 -0
- data/examples/basic_event_listener.rb +35 -0
- data/examples/basic_request_processor.rb +38 -0
- data/examples/using_messaging_low.rb +59 -0
- data/examples/using_pure_adapter.rb +62 -0
- data/lib/mimi/messaging.rb +429 -92
- data/lib/mimi/messaging/adapters.rb +22 -0
- data/lib/mimi/messaging/adapters/base.rb +233 -0
- data/lib/mimi/messaging/adapters/memory.rb +119 -0
- data/lib/mimi/messaging/adapters/test.rb +50 -0
- data/lib/mimi/messaging/errors.rb +24 -12
- data/lib/mimi/messaging/json_serializer.rb +45 -0
- data/lib/mimi/messaging/message.rb +25 -65
- data/lib/mimi/messaging/version.rb +3 -1
- data/mimi-messaging.gemspec +25 -23
- metadata +34 -77
- data/lib/mimi/messaging/connection.rb +0 -181
- data/lib/mimi/messaging/listener.rb +0 -72
- data/lib/mimi/messaging/mock.rb +0 -13
- data/lib/mimi/messaging/mock/connection.rb +0 -153
- data/lib/mimi/messaging/mock/request.rb +0 -18
- data/lib/mimi/messaging/mock/request_processor.rb +0 -92
- data/lib/mimi/messaging/model.rb +0 -27
- data/lib/mimi/messaging/model_provider.rb +0 -100
- data/lib/mimi/messaging/msgpack/msgpack_ext.rb +0 -14
- data/lib/mimi/messaging/msgpack/type_packer.rb +0 -104
- data/lib/mimi/messaging/notification.rb +0 -35
- data/lib/mimi/messaging/provider.rb +0 -48
- data/lib/mimi/messaging/request.rb +0 -56
- data/lib/mimi/messaging/request_processor.rb +0 -216
- data/lib/mimi/messaging/request_processor/context.rb +0 -39
- data/lib/mimi/messaging/request_processor/dsl.rb +0 -121
- data/lib/tasks/console_ext.rake +0 -6
- data/lib/tasks/console_helpers.rb +0 -116
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mimi
|
4
|
+
module Messaging
|
5
|
+
#
|
6
|
+
# Common namespace for all Mimi Messaging adapters
|
7
|
+
#
|
8
|
+
module Adapters
|
9
|
+
# Returns a Hash containing all registered adapters
|
10
|
+
#
|
11
|
+
# @return [Hash{String => Class < Mimi::Messaging::Adapters::Base}]
|
12
|
+
#
|
13
|
+
def self.registered_adapters
|
14
|
+
@registered_adapters ||= {}
|
15
|
+
end
|
16
|
+
end # module Adapters
|
17
|
+
end # module Messaging
|
18
|
+
end # module Mimi
|
19
|
+
|
20
|
+
require_relative "adapters/base"
|
21
|
+
require_relative "adapters/memory"
|
22
|
+
require_relative "adapters/test"
|
@@ -0,0 +1,233 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mimi
|
4
|
+
module Messaging
|
5
|
+
module Adapters
|
6
|
+
#
|
7
|
+
# An abstract messaging adapter.
|
8
|
+
#
|
9
|
+
# An adapter implementation must implement the following methods:
|
10
|
+
# * #start()
|
11
|
+
# * #stop()
|
12
|
+
# * #command(target, message, opts)
|
13
|
+
# * #query(target, message, opts)
|
14
|
+
# * #event(target, message, opts)
|
15
|
+
# * #start_request_processor(queue_name, processor, opts)
|
16
|
+
# * #start_event_processor(topic_name, processor, opts)
|
17
|
+
# * #start_event_processor_with_queue(topic_name, queue_name, processor, opts)
|
18
|
+
# * #stop_all_processors
|
19
|
+
#
|
20
|
+
# An adapter implementation must register itself using `.register_adapter_name` method.
|
21
|
+
#
|
22
|
+
class Base
|
23
|
+
attr_reader :serializer
|
24
|
+
|
25
|
+
# Registers adapter class with given adapter name
|
26
|
+
#
|
27
|
+
# @param adapter_name [String,Symbol]
|
28
|
+
#
|
29
|
+
def self.register_adapter_name(adapter_name)
|
30
|
+
adapter_name = adapter_name.to_s
|
31
|
+
if Mimi::Messaging::Adapters.registered_adapters.key?(adapter_name)
|
32
|
+
raise "Mimi::Messaging adapter '#{adapter_name}' is already registered"
|
33
|
+
end
|
34
|
+
|
35
|
+
Mimi::Messaging::Adapters.registered_adapters[adapter_name] = self
|
36
|
+
end
|
37
|
+
|
38
|
+
# Creates an Adapter instance
|
39
|
+
#
|
40
|
+
# @param params [Hash] adapter-specific configuration parameters
|
41
|
+
#
|
42
|
+
def initialize(params = {})
|
43
|
+
end
|
44
|
+
|
45
|
+
# Starts the adapter.
|
46
|
+
#
|
47
|
+
# All the message processors must be started after the adapter is started.
|
48
|
+
# Before the adapter is started it MAY respond with an error to an attempt
|
49
|
+
# to start a message processor.
|
50
|
+
#
|
51
|
+
# Serializer must be registered before any message is sent or received.
|
52
|
+
#
|
53
|
+
def start
|
54
|
+
raise "Method #start() is not implemented by #{self.class}"
|
55
|
+
end
|
56
|
+
|
57
|
+
# Stops all message processors and then stops the adapter.
|
58
|
+
#
|
59
|
+
def stop
|
60
|
+
raise "Method #stop() is not implemented by #{self.class}"
|
61
|
+
end
|
62
|
+
|
63
|
+
# Sends the command to the given target
|
64
|
+
#
|
65
|
+
# @param target [String] "<queue>/<method>"
|
66
|
+
# @param message [Hash]
|
67
|
+
# @param opts [Hash] additional options
|
68
|
+
#
|
69
|
+
# @return nil
|
70
|
+
# @raise [SomeError]
|
71
|
+
#
|
72
|
+
def command(_target, _message, _opts = {})
|
73
|
+
raise "Method #command(target, message, opts) is not implemented by #{self.class}"
|
74
|
+
end
|
75
|
+
|
76
|
+
# Executes the query to the given target and returns response
|
77
|
+
#
|
78
|
+
# @param target [String] "<queue>/<method>"
|
79
|
+
# @param message [Hash]
|
80
|
+
# @param opts [Hash] additional options, e.g. :timeout
|
81
|
+
#
|
82
|
+
# @return [Hash]
|
83
|
+
# @raise [SomeError,TimeoutError]
|
84
|
+
#
|
85
|
+
def query(_target, _message, _opts = {})
|
86
|
+
raise "Method #query(target, message, opts) is not implemented by #{self.class}"
|
87
|
+
end
|
88
|
+
|
89
|
+
# Broadcasts the event with the given target
|
90
|
+
#
|
91
|
+
# @param target [String] "<topic>/<event_type>", e.g. "customers/created"
|
92
|
+
# @param message [Hash]
|
93
|
+
# @param opts [Hash] additional options
|
94
|
+
#
|
95
|
+
def event(_target, _message, _opts = {})
|
96
|
+
raise "Method #event(target, message, opts) is not implemented by #{self.class}"
|
97
|
+
end
|
98
|
+
|
99
|
+
# Starts a request (command/query) processor.
|
100
|
+
#
|
101
|
+
# Processor must respond to #call_command() AND #call_query()
|
102
|
+
# which accepts 3 arguments: (method, message, opts).
|
103
|
+
#
|
104
|
+
# TBD: It must #ack! or #nack! the message.
|
105
|
+
#
|
106
|
+
# If the processor raises an error, the message will be NACK-ed and accepted again
|
107
|
+
# at a later time.
|
108
|
+
#
|
109
|
+
# NOTE: Method must be overloaded by a subclass.
|
110
|
+
#
|
111
|
+
# @param queue_name [String] "<queue>"
|
112
|
+
# @param processor [#call_command(),#call_query()]
|
113
|
+
# @param opts [Hash] additional adapter-specific options
|
114
|
+
#
|
115
|
+
def start_request_processor(_queue_name, processor, _opts = {})
|
116
|
+
# validates processor
|
117
|
+
if (
|
118
|
+
processor.respond_to?(:call_command) && processor.method(:call_command).arity >= 3 &&
|
119
|
+
processor.respond_to?(:call_query) && processor.method(:call_query).arity >= 3
|
120
|
+
)
|
121
|
+
return
|
122
|
+
end
|
123
|
+
|
124
|
+
raise(
|
125
|
+
ArgumentError,
|
126
|
+
"Invalid request processor passed to #{self.class}##{__method__}(), " \
|
127
|
+
"expected to respond to #call_command(...) AND #call_query(method_name, request, opts)"
|
128
|
+
)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Starts an event processor without a queue
|
132
|
+
#
|
133
|
+
# Processor must respond to #call_event() which accepts 3 arguments:
|
134
|
+
# (method, message, opts).
|
135
|
+
#
|
136
|
+
# TBD: It must #ack! or #nack! the message.
|
137
|
+
#
|
138
|
+
# If the processor raises an error, the message will be NACK-ed and accepted again
|
139
|
+
# at a later time.
|
140
|
+
#
|
141
|
+
# @param topic_name [String] "<topic>"
|
142
|
+
# @param processor [#call_event()]
|
143
|
+
# @param opts [Hash] additional adapter-specific options
|
144
|
+
#
|
145
|
+
def start_event_processor(_topic_name, processor, _opts = {})
|
146
|
+
# validates processor
|
147
|
+
return if processor.respond_to?(:call_event) && processor.method(:call_event).arity >= 3
|
148
|
+
|
149
|
+
raise(
|
150
|
+
ArgumentError,
|
151
|
+
"Invalid event processor passed to #{self.class}##{__method__}(), " \
|
152
|
+
"expected to respond to #call_event(method_name, request, opts)"
|
153
|
+
)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Starts an event processor with a queue
|
157
|
+
#
|
158
|
+
# Processor must respond to #call_event() which accepts 3 arguments:
|
159
|
+
# (method, message, opts).
|
160
|
+
#
|
161
|
+
# TBD: It must #ack! or #nack! the message.
|
162
|
+
#
|
163
|
+
# If the processor raises an error, the message will be NACK-ed and accepted again
|
164
|
+
# at a later time.
|
165
|
+
#
|
166
|
+
# @param topic_name [String] "<topic>"
|
167
|
+
# @param queue_name [String] "<queue>"
|
168
|
+
# @param processor [#call_event()]
|
169
|
+
# @param opts [Hash] additional adapter-specific options
|
170
|
+
#
|
171
|
+
def start_event_processor_with_queue(_topic_name, _queue_name, processor, _opts = {})
|
172
|
+
# validates processor
|
173
|
+
return if processor.respond_to?(:call_event) && processor.method(:call_event).arity >= 3
|
174
|
+
|
175
|
+
raise(
|
176
|
+
ArgumentError,
|
177
|
+
"Invalid event processor passed to #{self.class}##{__method__}(), " \
|
178
|
+
"expected to respond to #call_event(method_name, request, opts)"
|
179
|
+
)
|
180
|
+
end
|
181
|
+
|
182
|
+
# Registers the message serializer
|
183
|
+
#
|
184
|
+
# Message serializer must implement methods #serialize(Hash) -> String
|
185
|
+
# and #deserialize(String) -> Hash
|
186
|
+
#
|
187
|
+
# @param serializer [#serialize(),#deserialize()]
|
188
|
+
#
|
189
|
+
def register_message_serializer(serializer)
|
190
|
+
raise "Message serializer is already registered in #{self.class}" if @serializer
|
191
|
+
if !serializer.respond_to?(:serialize) || !serializer.respond_to?(:deserialize)
|
192
|
+
raise "Invalid message serializer passed to #{self.class}"
|
193
|
+
end
|
194
|
+
|
195
|
+
@serializer = serializer
|
196
|
+
end
|
197
|
+
|
198
|
+
# Stops all message (command, query and event) processors.
|
199
|
+
#
|
200
|
+
# Stops currently registered processors and stops accepting new messages
|
201
|
+
# for processors.
|
202
|
+
#
|
203
|
+
def stop_all_processors
|
204
|
+
raise "Method #stop_all_processors() is not implemented by #{self.class}"
|
205
|
+
end
|
206
|
+
|
207
|
+
protected
|
208
|
+
|
209
|
+
# Serializes a message (Hash) to be sent on-the-wire using configured serializer
|
210
|
+
#
|
211
|
+
# @param message [Hash]
|
212
|
+
# @return [String]
|
213
|
+
#
|
214
|
+
def serialize(message)
|
215
|
+
raise "Message serializer is not registered in #{self.class}" unless @serializer
|
216
|
+
|
217
|
+
@serializer.serialize(message)
|
218
|
+
end
|
219
|
+
|
220
|
+
# Deserializes a message (String) received on-the-wire to a Hash
|
221
|
+
#
|
222
|
+
# @param message [String]
|
223
|
+
# @return [Hash]
|
224
|
+
#
|
225
|
+
def deserialize(message)
|
226
|
+
raise "Message serializer is not registered in #{self.class}" unless @serializer
|
227
|
+
|
228
|
+
@serializer.deserialize(message)
|
229
|
+
end
|
230
|
+
end # class Base
|
231
|
+
end # module Adapters
|
232
|
+
end # module Messaging
|
233
|
+
end # module Mimi
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "timeout"
|
4
|
+
|
5
|
+
module Mimi
|
6
|
+
module Messaging
|
7
|
+
module Adapters
|
8
|
+
#
|
9
|
+
# A Memory is an in-memory implementation of a messaging adapter.
|
10
|
+
#
|
11
|
+
# All message dispatching happens within a single thread, the same as the caller's,
|
12
|
+
# so all ivocations are synchronous.
|
13
|
+
#
|
14
|
+
# The Memory purpose is only to use in tests and for the higher abstractions
|
15
|
+
# development.
|
16
|
+
#
|
17
|
+
class Memory < Base
|
18
|
+
register_adapter_name "memory"
|
19
|
+
|
20
|
+
def start
|
21
|
+
end
|
22
|
+
|
23
|
+
def stop
|
24
|
+
end
|
25
|
+
|
26
|
+
def command(target, message, opts = {})
|
27
|
+
message_serialized = serialize(message)
|
28
|
+
dispatch_command(target, message_serialized, opts)
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def query(target, message, opts = {})
|
33
|
+
message_serialized = serialize(message)
|
34
|
+
response_serialized = dispatch_query(target, message_serialized, opts)
|
35
|
+
deserialize(response_serialized)
|
36
|
+
end
|
37
|
+
|
38
|
+
def event(target, message, opts = {})
|
39
|
+
message_serialized = serialize(message)
|
40
|
+
dispatch_event(target, message_serialized, opts)
|
41
|
+
end
|
42
|
+
|
43
|
+
def start_request_processor(queue_name, processor, _opts = {})
|
44
|
+
super
|
45
|
+
request_processors[queue_name] ||= []
|
46
|
+
request_processors[queue_name] << processor
|
47
|
+
end
|
48
|
+
|
49
|
+
def start_event_processor(topic_name, processor, _opts = {})
|
50
|
+
super
|
51
|
+
event_processors[topic_name] ||= []
|
52
|
+
event_processors[topic_name] << processor
|
53
|
+
end
|
54
|
+
|
55
|
+
def start_event_processor_with_queue(topic_name, queue_name, processor, opts = {})
|
56
|
+
super
|
57
|
+
event_processors_with_queue[topic_name] ||= {}
|
58
|
+
event_processors_with_queue[topic_name][queue_name] ||= []
|
59
|
+
event_processors_with_queue[topic_name][queue_name] << processor
|
60
|
+
end
|
61
|
+
|
62
|
+
def stop_all_processors
|
63
|
+
@request_processors = {}
|
64
|
+
@event_processors = {}
|
65
|
+
@event_processors_with_queue = {}
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def dispatch_command(target, message_serialized, _opts = {})
|
71
|
+
queue_name, method_name = target.split("/")
|
72
|
+
message = deserialize(message_serialized)
|
73
|
+
return unless request_processors[queue_name]
|
74
|
+
|
75
|
+
# pick random processor serving the target
|
76
|
+
processor = request_processors[queue_name].sample
|
77
|
+
processor.call_command(method_name, message, {})
|
78
|
+
end
|
79
|
+
|
80
|
+
def dispatch_query(target, message_serialized, _opts = {})
|
81
|
+
queue_name, method_name = target.split("/")
|
82
|
+
message = deserialize(message_serialized)
|
83
|
+
raise Timeout::Error unless request_processors[queue_name]
|
84
|
+
|
85
|
+
# pick random processor serving the target
|
86
|
+
processor = request_processors[queue_name].sample
|
87
|
+
response = processor.call_query(method_name, message, {})
|
88
|
+
serialize(response)
|
89
|
+
end
|
90
|
+
|
91
|
+
def dispatch_event(target, message_serialized, _opts = {})
|
92
|
+
topic_name, event_type = target.split("/")
|
93
|
+
processors = event_processors[topic_name] || []
|
94
|
+
processor_queues = event_processors_with_queue[topic_name] || {}
|
95
|
+
processor_queues.values.each do |same_queue_processors|
|
96
|
+
processors << same_queue_processors.sample
|
97
|
+
end
|
98
|
+
|
99
|
+
message = deserialize(message_serialized)
|
100
|
+
processors.each do |processor|
|
101
|
+
processor.call_event(event_type, message, {})
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def request_processors
|
106
|
+
@request_processors ||= {}
|
107
|
+
end
|
108
|
+
|
109
|
+
def event_processors
|
110
|
+
@event_processors ||= {}
|
111
|
+
end
|
112
|
+
|
113
|
+
def event_processors_with_queue
|
114
|
+
@event_processors_with_queue ||= {}
|
115
|
+
end
|
116
|
+
end # class Memory
|
117
|
+
end # module Adapters
|
118
|
+
end # module Messaging
|
119
|
+
end # module Mimi
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "timeout"
|
4
|
+
|
5
|
+
module Mimi
|
6
|
+
module Messaging
|
7
|
+
module Adapters
|
8
|
+
#
|
9
|
+
# A Test is a mock adapter object for running tests.
|
10
|
+
#
|
11
|
+
class Test < Base
|
12
|
+
register_adapter_name "test"
|
13
|
+
|
14
|
+
def start
|
15
|
+
end
|
16
|
+
|
17
|
+
def stop
|
18
|
+
end
|
19
|
+
|
20
|
+
def command(target, message, opts = {})
|
21
|
+
end
|
22
|
+
|
23
|
+
def query(target, message, opts = {})
|
24
|
+
end
|
25
|
+
|
26
|
+
def event(target, message, opts = {})
|
27
|
+
end
|
28
|
+
|
29
|
+
def start_request_processor(queue_name, processor, _opts = {})
|
30
|
+
super
|
31
|
+
true
|
32
|
+
end
|
33
|
+
|
34
|
+
def start_event_processor(topic_name, processor, _opts = {})
|
35
|
+
super
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
def start_event_processor_with_queue(topic_name, queue_name, processor, opts = {})
|
40
|
+
super
|
41
|
+
true
|
42
|
+
end
|
43
|
+
|
44
|
+
def stop_all_processors
|
45
|
+
true
|
46
|
+
end
|
47
|
+
end # class Test
|
48
|
+
end # module Adapters
|
49
|
+
end # module Messaging
|
50
|
+
end # module Mimi
|
@@ -1,19 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Mimi
|
2
4
|
module Messaging
|
3
|
-
|
4
|
-
|
5
|
+
#
|
6
|
+
# Error definitions
|
7
|
+
#
|
8
|
+
|
9
|
+
# Generic error in the Messaging layer
|
10
|
+
#
|
11
|
+
# Base class for other more specific errors raised by Messaging layer
|
12
|
+
#
|
13
|
+
class Error < StandardError; end
|
5
14
|
|
6
|
-
|
7
|
-
|
15
|
+
# Configuration related errors
|
16
|
+
#
|
17
|
+
class ConfigurationError < Error; end
|
8
18
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
19
|
+
# Connection level error
|
20
|
+
#
|
21
|
+
# Raised on errors related to network level, e.g. message broker host not reachable
|
22
|
+
# or authentication/authorization at message broker failed.
|
23
|
+
#
|
24
|
+
class ConnectionError < Error; end
|
13
25
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
26
|
+
# An error raised to indicate that the message should be NACK-ed, but
|
27
|
+
# no additional error logging or processing should happen.
|
28
|
+
#
|
29
|
+
class NACK < Error; end
|
18
30
|
end # module Messaging
|
19
31
|
end # module Mimi
|