mimi-messaging-sqs_sns 0.3.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7a8b8bacfe2cc6db227168999349d0aa7b3e917e
4
- data.tar.gz: e83e778b72017c79d108c919f0495945ae8be41f
3
+ metadata.gz: b740db8e4dab81e38d1b84ecd40c508a67d4c4c8
4
+ data.tar.gz: b55b8188a57c5e0b8095ba133b27dad09f17f2a2
5
5
  SHA512:
6
- metadata.gz: 546272f3b86be0a2218f89051aa090eb0cf5d36addca02abb0f521f5fa4a90ec6dec5d72e5e56a3bb741ac6d54afb6d586d4836050013dad0c1d7781bfcb6633
7
- data.tar.gz: cb3d2f9547dd421f21ca9c2f8def098d1c81ba225e68b156ff4f2eeb44b1875f85d2347ff480d399fe0bc2e639db644d2ec72160edc8c9c4db8adfd9f31d8a0d
6
+ metadata.gz: c215ab899f4217ecd11e628f9739671d569da25cd2abd8ea3ce35572c5076b09941898f18b22f9d97926a9e29a93fb1729064919643ae0119a0a4f62c860b3d1
7
+ data.tar.gz: 28b7ff63ec0db04041fc173a329bf39a3179d16f50e3d72133f776806fb95c2de5572ec51f8b97681a507645a05021fd01df9b2e1d3f36feae98601f8c8f39ee
data/README.md CHANGED
@@ -32,6 +32,7 @@ Mimi::Messaging.configure(
32
32
  mq_aws_access_key_id: nil,
33
33
  mq_aws_secret_access_key: nil,
34
34
  mq_aws_sqs_endpoint: nil,
35
+ mq_aws_sns_endpoint: nil,
35
36
 
36
37
  mq_aws_sqs_read_timeout: 10, # seconds
37
38
  mq_namespace: nil,
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mimi/messaging/sqs_sns"
4
+
5
+ COUNT = 10
6
+ AWS_REGION = "eu-west-1"
7
+ AWS_SQS_ENDPOINT_URL = "http://localstack:4576"
8
+ AWS_SNS_ENDPOINT_URL = "http://localstack:4575"
9
+ AWS_ACCESS_KEY_ID = "foo"
10
+ AWS_SECRET_ACCESS_KEY = "bar"
11
+
12
+ logger = Logger.new(STDOUT)
13
+ logger.level = Logger::INFO
14
+ Mimi::Messaging.use(logger: logger, serializer: Mimi::Messaging::JsonSerializer)
15
+ Mimi::Messaging.configure(
16
+ mq_adapter: "sqs_sns",
17
+ mq_aws_access_key_id: AWS_ACCESS_KEY_ID,
18
+ mq_aws_secret_access_key: AWS_SECRET_ACCESS_KEY,
19
+ mq_aws_region: AWS_REGION,
20
+ mq_aws_sqs_endpoint: AWS_SQS_ENDPOINT_URL,
21
+ mq_aws_sns_endpoint: AWS_SNS_ENDPOINT_URL,
22
+ mq_log_at_level: :info
23
+ )
24
+ adapter = Mimi::Messaging.adapter
25
+
26
+ adapter.start
27
+
28
+ t_start = Time.now
29
+ COUNT.times do |i|
30
+ t = Time.now
31
+ puts "Publishing event: #{i}"
32
+ adapter.event("hello#tested", i: i) # rand(100))
33
+ sleep 1
34
+ end
@@ -5,6 +5,7 @@ require "mimi/messaging/sqs_sns"
5
5
  COUNT = 10
6
6
  AWS_REGION = "eu-west-1"
7
7
  AWS_SQS_ENDPOINT_URL = "http://localstack:4576"
8
+ AWS_SNS_ENDPOINT_URL = "http://localstack:4575"
8
9
  AWS_ACCESS_KEY_ID = "foo"
9
10
  AWS_SECRET_ACCESS_KEY = "bar"
10
11
 
@@ -16,7 +17,8 @@ Mimi::Messaging.configure(
16
17
  mq_aws_access_key_id: AWS_ACCESS_KEY_ID,
17
18
  mq_aws_secret_access_key: AWS_SECRET_ACCESS_KEY,
18
19
  mq_aws_region: AWS_REGION,
19
- mq_aws_sqs_endpoint: AWS_SQS_ENDPOINT_URL
20
+ mq_aws_sqs_endpoint: AWS_SQS_ENDPOINT_URL,
21
+ mq_aws_sns_endpoint: AWS_SNS_ENDPOINT_URL
20
22
  )
21
23
  adapter = Mimi::Messaging.adapter
22
24
 
@@ -4,6 +4,7 @@ require "mimi/messaging/sqs_sns"
4
4
 
5
5
  AWS_REGION = "eu-west-1"
6
6
  AWS_SQS_ENDPOINT_URL = "http://localstack:4576"
7
+ AWS_SNS_ENDPOINT_URL = "http://localstack:4575"
7
8
  AWS_ACCESS_KEY_ID = "foo"
8
9
  AWS_SECRET_ACCESS_KEY = "bar"
9
10
 
@@ -28,7 +29,8 @@ Mimi::Messaging.configure(
28
29
  mq_aws_access_key_id: AWS_ACCESS_KEY_ID,
29
30
  mq_aws_secret_access_key: AWS_SECRET_ACCESS_KEY,
30
31
  mq_aws_region: AWS_REGION,
31
- mq_aws_sqs_endpoint: AWS_SQS_ENDPOINT_URL
32
+ mq_aws_sqs_endpoint: AWS_SQS_ENDPOINT_URL,
33
+ mq_aws_sns_endpoint: AWS_SNS_ENDPOINT_URL
32
34
  )
33
35
  adapter = Mimi::Messaging.adapter
34
36
  queue_name = "test"
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mimi/messaging/sqs_sns"
4
+
5
+ AWS_REGION = "eu-west-1"
6
+ AWS_SQS_ENDPOINT_URL = "http://localstack:4576"
7
+ AWS_SNS_ENDPOINT_URL = "http://localstack:4575"
8
+ AWS_ACCESS_KEY_ID = "foo"
9
+ AWS_SECRET_ACCESS_KEY = "bar"
10
+
11
+ class Processor
12
+ def self.call_command(method_name, message, opts)
13
+ puts "COMMAND: #{method_name}, #{message}, headers: #{message.headers}"
14
+ end
15
+
16
+ def self.call_query(method_name, message, opts)
17
+ puts "QUERY: #{method_name}, #{message}, headers: #{message.headers}"
18
+ {}
19
+ end
20
+
21
+ def self.call_event(event_type, message, opts)
22
+ puts "EVENT: #{event_type}, #{message}, headers: #{message.headers}"
23
+ end
24
+ end # class Processor
25
+
26
+
27
+ logger = Logger.new(STDOUT)
28
+ logger.level = Logger::INFO
29
+ Mimi::Messaging.use(logger: logger, serializer: Mimi::Messaging::JsonSerializer)
30
+ Mimi::Messaging.configure(
31
+ mq_adapter: "sqs_sns",
32
+ mq_aws_access_key_id: AWS_ACCESS_KEY_ID,
33
+ mq_aws_secret_access_key: AWS_SECRET_ACCESS_KEY,
34
+ mq_aws_region: AWS_REGION,
35
+ mq_aws_sqs_endpoint: AWS_SQS_ENDPOINT_URL,
36
+ mq_aws_sns_endpoint: AWS_SNS_ENDPOINT_URL,
37
+ mq_log_at_level: :info
38
+ )
39
+ adapter = Mimi::Messaging.adapter
40
+
41
+ topic_name = "hello"
42
+ queue_name = "listener.hello"
43
+ adapter.start
44
+ puts "Registering event processor on '#{topic_name}'->'#{queue_name}'"
45
+ adapter.start_event_processor_with_queue(topic_name, queue_name, Processor)
46
+
47
+ begin
48
+ loop do
49
+ sleep 1
50
+ end
51
+ ensure
52
+ puts "Stopping adapter"
53
+ adapter.stop
54
+ end
55
+
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "mimi/messaging"
4
4
  require "aws-sdk-sqs"
5
+ require "aws-sdk-sns"
5
6
  require "timeout"
6
7
  require "securerandom"
7
8
 
@@ -23,20 +24,37 @@ module Mimi
23
24
  # * #stop_all_processors
24
25
  #
25
26
  class Adapter < Mimi::Messaging::Adapters::Base
26
- attr_reader :options, :sqs_client
27
+ #
28
+ # NOTE: AWS SQS/SNS alphabet for queue and topic names
29
+ # is different from what mimi-messaging allows:
30
+ # '.' is not an allowed character.
31
+ #
32
+ # SQS_SNS_ALPHABET_MAP structure is used to convert
33
+ # names from mimi-messaging alphabet to SQS/SNS alphabet.
34
+ #
35
+ # Mimi::Messaging still accepts queue and topic names
36
+ # containing the '.', but the adapter will convert those
37
+ # to valid SQS/SNS names using this mapping.
38
+ #
39
+ SQS_SNS_ALPHABET_MAP = {
40
+ "." => "-"
41
+ }.freeze
42
+
43
+ attr_reader :options, :sqs_client, :sns_client
27
44
 
28
45
  register_adapter_name "sqs_sns"
29
46
 
30
47
  DEFAULT_OPTIONS = {
31
48
  mq_namespace: nil,
32
49
  mq_default_query_timeout: 15, # seconds,
33
- mq_reply_queue_prefix: "reply.",
50
+ mq_reply_queue_prefix: "reply-",
34
51
 
35
52
  # if nil, AWS SDK will guess values from environment
36
53
  mq_aws_region: nil,
37
54
  mq_aws_access_key_id: nil,
38
55
  mq_aws_secret_access_key: nil,
39
56
  mq_aws_sqs_endpoint: nil,
57
+ mq_aws_sns_endpoint: nil,
40
58
 
41
59
  mq_aws_sqs_read_timeout: 10, # seconds
42
60
  }.freeze
@@ -59,11 +77,13 @@ module Mimi
59
77
 
60
78
  def start
61
79
  @sqs_client = Aws::SQS::Client.new(sqs_client_config)
80
+ @sns_client = Aws::SNS::Client.new(sns_client_config)
62
81
  end
63
82
 
64
83
  def stop
65
84
  stop_all_processors
66
85
  @sqs_client = nil
86
+ @sns_client = nil
67
87
  end
68
88
 
69
89
  # Stops all message (command, query and event) processors.
@@ -92,8 +112,8 @@ module Mimi
92
112
  def command(target, message, _opts = {})
93
113
  queue_name, method_name = target.split("/")
94
114
  message = Mimi::Messaging::Message.new(message, __method: method_name)
95
- queue_url = find_queue(queue_name)
96
- deliver_message(queue_url, message)
115
+ queue_url = find_queue!(queue_name)
116
+ deliver_message_queue(queue_url, message)
97
117
  end
98
118
 
99
119
  # Executes the query to the given target and returns response
@@ -107,7 +127,7 @@ module Mimi
107
127
  #
108
128
  def query(target, message, opts = {})
109
129
  queue_name, method_name = target.split("/")
110
- queue_url = find_queue(queue_name)
130
+ queue_url = find_queue!(queue_name)
111
131
  request_id = SecureRandom.hex(8)
112
132
  reply_queue = reply_consumer.register_request_id(request_id)
113
133
 
@@ -117,7 +137,7 @@ module Mimi
117
137
  __reply_queue_url: reply_consumer.reply_queue_url,
118
138
  __request_id: request_id
119
139
  )
