mimi-messaging 0.1.12 → 1.2.2

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 +69 -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 +441 -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 +147 -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 -182
  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 -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 [Mimi::Messaging::Message]
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 [Mimi::Messaging::Message]
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 [Mimi::Messaging::Message]
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(method_name, message, opts) AND #call_query(...)"
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(event_type, message, 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(event_type, message, 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,147 @@
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
+ # Sends COMMAND to target
27
+ #
28
+ # @param target [String]
29
+ # @param message [Mimi::Messaging::Message]
30
+ # @param opts [Hash]
31
+ #
32
+ def command(target, message, opts = {})
33
+ raise ArgumentError, "Message is expected" unless message.is_a?(Mimi::Messaging::Message)
34
+ dispatch_command(target, message, opts)
35
+ nil
36
+ end
37
+
38
+ # Sends QUERY to target
39
+ #
40
+ # @param target [String]
41
+ # @param message [Mimi::Messaging::Message]
42
+ # @param opts [Hash]
43
+ #
44
+ def query(target, message, opts = {})
45
+ raise ArgumentError, "Message is expected" unless message.is_a?(Mimi::Messaging::Message)
46
+ response_serialized = dispatch_query(target, message, opts)
47
+ deserialize(response_serialized)
48
+ end
49
+
50
+ # Sends EVENT to target
51
+ #
52
+ # @param target [String]
53
+ # @param message [Mimi::Messaging::Message]
54
+ # @param opts [Hash]
55
+ #
56
+ def event(target, message, opts = {})
57
+ raise ArgumentError, "Message is expected" unless message.is_a?(Mimi::Messaging::Message)
58
+ dispatch_event(target, message, opts)
59
+ end
60
+
61
+ def start_request_processor(queue_name, processor, _opts = {})
62
+ super
63
+ request_processors[queue_name] ||= []
64
+ request_processors[queue_name] << processor
65
+ end
66
+
67
+ def start_event_processor(topic_name, processor, _opts = {})
68
+ super
69
+ event_processors[topic_name] ||= []
70
+ event_processors[topic_name] << processor
71
+ end
72
+
73
+ def start_event_processor_with_queue(topic_name, queue_name, processor, opts = {})
74
+ super
75
+ event_processors_with_queue[topic_name] ||= {}
76
+ event_processors_with_queue[topic_name][queue_name] ||= []
77
+ event_processors_with_queue[topic_name][queue_name] << processor
78
+ end
79
+
80
+ def stop_all_processors
81
+ @request_processors = {}
82
+ @event_processors = {}
83
+ @event_processors_with_queue = {}
84
+ end
85
+
86
+ private
87
+
88
+ # Simulates a transmitted message, following serialization/deserialization:
89
+ # message out -> message in
90
+ #
91
+ # @param message [Mimi::Messaging::Message]
92
+ # @return [Mimi::Messaging::Message]
93
+ #
94
+ def transmitted_message(message)
95
+ Mimi::Messaging::Message.new(
96
+ deserialize(serialize(message)),
97
+ message.headers
98
+ )
99
+ end
100
+
101
+ def dispatch_command(target, message, _opts = {})
102
+ queue_name, method_name = target.split("/")
103
+ return unless request_processors[queue_name]
104
+
105
+ # pick random processor serving the target
106
+ processor = request_processors[queue_name].sample
107
+ processor.call_command(method_name, transmitted_message(message), {})
108
+ end
109
+
110
+ def dispatch_query(target, message, _opts = {})
111
+ queue_name, method_name = target.split("/")
112
+ raise Timeout::Error unless request_processors[queue_name]
113
+
114
+ # pick random processor serving the target
115
+ processor = request_processors[queue_name].sample
116
+ response = processor.call_query(method_name, transmitted_message(message), {})
117
+ serialize(response)
118
+ end
119
+
120
+ def dispatch_event(target, message_serialized, _opts = {})
121
+ topic_name, event_type = target.split("#")
122
+ processors = event_processors[topic_name] || []
123
+ processor_queues = event_processors_with_queue[topic_name] || {}
124
+ processor_queues.values.each do |same_queue_processors|
125
+ processors << same_queue_processors.sample
126
+ end
127
+
128
+ processors.each do |processor|
129
+ processor.call_event(event_type, transmitted_message(message), {})
130
+ end
131
+ end
132
+
133
+ def request_processors
134
+ @request_processors ||= {}
135
+ end
136
+
137
+ def event_processors
138
+ @event_processors ||= {}
139
+ end
140
+
141
+ def event_processors_with_queue
142
+ @event_processors_with_queue ||= {}
143
+ end
144
+ end # class Memory
145
+ end # module Adapters
146
+ end # module Messaging
147
+ 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