mimi-messaging-sqs_sns 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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