120
- deliver_message(queue_url, message)
140
+ deliver_message_queue(queue_url, message)
121
141
  timeout = opts[:timeout] || options[:mq_default_query_timeout]
122
142
  response = nil
123
143
  Timeout::timeout(timeout) do
@@ -134,8 +154,9 @@ module Mimi
134
154
  #
135
155
  def event(target, message, _opts = {})
136
156
  topic_name, event_type = target.split("#")
137
-
138
- raise "Not implemented"
157
+ message = Mimi::Messaging::Message.new(message, __event_type: event_type)
158
+ topic_arn = find_or_create_topic(topic_name) # TODO: or find_topic!(...) ?
159
+ deliver_message_topic(topic_arn, message)
139
160
  end
140
161
 
141
162
  # Starts a request (command/query) processor.
@@ -168,7 +189,7 @@ module Mimi
168
189
  response,
169
190
  __request_id: message.headers[:__request_id]
170
191
  )
171
- deliver_message(reply_to, response_message)
192
+ deliver_message_queue(reply_to, response_message)
172
193
  else
173
194
  processor.call_command(method_name, message, {})
174
195
  end
@@ -176,25 +197,25 @@ module Mimi
176
197
  end
177
198
 
178
199
  def start_event_processor(topic_name, processor, opts = {})
