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.
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 +429 -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/message.rb +25 -65
  30. data/lib/mimi/messaging/version.rb +3 -1
  31. data/mimi-messaging.gemspec +25 -23
  32. metadata +34 -77
  33. data/lib/mimi/messaging/connection.rb +0 -181
  34. data/lib/mimi/messaging/listener.rb +0 -72
  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 -18
  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