mimi-messaging 0.1.12 → 1.0.0

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +66 -0
  4. data/README.md +68 -3
  5. data/TODO.md +8 -0
  6. data/docs/Messaging_Layer_Properties.md +141 -0
  7. data/docs/Why_HTTP_is_a_bad_choice.md +20 -0
  8. data/docs/diagrams/Pattern -- Command.drawio +1 -0
  9. data/docs/diagrams/Pattern -- Event direct.drawio +1 -0
  10. data/docs/diagrams/Pattern -- Event with Queue.drawio +1 -0
  11. data/docs/diagrams/Pattern -- Event.drawio +1 -0
  12. data/docs/diagrams/Pattern -- Query.drawio +1 -0
  13. data/docs/img/pattern--command.png +0 -0
  14. data/docs/img/pattern--event-direct.png +0 -0
  15. data/docs/img/pattern--event-using-queue.png +0 -0
  16. data/docs/img/pattern--event.png +0 -0
  17. data/docs/img/pattern--query.png +0 -0
  18. data/examples/basic_event_listener.rb +35 -0
  19. data/examples/basic_request_processor.rb +38 -0
  20. data/examples/using_messaging_low.rb +59 -0
  21. data/examples/using_pure_adapter.rb +62 -0
  22. data/lib/mimi/messaging.rb +428 -92
  23. data/lib/mimi/messaging/adapters.rb +22 -0
  24. data/lib/mimi/messaging/adapters/base.rb +233 -0
  25. data/lib/mimi/messaging/adapters/memory.rb +119 -0
  26. data/lib/mimi/messaging/adapters/test.rb +50 -0
  27. data/lib/mimi/messaging/errors.rb +24 -12
  28. data/lib/mimi/messaging/json_serializer.rb +45 -0
  29. data/lib/mimi/messaging/version.rb +3 -1
  30. data/mimi-messaging.gemspec +25 -23
  31. metadata +34 -78
  32. data/lib/mimi/messaging/connection.rb +0 -182
  33. data/lib/mimi/messaging/listener.rb +0 -72
  34. data/lib/mimi/messaging/message.rb +0 -74
  35. data/lib/mimi/messaging/mock.rb +0 -13
  36. data/lib/mimi/messaging/mock/connection.rb +0 -153
  37. data/lib/mimi/messaging/mock/request.rb +0 -19
  38. data/lib/mimi/messaging/mock/request_processor.rb +0 -92
  39. data/lib/mimi/messaging/model.rb +0 -27
  40. data/lib/mimi/messaging/model_provider.rb +0 -100
  41. data/lib/mimi/messaging/msgpack/msgpack_ext.rb +0 -14
  42. data/lib/mimi/messaging/msgpack/type_packer.rb +0 -104
  43. data/lib/mimi/messaging/notification.rb +0 -35
  44. data/lib/mimi/messaging/provider.rb +0 -48
  45. data/lib/mimi/messaging/request.rb +0 -56
  46. data/lib/mimi/messaging/request_processor.rb +0 -216
  47. data/lib/mimi/messaging/request_processor/context.rb +0 -39
  48. data/lib/mimi/messaging/request_processor/dsl.rb +0 -121
  49. data/lib/tasks/console_ext.rake +0 -6
  50. 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
- class ConnectionError < StandardError
4
- end # class ConnectionError
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
- class RequestError < StandardError
7
- attr_accessor :params
15
+ # Configuration related errors
16
+ #
17
+ class ConfigurationError < Error; end
8
18
 
9
- def initialize(message = 'failed to process request', params = {})
10
- @message = message
11
- @params = params.dup
12
- end
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
- def to_s
15
- @message
16
- end
17
- end # class RequestError
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