200
+ # NOTE: due to SQS/SNS limitations, implementing this will
201
+ # require creating a temporary queue and subscribing it to the topic
179
202
  raise "Not implemented"
180
203
  end
181
204
 
182
205
  def start_event_processor_with_queue(topic_name, queue_name, processor, opts = {})
183
- raise "Not implemented"
184
- end
185
-
186
- # Creates a new queue
187
- #
188
- # @param queue_name [String] name of the topic to be created
189
- # @return [String] a new queue URL
190
- #
191
- def create_queue(queue_name)
192
- fqn = full_queue_name(queue_name)
193
- Mimi::Messaging.log "Creating a queue: #{fqn}"
194
- result = sqs_client.create_queue(queue_name: fqn)
195
- result.queue_url
196
- rescue StandardError => e
197
- raise Mimi::Messaging::ConnectionError, "Failed to create queue '#{queue_name}': #{e}"
206
+ @consumers ||= []
207
+ opts = opts.dup
208
+ topic_arn = find_or_create_topic(topic_name) # TODO: or find_topic!(...) ?
209
+ queue_url = find_or_create_queue(queue_name)
210
+ subscribe_topic_queue(topic_arn, queue_url)
211
+ @consumers << Consumer.new(self, queue_url) do |m|
212
+ message = Mimi::Messaging::Message.new(
213
+ deserialize(m.body),
214
+ deserialize_headers(m)
215
+ )
216
+ event_type = message.headers[:__event_type]
217
+ processor.call_event(event_type, message, {})
218
+ end
198
219
  end
199
220
 
200
221
  private
@@ -213,12 +234,40 @@ module Mimi
213
234
  params.compact
214
235
  end
215
236
 
237
+ # Returns configuration parameters for AWS SNS client
238
+ #
239
+ # @return [Hash]
240
+ #
241
+ def sns_client_config
242
+ params = {
243
+ region: options[:mq_aws_region],
244
+ endpoint: options[:mq_aws_sns_endpoint],
245
+ access_key_id: options[:mq_aws_access_key_id],
246
+ secret_access_key: options[:mq_aws_secret_access_key]
247
+ }
248
+ params.compact
249
+ end
250
+
251
+ # Creates a new queue
252
+ #
253
+ # @param queue_name [String] name of the topic to be created
254
+ # @return [String] a new queue URL
255
+ #
256
+ def create_queue(queue_name)
257
+ fqn = sqs_sns_converted_full_name(queue_name)
258
+ Mimi::Messaging.log "Creating a queue: #{fqn}"
259
+ result = sqs_client.create_queue(queue_name: fqn)
260
+ result.queue_url
261
+ rescue StandardError => e
262
+ raise Mimi::Messaging::ConnectionError, "Failed to create queue '#{queue_name}': #{e}"
263
+ end
264
+
216
265
  # Delivers a message to a queue with given URL.
217
266
  #
218
267
  # @param queue_url [String]
219
268
  # @param message [Mimi::Messaging::Message]
220
269
  #
221
- def deliver_message(queue_url, message)
270
+ def deliver_message_queue(queue_url, message)
222
271
  raise ArgumentError, "Non-empty queue URL is expected" unless queue_url
223
272
  unless message.is_a?(Mimi::Messaging::Message)
224
273
  raise ArgumentError, "Message is expected as argument"
@@ -243,7 +292,7 @@ module Mimi
243
292
  # @return [String,nil] queue URL
244
293
  #
245
294
  def queue_registry(queue_name)
246
- fqn = full_queue_name(queue_name)
295
+ fqn = sqs_sns_converted_full_name(queue_name)
247
296
  @queue_registry ||= {}
248
297
  @queue_registry[fqn] ||= begin
249
298
  result = sqs_client.get_queue_url(queue_name: fqn)
@@ -253,13 +302,18 @@ module Mimi
253
302
  nil
254
303
  end
255
304
 
256
- # Converts a queue name to a fully qualified queue name
305
+ # Converts a topic or queue name to a fully qualified (with namespace)
306
+ # and in a valid SQS/SNS alphabet.
257
307
  #
258
- # @param queue_name [String]
259
- # @return [String]
308
+ # @param name [String] a mimi-messaging valid name
309
+ # @return [String] an SQS/SNS valid name
260
310
  #
261
- def full_queue_name(queue_name)
262
- "#{options[:mq_namespace]}#{queue_name}"
311
+ def sqs_sns_converted_full_name(name)
312
+ name = "#{options[:mq_namespace]}#{name}"
313
+ SQS_SNS_ALPHABET_MAP.each do |from, to|
314
+ name = name.gsub(from, to)
315
+ end
316
+ name
263
317
  end
264
318
 
265
319
  # Finds a queue URL for a queue with a given name,
@@ -268,7 +322,7 @@ module Mimi
268
322
  # @param queue_name [String]
269
323
  # @return [String] a queue URL
270
324
  #
271
- def find_queue(queue_name)
325
+ def find_queue!(queue_name)
272
326
  queue_registry(queue_name) || (
273
327
  raise Mimi::Messaging::ConnectionError,
274
328
  "Failed to find a queue with given name: '#{queue_name}'"
@@ -307,6 +361,125 @@ module Mimi
307
361
  def deserialize_headers(message)
308
362
  message.message_attributes.to_h.map { |k, v| [k.to_sym, v.string_value] }.to_h
309
363
  end
364
+
365
+ # Lists all SNS topics by their ARNs.
366
+ #
367
+ # NOTE: iterates over all topics at SNS every time
368
+ #
369
+ # @return [Array<String>] array of topic ARNs
370
+ #
371
+ def sns_list_topics
372
+ result = []
373
+ next_token = nil
374
+ loop do
375
+ response = sns_client.list_topics(next_token: next_token)
376
+ result += response.topics.map(&:topic_arn)
377
+ next_token = response.next_token
378
+ break unless next_token
379
+ end
380
+ result
381
+ rescue StandardError => e
382
+ raise Mimi::Messaging::ConnectionError, "Failed to list topics: #{e}"
383
+ end
384
+
385
+ # Returns ARN of a topic with a given name.
386
+ #
387
+ # If the topic with given name does not exist, returns nil
388
+ #
389
+ # @param topic_name [String]
390
+ # @return [String,nil] topic ARN or nil, if not found
391
+ #
392
+ def topic_registry(topic_name)
393
+ fqn = sqs_sns_converted_full_name(topic_name)
394
+ @topic_registry ||= {}
395
+ @topic_registry[fqn] ||= begin
396
+ sns_list_topics.find { |topic_arn| topic_arn.split(":").last == fqn }
397
+ end
398
+ end
399
+
400
+ # Finds a topic ARN for a topic with a given name,
401
+ # or raises an error if the topic is not found.
402
+ #
403
+ # @param topic_name [String]
404
+ # @return [String] a topic ARN
405
+ #
406
+ def find_topic!(topic_name)
407
+ topic_registry(topic_name) || (
408
+ raise Mimi::Messaging::ConnectionError,
409
+ "Failed to find a topic with given name: '#{topic_name}'"
410
+ )
411
+ end
412
+
413
+ # Finds a topic ARN for a topic with given name.
414
+ #
415
+ # If an existing topic with this name is not found,
416
+ # the method will try to create a new one.
417
+ #
418
+ # @param topic_name [String]
419
+ # @return [String] a topic ARN
420
+ #
421
+ def find_or_create_topic(topic_name)
422
+ topic_registry(topic_name) || create_topic(topic_name)
423
+ end
424
+
425
+ # Creates a new topic
426
+ #
427
+ # @param topic_name [String] name of the topic to be created
428
+ # @return [String] a new topic ARN
429
+ #
430
+ def create_topic(topic_name)
431
+ fqn = sqs_sns_converted_full_name(topic_name)
432
+ Mimi::Messaging.log "Creating a topic: #{fqn}"
433
+ result = sns_client.create_topic(name: fqn)
434
+ result.topic_arn
435
+ rescue StandardError => e
436
+ raise Mimi::Messaging::ConnectionError, "Failed to create topic '#{topic_name}': #{e}"
437
+ end
438
+
439
+ # Subscribes an existing queue to an existing topic
440
+ #
441
+ # @param topic_arn [String]
442
+ # @param queue_url [String]
443
+ #
444
+ def subscribe_topic_queue(topic_arn, queue_url)
445
+ result = sqs_client.get_queue_attributes(
446
+ queue_url: queue_url, attribute_names: ["QueueArn"]
447
+ )
448
+ queue_arn = result.attributes["QueueArn"]
449
+ Mimi::Messaging.log "Subscribing queue to a topic: '#{topic_arn}'->'#{queue_url}'"
450
+ result = sns_client.subscribe(
451
+ topic_arn: topic_arn,
452
+ protocol: "sqs",
453
+ endpoint: queue_arn,
454
+ attributes: { "RawMessageDelivery" => "true" }
455
+ )
456
+ true
457
+ rescue StandardError => e
458
+ raise Mimi::Messaging::ConnectionError,
459
+ "Failed to subscribe queue to topic '#{topic_arn}'->'#{queue_url}': #{e}"
460
+ end
461
+
462
+ # Delivers a message to a topic with given ARN.
463
+ #
464
+ # @param topic_arn [String]
465
+ # @param message [Mimi::Messaging::Message]
466
+ #
467
+ def deliver_message_topic(topic_arn, message)
468
+ raise ArgumentError, "Non-empty topic ARN is expected" unless topic_arn
469
+ unless message.is_a?(Mimi::Messaging::Message)
470
+ raise ArgumentError, "Message is expected as argument"
471
+ end
472
+ Mimi::Messaging.log "Delivering message to: #{topic_arn}"
473
+ sns_client.publish(
474
+ topic_arn: topic_arn,
475
+ message: serialize(message),
476
+ message_attributes: message.headers.map do |k, v|
477
+ [k.to_s, { data_type: "String", string_value: v.to_s }]
478
+ end.to_h
479
+ )
480
+ rescue StandardError => e
481
+ raise Mimi::Messaging::ConnectionError, "Failed to deliver message to '#{topic_arn}': #{e}"
482
+ end
310
483
  end # class Adapter
311
484
  end # module SQS_SNS
312
485
  end # module Messaging
@@ -3,7 +3,7 @@
3
3
  module Mimi
4
4
  module Messaging
5
5
  module SQS_SNS
6
- VERSION = "0.3.0"
6
+ VERSION = "0.4.0"
7
7
  end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mimi-messaging-sqs_sns
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Kukushkin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-10-09 00:00:00.000000000 Z
11
+ date: 2019-10-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mimi-messaging
@@ -125,8 +125,10 @@ files:
125
125
  - Rakefile
126
126
  - bin/console
127
127
  - bin/setup
128
+ - examples/event.rb
128
129
  - examples/query.rb
129
130
  - examples/responder.rb
131
+ - examples/subscriber.rb
130
132
  - lib/mimi/messaging/sqs_sns.rb
131
133
  - lib/mimi/messaging/sqs_sns/adapter.rb
132
134
  - lib/mimi/messaging/sqs_sns/consumer.rb