mimi-messaging-sqs_sns 0.4.2 → 0.8.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
- SHA1:
3
- metadata.gz: 8da1599ea4862a2ecf1ffac4628caf6ac0b5c45c
4
- data.tar.gz: ecb91516097a0f74d72796ac96a4b412779451d4
2
+ SHA256:
3
+ metadata.gz: cf7944b9a3e10f154f738d66544cfa63611b17a742b156460969f70821ad86bb
4
+ data.tar.gz: f26e3e3623208170de192174c9c0fdb2ef5e26fa8cca831e73065eb86e21b0b5
5
5
  SHA512:
6
- metadata.gz: 64a71c2ee788913e5977e4cabb1694458053c3622b4d52ed67be265b66f970a0665615176c28219944f1b7abfb0fe4c18261eab37f155a1968c6991210548e3e
7
- data.tar.gz: 02bcf69c4f6cf193498954a80548f96c19ee7fb97dff2a8ec5876f077c43f2bd3d10a0266909985a62801d137effcee65a1da3abb8a34dbcde74d26114168fa7
6
+ metadata.gz: 37bbb87ba16df4886a5e525ca13732b3712021c5269418d8d8c90e1af125cbaa7e32aa5757a96293569a1717cc4c00542aaf20d12f3fc7514477cd3d708cd615
7
+ data.tar.gz: 9d53cc0479c47af711f0e89203f9ef1ced767115743fb2fa685ea559ebb0b891226a248219ff2103ec95207b678814c1c0b983f82a22eda8e741feac01a2d4dc
data/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ ## UNRELEASED
4
+
5
+ ## v0.8.0
6
+
7
+ * [#3](https://github.com/kukushkin/mimi-messaging-sqs_sns/pull/3)
8
+ * Added a worker pool:
9
+ * now processing of messages from a single queue can be done in multiple parallel threads (workers)
10
+ * the worker threads are shared across all message consumers which read messages from different queues
11
+ * the size of the worker pool is limited, to have at most `mq_worker_pool_max_threads` processing messages in parallel
12
+ * `mq_worker_pool_min_threads` determines the target minimal number of threads in the pool, waiting for new messages
13
+ * `mq_worker_pool_max_backlog` controls how many messages which are read from SQS queues can be put in the worker pool backlog; if a new message is read from SQS queue and the backlog is full, this message is NACK-ed (put back into SQS queue for the other consumers to process)
14
+ * Improved thread-safety of the adapter: reply consumer, TimeoutQueue#pop
15
+
16
+ ## v0.7.0
17
+
18
+ * [#1](https://github.com/kukushkin/mimi-messaging-sqs_sns/pull/1)
19
+ * Added KMS support for creating queues/topics with sever-side encryption enabled
20
+ * Optimized stopping of the adapter: stopping all consumers in parallel
21
+
22
+
23
+ ## v0.6.x
24
+
25
+ * Basic functionality implemented, see missing features in [TODO](TODO.md)
data/README.md CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  AWS SQS/SNS adapter for [mimi-messaging](https://github.com/kukushkin/mimi-messaging).
4
4
 
5
+ * [Changes](CHANGELOG.md)
6
+
7
+
5
8
  ## Installation
6
9
 
7
10
  Add this line to your application's Gemfile:
@@ -43,7 +46,6 @@ Mimi::Messaging.configure(
43
46
  Mimi::Messaging.start
44
47
  ```
45
48
 
46
-
47
49
  ## Contributing
48
50
 
49
51
  Bug reports and pull requests are welcome on GitHub at https://github.com/kukushkin/mimi-messaging-sqs_sns. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
data/TODO.md ADDED
@@ -0,0 +1,9 @@
1
+ # TODO
2
+
3
+ List of missing features planned for future releases.
4
+
5
+ * [X] Log error and recover if the reply cannot be sent
6
+ * [ ] Threadsafe TimeoutQueue
7
+ * [ ] Multithreaded consumers
8
+ * [ ] Subscribe to topic without a queue (Temporary queues)
9
+
data/examples/event.rb CHANGED
@@ -4,8 +4,8 @@ require "mimi/messaging/sqs_sns"
4
4
 
5
5
  COUNT = 10
6
6
  AWS_REGION = "eu-west-1"
7
- AWS_SQS_ENDPOINT_URL = "http://localstack:4576"
8
- AWS_SNS_ENDPOINT_URL = "http://localstack:4575"
7
+ AWS_SQS_ENDPOINT_URL = "http://localstack:4566"
8
+ AWS_SNS_ENDPOINT_URL = "http://localstack:4566"
9
9
  AWS_ACCESS_KEY_ID = "foo"
10
10
  AWS_SECRET_ACCESS_KEY = "bar"
11
11
 
@@ -30,5 +30,5 @@ COUNT.times do |i|
30
30
  t = Time.now
31
31
  puts "Publishing event: #{i}"
32
32
  adapter.event("hello#tested", i: i) # rand(100))
33
- sleep 1
33
+ sleep 0.1
34
34
  end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mimi/messaging/sqs_sns"
4
+
5
+ COUNT = 10
6
+ THREADS = 10
7
+ QUERY_TIMEOUT = 60
8
+ AWS_REGION = "eu-west-1"
9
+ AWS_SQS_ENDPOINT_URL = "http://localstack:4566"
10
+ AWS_SNS_ENDPOINT_URL = "http://localstack:4566"
11
+ AWS_ACCESS_KEY_ID = "foo"
12
+ AWS_SECRET_ACCESS_KEY = "bar"
13
+ AWS_SQS_SNS_KMS_MASTER_KEY_ID = "blah"
14
+
15
+ logger = Logger.new(STDOUT)
16
+ logger.level = Logger::INFO
17
+ Mimi::Messaging.use(logger: logger, serializer: Mimi::Messaging::JsonSerializer)
18
+ Mimi::Messaging.configure(
19
+ mq_adapter: "sqs_sns",
20
+ mq_aws_access_key_id: AWS_ACCESS_KEY_ID,
21
+ mq_aws_secret_access_key: AWS_SECRET_ACCESS_KEY,
22
+ mq_aws_region: AWS_REGION,
23
+ mq_aws_sqs_endpoint: AWS_SQS_ENDPOINT_URL,
24
+ mq_aws_sns_endpoint: AWS_SNS_ENDPOINT_URL,
25
+ mq_aws_sqs_sns_kms_master_key_id: AWS_SQS_SNS_KMS_MASTER_KEY_ID,
26
+ mq_default_query_timeout: QUERY_TIMEOUT
27
+ )
28
+ adapter = Mimi::Messaging.adapter
29
+
30
+ adapter.start
31
+
32
+ t_start = Time.now
33
+ t_queries = []
34
+ threads = []
35
+ THREADS.times do |ti|
36
+ threads << Thread.new do
37
+ COUNT.times do |i|
38
+ t = Time.now
39
+ result = adapter.query("test/hello", i: i) # rand(100))
40
+ t = Time.now - t
41
+ t_queries << t
42
+ puts "result: #{result.to_h}, t: %.3fs" % t
43
+ sleep 0.1
44
+ end
45
+ end
46
+ end
47
+
48
+ threads.each(&:join)
49
+
50
+ t_elapsed = Time.now - t_start
51
+ puts "t_elapsed: %.3fs" % t_elapsed
52
+ adapter.stop
53
+ puts "t.avg: %.3fs" % (t_queries.sum / t_queries.count)
data/examples/query.rb CHANGED
@@ -4,10 +4,11 @@ require "mimi/messaging/sqs_sns"
4
4
 
5
5
  COUNT = 10
6
6
  AWS_REGION = "eu-west-1"
7
- AWS_SQS_ENDPOINT_URL = "http://localstack:4576"
8
- AWS_SNS_ENDPOINT_URL = "http://localstack:4575"
7
+ AWS_SQS_ENDPOINT_URL = "http://localstack:4566"
8
+ AWS_SNS_ENDPOINT_URL = "http://localstack:4566"
9
9
  AWS_ACCESS_KEY_ID = "foo"
10
10
  AWS_SECRET_ACCESS_KEY = "bar"
11
+ AWS_SQS_SNS_KMS_MASTER_KEY_ID = "blah"
11
12
 
12
13
  logger = Logger.new(STDOUT)
13
14
  logger.level = Logger::INFO
@@ -18,7 +19,8 @@ Mimi::Messaging.configure(
18
19
  mq_aws_secret_access_key: AWS_SECRET_ACCESS_KEY,
19
20
  mq_aws_region: AWS_REGION,
20
21
  mq_aws_sqs_endpoint: AWS_SQS_ENDPOINT_URL,
21
- mq_aws_sns_endpoint: AWS_SNS_ENDPOINT_URL
22
+ mq_aws_sns_endpoint: AWS_SNS_ENDPOINT_URL,
23
+ mq_aws_sqs_sns_kms_master_key_id: AWS_SQS_SNS_KMS_MASTER_KEY_ID
22
24
  )
23
25
  adapter = Mimi::Messaging.adapter
24
26
 
@@ -2,11 +2,12 @@
2
2
 
3
3
  require "mimi/messaging/sqs_sns"
4
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"
5
+ AWS_REGION = "eu-west-1"
6
+ AWS_SQS_ENDPOINT_URL = "http://localstack:4566"
7
+ AWS_SNS_ENDPOINT_URL = "http://localstack:4566"
8
+ AWS_ACCESS_KEY_ID = "foo"
9
9
  AWS_SECRET_ACCESS_KEY = "bar"
10
+ AWS_SQS_SNS_KMS_MASTER_KEY_ID = "blah"
10
11
 
11
12
  class Processor
12
13
  def self.call_command(method_name, message, opts)
@@ -16,13 +17,14 @@ class Processor
16
17
  def self.call_query(method_name, message, opts)
17
18
  # puts "QUERY: #{method_name}, #{message}, headers: #{opts[:headers]}"
18
19
  puts "QUERY: #{method_name}, headers: #{opts[:headers]}"
20
+ sleep 1 # imitate work
19
21
  {}
20
22
  end
21
23
  end # class Processor
22
24
 
23
25
 
24
26
  logger = Logger.new(STDOUT)
25
- logger.level = Logger::INFO
27
+ logger.level = Logger::DEBUG
26
28
  Mimi::Messaging.use(logger: logger, serializer: Mimi::Messaging::JsonSerializer)
27
29
  Mimi::Messaging.configure(
28
30
  mq_adapter: "sqs_sns",
@@ -30,7 +32,12 @@ Mimi::Messaging.configure(
30
32
  mq_aws_secret_access_key: AWS_SECRET_ACCESS_KEY,
31
33
  mq_aws_region: AWS_REGION,
32
34
  mq_aws_sqs_endpoint: AWS_SQS_ENDPOINT_URL,
33
- mq_aws_sns_endpoint: AWS_SNS_ENDPOINT_URL
35
+ mq_aws_sns_endpoint: AWS_SNS_ENDPOINT_URL,
36
+ mq_aws_sqs_sns_kms_master_key_id: AWS_SQS_SNS_KMS_MASTER_KEY_ID,
37
+ mq_worker_pool_min_threads: 1,
38
+ mq_worker_pool_max_threads: 2,
39
+ mq_worker_pool_max_backlog: 4,
40
+ mq_log_at_level: :info
34
41
  )
35
42
  adapter = Mimi::Messaging.adapter
36
43
  queue_name = "test"
@@ -47,4 +54,3 @@ ensure
47
54
  puts "Stopping adapter"
48
55
  adapter.stop
49
56
  end
50
-
@@ -3,8 +3,8 @@
3
3
  require "mimi/messaging/sqs_sns"
4
4
 
5
5
  AWS_REGION = "eu-west-1"
6
- AWS_SQS_ENDPOINT_URL = "http://localstack:4576"
7
- AWS_SNS_ENDPOINT_URL = "http://localstack:4575"
6
+ AWS_SQS_ENDPOINT_URL = "http://localstack:4566"
7
+ AWS_SNS_ENDPOINT_URL = "http://localstack:4566"
8
8
  AWS_ACCESS_KEY_ID = "foo"
9
9
  AWS_SECRET_ACCESS_KEY = "bar"
10
10
 
@@ -20,6 +20,7 @@ class Processor
20
20
 
21
21
  def self.call_event(event_type, message, opts)
22
22
  puts "EVENT: #{event_type}, #{message}, headers: #{message.headers}"
23
+ sleep 1 # imitate work
23
24
  end
24
25
  end # class Processor
25
26
 
@@ -34,6 +35,9 @@ Mimi::Messaging.configure(
34
35
  mq_aws_region: AWS_REGION,
35
36
  mq_aws_sqs_endpoint: AWS_SQS_ENDPOINT_URL,
36
37
  mq_aws_sns_endpoint: AWS_SNS_ENDPOINT_URL,
38
+ mq_worker_pool_min_threads: 1,
39
+ mq_worker_pool_max_threads: 2,
40
+ mq_worker_pool_max_backlog: 4,
37
41
  mq_log_at_level: :info
38
42
  )
39
43
  adapter = Mimi::Messaging.adapter
@@ -52,4 +56,3 @@ ensure
52
56
  puts "Stopping adapter"
53
57
  adapter.stop
54
58
  end
55
-
@@ -12,4 +12,5 @@ end # module Mimi
12
12
 
13
13
  require_relative "sqs_sns/adapter"
14
14
  require_relative "sqs_sns/consumer"
15
+ require_relative "sqs_sns/temporary_queue_consumer"
15
16
  require_relative "sqs_sns/reply_consumer"
@@ -3,8 +3,9 @@
3
3
  require "mimi/messaging"
4
4
  require "aws-sdk-sqs"
5
5
  require "aws-sdk-sns"
6
- require "timeout"
7
6
  require "securerandom"
7
+ require "concurrent"
8
+ require_relative "timeout_queue"
8
9
 
9
10
  module Mimi
10
11
  module Messaging
@@ -40,7 +41,7 @@ module Mimi
40
41
  "." => "-"
41
42
  }.freeze
42
43
 
43
- attr_reader :options, :sqs_client, :sns_client
44
+ attr_reader :options, :sqs_client, :sns_client, :worker_pool
44
45
 
45
46
  register_adapter_name "sqs_sns"
46
47
 
@@ -49,6 +50,11 @@ module Mimi
49
50
  mq_default_query_timeout: 15, # seconds,
50
51
  mq_reply_queue_prefix: "reply-",
51
52
 
53
+ # worker pool parameters
54
+ mq_worker_pool_min_threads: 1,
55
+ mq_worker_pool_max_threads: 16,
56
+ mq_worker_pool_max_backlog: 16,
57
+
52
58
  # if nil, AWS SDK will guess values from environment
53
59
  mq_aws_region: nil,
54
60
  mq_aws_access_key_id: nil,
@@ -56,7 +62,8 @@ module Mimi
56
62
  mq_aws_sqs_endpoint: nil,
57
63
  mq_aws_sns_endpoint: nil,
58
64
 
59
- mq_aws_sqs_read_timeout: 10, # seconds
65
+ mq_aws_sqs_sns_kms_master_key_id: nil,
66
+ mq_aws_sqs_read_timeout: 20, # seconds
60
67
  }.freeze
61
68
 
62
69
  # Initializes SQS/SNS adapter
@@ -73,16 +80,19 @@ module Mimi
73
80
  #
74
81
  def initialize(options)
75
82
  @options = DEFAULT_OPTIONS.merge(options).dup
83
+ @reply_consumer_mutex = Mutex.new
76
84
  end
77
85
 
78
86
  def start
79
87
  @sqs_client = Aws::SQS::Client.new(sqs_client_config)
80
88
  @sns_client = Aws::SNS::Client.new(sns_client_config)
89
+ start_worker_pool!
81
90
  check_availability!
82
91
  end
83
92
 
84
93
  def stop
85
94
  stop_all_processors
95
+ stop_worker_pool!
86
96
  @sqs_client = nil
87
97
  @sns_client = nil
88
98
  end
@@ -93,6 +103,7 @@ module Mimi
93
103
  # for processors.
94
104
  #
95
105
  def stop_all_processors
106
+ @consumers&.each(&:signal_stop)
96
107
  @consumers&.each(&:stop)
97
108
  @consumers = nil
98
109
  @reply_consumer&.stop
@@ -124,7 +135,7 @@ module Mimi
124
135
  # @param opts [Hash] additional options, e.g. :timeout
125
136
  #
126
137
  # @return [Hash]
127
- # @raise [SomeError,TimeoutError]
138
+ # @raise [SomeError,Timeout::Error]
128
139
  #
129
140
  def query(target, message, opts = {})
130
141
  queue_name, method_name = target.split("/")
@@ -140,10 +151,7 @@ module Mimi
140
151
  )
141
152
  deliver_message_queue(queue_url, message)
142
153
  timeout = opts[:timeout] || options[:mq_default_query_timeout]
143
- response = nil
144
- Timeout::timeout(timeout) do
145
- response = reply_queue.pop
146
- end
154
+ response = reply_queue.pop(true, timeout)
147
155
  deserialize(response.body)
148
156
  end
149
157
 
@@ -178,22 +186,14 @@ module Mimi
178
186
  opts = opts.dup
179
187
  queue_url = find_or_create_queue(queue_name)
180
188
  @consumers << Consumer.new(self, queue_url) do |m|
181
- message = Mimi::Messaging::Message.new(
182
- deserialize(m.body),
183
- deserialize_headers(m)
184
- )
185
- method_name = message.headers[:__method]
186
- reply_to = message.headers[:__reply_queue_url]
187
- if reply_to
188
- response = processor.call_query(method_name, message, {})
189
- response_message = Mimi::Messaging::Message.new(
190
- response,
191
- __request_id: message.headers[:__request_id]
192
- )
193
- deliver_message_queue(reply_to, response_message)
194
- else
195
- processor.call_command(method_name, message, {})
189
+ worker_pool.post do
190
+ process_request_message(processor, m)
196
191
  end
192
+ rescue Concurrent::RejectedExecutionError
193
+ # the backlog is overflown, put the message back
194
+ Mimi::Messaging.log "Worker pool backlog is full, nack-ing the message " \
195
+ "(workers:#{worker_pool.length}, backlog:#{worker_pool.queue_length})"
196
+ raise Mimi::Messaging::NACK # exception raised in Consumer thread
197
197
  end
198
198
  end
199
199
 
@@ -210,15 +210,59 @@ module Mimi
210
210
  queue_url = find_or_create_queue(queue_name)
211
211
  subscribe_topic_queue(topic_arn, queue_url)
212
212
  @consumers << Consumer.new(self, queue_url) do |m|
213
- message = Mimi::Messaging::Message.new(
214
- deserialize(m.body),
215
- deserialize_headers(m)
216
- )
217
- event_type = message.headers[:__event_type]
218
- processor.call_event(event_type, message, {})
213
+ worker_pool.post do
214
+ process_event_message(processor, m)
215
+ end
216
+ rescue Concurrent::RejectedExecutionError
217
+ # the backlog is overflown, put the message back
218
+ Mimi::Messaging.log "Worker pool backlog is full, nack-ing the message " \
219
+ "(workers:#{worker_pool.length}, backlog:#{worker_pool.queue_length})"
220
+ raise Mimi::Messaging::NACK # exception raised in Consumer thread
219
221
  end
220
222
  end
221
223
 
224
+ # Creates a new queue
225
+ #
226
+ # @param queue_name [String] name of the topic to be created
227
+ # @return [String] a new queue URL
228
+ #
229
+ def create_queue(queue_name)
230
+ fqn = sqs_sns_converted_full_name(queue_name)
231
+ Mimi::Messaging.log "Creating a queue: #{fqn}"
232
+ attrs = {}
233
+ if options[:mq_aws_sqs_sns_kms_master_key_id]
234
+ attrs["KmsMasterKeyId"] = options[:mq_aws_sqs_sns_kms_master_key_id]
235
+ end
236
+ result = sqs_client.create_queue(queue_name: fqn, attributes: attrs)
237
+ result.queue_url
238
+ rescue StandardError => e
239
+ raise Mimi::Messaging::ConnectionError, "Failed to create queue '#{queue_name}': #{e}"
240
+ end
241
+
242
+ # Finds a queue URL for a queue with given name.
243
+ #
244
+ # If an existing queue with this name is not found,
245
+ # the method will try to create a new one.
246
+ #
247
+ # @param queue_name [String]
248
+ # @return [String] a queue URL
249
+ #
250
+ def find_or_create_queue(queue_name)
251
+ queue_registry(queue_name) || create_queue(queue_name)
252
+ end
253
+
254
+ # Deletes a queue identified by the queue URL
255
+ #
256
+ # @param queue_url [String]
257
+ #
258
+ def delete_queue(queue_url)
259
+ Mimi::Messaging.log "Deleting a queue: #{queue_url}"
260
+ sqs_client.delete_queue(queue_url: queue_url)
261
+ rescue StandardError => e
262
+ raise Mimi::Messaging::ConnectionError,
263
+ "Failed to delete queue with url '#{queue_url}': #{e}"
264
+ end
265
+
222
266
  private
223
267
 
224
268
  # Returns configuration parameters for AWS SQS client
@@ -266,20 +310,6 @@ module Mimi
266
310
  end
267
311
  end
268
312
 
269
- # Creates a new queue
270
- #
271
- # @param queue_name [String] name of the topic to be created
272
- # @return [String] a new queue URL
273
- #
274
- def create_queue(queue_name)
275
- fqn = sqs_sns_converted_full_name(queue_name)
276
- Mimi::Messaging.log "Creating a queue: #{fqn}"
277
- result = sqs_client.create_queue(queue_name: fqn)
278
- result.queue_url
279
- rescue StandardError => e
280
- raise Mimi::Messaging::ConnectionError, "Failed to create queue '#{queue_name}': #{e}"
281
- end
282
-
283
313
  # Delivers a message to a queue with given URL.
284
314
  #
285
315
  # @param queue_url [String]
@@ -290,7 +320,8 @@ module Mimi
290
320
  unless message.is_a?(Mimi::Messaging::Message)
291
321
  raise ArgumentError, "Message is expected as argument"
292
322
  end
293
- Mimi::Messaging.log "Delivering message to: #{queue_url}"
323
+
324
+ Mimi::Messaging.log "Delivering message to: #{queue_url}, headers: #{message.headers}"
294
325
  sqs_client.send_message(
295
326
  queue_url: queue_url,
296
327
  message_body: serialize(message),
@@ -302,6 +333,47 @@ module Mimi
302
333
  raise Mimi::Messaging::ConnectionError, "Failed to deliver message to '#{queue_url}': #{e}"
303
334
  end
304
335
 
336
+ # Processes an incoming COMMAND or QUERY message
337
+ #
338
+ # @param processor [#call_query(),#call_command()] request processor object
339
+ # @param sqs_message
340
+ #
341
+ def process_request_message(processor, sqs_message)
342
+ message = Mimi::Messaging::Message.new(
343
+ deserialize(sqs_message.body),
344
+ deserialize_headers(sqs_message)
345
+ )
346
+ method_name = message.headers[:__method]
347
+ reply_to = message.headers[:__reply_queue_url]
348
+ if reply_to
349
+ response = processor.call_query(method_name, message, {})
350
+ response_message = Mimi::Messaging::Message.new(
351
+ response,
352
+ __request_id: message.headers[:__request_id]
353
+ )
354
+ deliver_query_response(reply_to, response_message)
355
+ else
356
+ processor.call_command(method_name, message, {})
357
+ end
358
+ end
359
+
360
+ # Delivers a message as a response to a QUERY
361
+ #
362
+ # Responses are allowed to fail. There can be a number of reasons
363
+ # why responses fail: reply queue does not exist (anymore?),
364
+ # response message is too big. In any case the error is reported,
365
+ # but the QUERY message is acknowledged as a successfully processed.
366
+ #
367
+ # @param queue_url [String]
368
+ # @param message [Mimi::Messaging::Message]
369
+ #
370
+ def deliver_query_response(queue_url, message)
371
+ deliver_message_queue(queue_url, message)
372
+ rescue Mimi::Messaging::ConnectionError => e
373
+ Mimi::Messaging.logger&.warn("Failed to deliver QRY response: #{e}")
374
+ # NOTE: error is recovered
375
+ end
376
+
305
377
  # Returns URL of a queue with a given name.
306
378
  #
307
379
  # If the queue with given name does not exist, returns nil
@@ -349,27 +421,16 @@ module Mimi
349
421
  )
350
422
  end
351
423
 
352
- # Finds a queue URL for a queue with given name.
353
- #
354
- # If an existing queue with this name is not found,
355
- # the method will try to create a new one.
356
- #
357
- # @param queue_name [String]
358
- # @return [String] a queue URL
359
- #
360
- def find_or_create_queue(queue_name)
361
- queue_registry(queue_name) || create_queue(queue_name)
362
- end
363
-
364
424
  # Returns the configured reply listener for this process
365
425
  #
366
426
  # @return [ReplyConsumer]
367
427
  #
368
428
  def reply_consumer
369
- @reply_consumer ||= begin
370
- reply_queue_name = options[:mq_reply_queue_prefix] + SecureRandom.hex(8)
371
- reply_queue_url = create_queue(reply_queue_name)
372
- Mimi::Messaging::SQS_SNS::ReplyConsumer.new(self, reply_queue_url)
429
+ @reply_consumer_mutex.synchronize do
430
+ @reply_consumer ||= begin
431
+ reply_queue_name = options[:mq_reply_queue_prefix] + SecureRandom.hex(8)
432
+ Mimi::Messaging::SQS_SNS::ReplyConsumer.new(self, reply_queue_name)
433
+ end
373
434
  end
374
435
  end
375
436
 
@@ -450,7 +511,11 @@ module Mimi
450
511
  def create_topic(topic_name)
451
512
  fqn = sqs_sns_converted_full_name(topic_name)
452
513
  Mimi::Messaging.log "Creating a topic: #{fqn}"
453
- result = sns_client.create_topic(name: fqn)
514
+ attrs = {}
515
+ if options[:mq_aws_sqs_sns_kms_master_key_id]
516
+ attrs["KmsMasterKeyId"] = options[:mq_aws_sqs_sns_kms_master_key_id]
517
+ end
518
+ result = sns_client.create_topic(name: fqn, attributes: attrs)
454
519
  result.topic_arn
455
520
  rescue StandardError => e
456
521
  raise Mimi::Messaging::ConnectionError, "Failed to create topic '#{topic_name}': #{e}"
@@ -467,7 +532,7 @@ module Mimi
467
532
  )
468
533
  queue_arn = result.attributes["QueueArn"]
469
534
  Mimi::Messaging.log "Subscribing queue to a topic: '#{topic_arn}'->'#{queue_url}'"
470
- result = sns_client.subscribe(
535
+ _result = sns_client.subscribe(
471
536
  topic_arn: topic_arn,
472
537
  protocol: "sqs",
473
538
  endpoint: queue_arn,
@@ -489,6 +554,7 @@ module Mimi
489
554
  unless message.is_a?(Mimi::Messaging::Message)
490
555
  raise ArgumentError, "Message is expected as argument"
491
556
  end
557
+
492
558
  Mimi::Messaging.log "Delivering message to: #{topic_arn}"
493
559
  sns_client.publish(
494
560
  topic_arn: topic_arn,
@@ -500,6 +566,46 @@ module Mimi
500
566
  rescue StandardError => e
501
567
  raise Mimi::Messaging::ConnectionError, "Failed to deliver message to '#{topic_arn}': #{e}"
502
568
  end
569
+
570
+ # Processes an incoming EVENT message
571
+ #
572
+ # @param processor [#call_event()] event processor object
573
+ # @param sqs_message []
574
+ #
575
+ def process_event_message(processor, sqs_message)
576
+ message = Mimi::Messaging::Message.new(
577
+ deserialize(sqs_message.body),
578
+ deserialize_headers(sqs_message)
579
+ )
580
+ event_type = message.headers[:__event_type]
581
+ processor.call_event(event_type, message, {})
582
+ end
583
+
584
+ # Starts the worker pool using current configuration
585
+ #
586
+ # @return [Concurrent::ThreadPoolExecutor]
587
+ #
588
+ def start_worker_pool!
589
+ Mimi::Messaging.log "Starting worker pool, " \
590
+ "min_threads:#{options[:mq_worker_pool_min_threads]}, " \
591
+ "max_threads:#{options[:mq_worker_pool_max_threads]}, " \
592
+ "max_backlog:#{options[:mq_worker_pool_max_backlog]}"
593
+
594
+ @worker_pool = Concurrent::ThreadPoolExecutor.new(
595
+ min_threads: options[:mq_worker_pool_min_threads],
596
+ max_threads: options[:mq_worker_pool_max_threads],
597
+ max_queue: options[:mq_worker_pool_max_backlog],
598
+ fallback_policy: :abort
599
+ )
600
+ end
601
+
602
+ # Gracefully stops the worker pool, allowing all threads to finish their jobs
603
+ #
604
+ def stop_worker_pool!
605
+ Mimi::Messaging.log "Stopping worker pool"
606
+ @worker_pool.shutdown
607
+ @worker_pool.wait_for_termination
608
+ end
503
609
  end # class Adapter
504
610
  end # module SQS_SNS
505
611
  end # module Messaging
@@ -4,25 +4,31 @@ module Mimi
4
4
  module Messaging
5
5
  module SQS_SNS
6
6
  #
7
- # Message processor for SQS queues
7
+ # Message consumer for SQS queues
8
8
  #
9
9
  class Consumer
10
+ # (seconds) determines how soon the NACK-ed message becomes visible to other consumers
11
+ NACK_VISIBILITY_TIMEOUT = 1
12
+
10
13
  def initialize(adapter, queue_url, &block)
11
- @block = block
12
14
  @stop_requested = false
13
15
  Mimi::Messaging.log "Starting consumer for: #{queue_url}"
14
16
  @consumer_thread = Thread.new do
15
- while not @stop_requested do
16
- message = read_message(adapter, queue_url)
17
- next unless message
18
- Mimi::Messaging.log "Read message from: #{queue_url}"
19
- block.call(message)
20
- ack_message(adapter, queue_url, message)
17
+ while not @stop_requested
18
+ read_and_process_message(adapter, queue_url, block)
21
19
  end
22
20
  Mimi::Messaging.log "Stopping consumer for: #{queue_url}"
23
21
  end
24
22
  end
25
23
 
24
+ # Requests the Consumer to stop, without actually waiting for it
25
+ #
26
+ def signal_stop
27
+ @stop_requested = true
28
+ end
29
+
30
+ # Requests the Consumer to stop AND waits until it does
31
+ #
26
32
  def stop
27
33
  @stop_requested = true
28
34
  @consumer_thread.join
@@ -35,13 +41,18 @@ module Mimi
35
41
  #
36
42
  # @param adapter [Mimi::Messaging::SQS_SNS::Adapter]
37
43
  # @param queue_url [String]
44
+ # @param block [Proc] a block to be invoked when a message is received
38
45
  #
39
- def read_and_process_message(adapter, queue_url)
46
+ def read_and_process_message(adapter, queue_url, block)
40
47
  message = read_message(adapter, queue_url)
41
48
  return unless message
49
+
42
50
  Mimi::Messaging.log "Read message from: #{queue_url}"
43
51
  block.call(message)
44
52
  ack_message(adapter, queue_url, message)
53
+ rescue Mimi::Messaging::NACK
54
+ Mimi::Messaging.log "NACK-ing message from: #{queue_url}"
55
+ nack_message(adapter, queue_url, message)
45
56
  rescue StandardError => e
46
57
  Mimi::Messaging.logger&.error(
47
58
  "#{self.class}: failed to read and process message from: #{queue_url}," \
@@ -58,14 +69,29 @@ module Mimi
58
69
  )
59
70
  return nil if result.messages.count == 0
60
71
  return result.messages.first if result.messages.count == 1
72
+
61
73
  raise Mimi::Messaging::ConnectionError, "Unexpected number of messages read"
62
74
  end
63
75
 
76
+ # ACK-ing the message indicates successfull processing of it
77
+ # and removes the message from the queue
78
+ #
64
79
  def ack_message(adapter, queue_url, msg)
65
80
  adapter.sqs_client.delete_message(
66
81
  queue_url: queue_url, receipt_handle: msg.receipt_handle
67
82
  )
68
83
  end
84
+
85
+ # NACK-ing the message indicates a failure to process the message.
86
+ # The message becomes immediately available to other consumers.
87
+ #
88
+ def nack_message(adapter, queue_url, msg)
89
+ adapter.sqs_client.change_message_visibility(
90
+ queue_url: queue_url,
91
+ receipt_handle: msg.receipt_handle,
92
+ visibility_timeout: NACK_VISIBILITY_TIMEOUT
93
+ )
94
+ end
69
95
  end # class Consumer
70
96
  end # module SQS_SNS
71
97
  end # module Messaging
@@ -8,25 +8,23 @@ module Mimi
8
8
  # and passes them to registered Queues (see Ruby ::Queue class).
9
9
  #
10
10
  class ReplyConsumer
11
- attr_reader :reply_queue_url
11
+ attr_reader :reply_queue_name, :reply_queue_url
12
12
 
13
- def initialize(adapter, reply_queue_url)
13
+ def initialize(adapter, reply_queue_name)
14
14
  @mutex = Mutex.new
15
15
  @queues = {}
16
16
  @adapter = adapter
17
- @reply_queue_url = reply_queue_url
18
- @consumer = Consumer.new(adapter, reply_queue_url) do |message|
17
+ @reply_queue_name = reply_queue_name
18
+ @consumer = TemporaryQueueConsumer.new(adapter, reply_queue_name) do |message|
19
19
  dispatch_message(message)
20
20
  end
21
+ @reply_queue_url = @consumer.queue_url
21
22
  end
22
23
 
23
24
  def stop
24
- begin
25
- @consumer.stop
26
- rescue StandardError => e
27
- raise Mimi::Messaging::Error, "Failed to stop consumer: #{e}"
28
- end
29
- # TODO: adapter.sqs_client.delete_queue(reply_queue_url)
25
+ @consumer.stop
26
+ rescue StandardError => e
27
+ raise Mimi::Messaging::Error, "Failed to stop reply consumer: #{e}"
30
28
  end
31
29
 
32
30
  # Register a new request_id to listen for.
@@ -38,7 +36,7 @@ module Mimi
38
36
  # @return [Queue] a new Queue object registered for this request_id
39
37
  #
40
38
  def register_request_id(request_id)
41
- queue = Queue.new
39
+ queue = TimeoutQueue.new
42
40
  @mutex.synchronize do
43
41
  queue = @queues[request_id] ||= queue
44
42
  end
@@ -65,6 +63,7 @@ module Mimi
65
63
  @mutex.synchronize do
66
64
  headers = deserialize_headers(message)
67
65
  request_id = headers[:__request_id]
66
+ Mimi::Messaging.log "dispatching response, headers:#{headers}"
68
67
  queue = @queues.delete(request_id)
69
68
  end
70
69
  queue&.push(message)
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mimi
4
+ module Messaging
5
+ module SQS_SNS
6
+ #
7
+ # Temporary queue consumer creates a temporary queue
8
+ # and attaches to it. The queue will be deleted
9
+ # on consumer shutdown.
10
+ #
11
+ class TemporaryQueueConsumer
12
+ attr_reader :queue_url
13
+
14
+ def initialize(adapter, queue_name, &block)
15
+ @adapter = adapter
16
+ @queue_url = adapter.find_or_create_queue(queue_name)
17
+ @consumer = Consumer.new(adapter, @queue_url, &block)
18
+ end
19
+
20
+ def stop
21
+ @consumer.stop
22
+ @adapter.delete_queue(queue_url)
23
+ rescue StandardError => e
24
+ raise Mimi::Messaging::Error, "Failed to stop temporary queue consumer: #{e}"
25
+ end
26
+ end # class TemporaryQueueConsumer
27
+ end # module SQS_SNS
28
+ end # module Messaging
29
+ end # module Mimi
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+
5
+ module Mimi
6
+ module Messaging
7
+ module SQS_SNS
8
+ # TimeoutQueue solves the problem the native Ruby Queue class has with waiting for elements.
9
+ #
10
+ # See the excellent blog post discussing the issue:
11
+ # https://medium.com/workday-engineering/ruby-concurrency-building-a-timeout-queue-5d7c588ca80d
12
+ #
13
+ # TLDR -- using Ruby standard Timeout.timeout() around Queue#pop() is unsafe
14
+ #
15
+ #
16
+ class TimeoutQueue
17
+ def initialize
18
+ @elems = []
19
+ @mutex = Mutex.new
20
+ @cond_var = ConditionVariable.new
21
+ end
22
+
23
+ # Pushes an element into the queue
24
+ #
25
+ # @param elem [Object]
26
+ #
27
+ def <<(elem)
28
+ @mutex.synchronize do
29
+ @elems << elem
30
+ @cond_var.signal
31
+ end
32
+ end
33
+ alias push <<
34
+
35
+ # Pops an element from the queue in either non-blocking
36
+ # or a blocking (with an optional timeout) way.
37
+ #
38
+ # @param blocking [true,false] wait for a new element (true) or return immediately
39
+ # @param timeout [nil,Integer] if in blocking mode,
40
+ # wait at most given number of seconds or forever (nil)
41
+ # @raise [Timeout::Error] if a timeout in blocking mode was reached
42
+ #
43
+ def pop(blocking = true, timeout = nil)
44
+ @mutex.synchronize do
45
+ if blocking
46
+ if timeout.nil?
47
+ while @elems.empty?
48
+ @cond_var.wait(@mutex)
49
+ end
50
+ else
51
+ timeout_time = Time.now.to_f + timeout
52
+ while @elems.empty? && (remaining_time = timeout_time - Time.now.to_f) > 0
53
+ @cond_var.wait(@mutex, remaining_time)
54
+ end
55
+ end
56
+ end
57
+ raise Timeout::Error, "queue timeout expired" if @elems.empty?
58
+
59
+ @elems.shift
60
+ end
61
+ end
62
+ end # class TimeoutQueue
63
+ end # module SQS_SNS
64
+ end # module Messaging
65
+ end # module Mimi
@@ -3,7 +3,7 @@
3
3
  module Mimi
4
4
  module Messaging
5
5
  module SQS_SNS
6
- VERSION = "0.4.2"
6
+ VERSION = "0.8.0"
7
7
  end
8
8
  end
9
9
  end
@@ -36,9 +36,10 @@ Gem::Specification.new do |spec|
36
36
  spec.add_dependency "mimi-messaging", "~> 1.2"
37
37
  spec.add_dependency "aws-sdk-sqs", "~> 1.22"
38
38
  spec.add_dependency "aws-sdk-sns", "~> 1.19"
39
+ spec.add_dependency "concurrent-ruby"
39
40
 
40
41
  spec.add_development_dependency "bundler", "~> 2.0"
41
42
  spec.add_development_dependency "pry", "~> 0.12"
42
- spec.add_development_dependency "rake", "~> 10.0"
43
+ spec.add_development_dependency "rake", "~> 12.0", ">= 12.3.3"
43
44
  spec.add_development_dependency "rspec", "~> 3.0"
44
45
  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.4.2
4
+ version: 0.8.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-21 00:00:00.000000000 Z
11
+ date: 2021-02-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mimi-messaging
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '1.19'
55
+ - !ruby/object:Gem::Dependency
56
+ name: concurrent-ruby
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: bundler
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -86,14 +100,20 @@ dependencies:
86
100
  requirements:
87
101
  - - "~>"
88
102
  - !ruby/object:Gem::Version
89
- version: '10.0'
103
+ version: '12.0'
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: 12.3.3
90
107
  type: :development
91
108
  prerelease: false
92
109
  version_requirements: !ruby/object:Gem::Requirement
93
110
  requirements:
94
111
  - - "~>"
95
112
  - !ruby/object:Gem::Version
96
- version: '10.0'
113
+ version: '12.0'
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: 12.3.3
97
117
  - !ruby/object:Gem::Dependency
98
118
  name: rspec
99
119
  requirement: !ruby/object:Gem::Requirement
@@ -118,14 +138,17 @@ files:
118
138
  - ".gitignore"
119
139
  - ".rspec"
120
140
  - ".travis.yml"
141
+ - CHANGELOG.md
121
142
  - CODE_OF_CONDUCT.md
122
143
  - Gemfile
123
144
  - LICENSE.txt
124
145
  - README.md
125
146
  - Rakefile
147
+ - TODO.md
126
148
  - bin/console
127
149
  - bin/setup
128
150
  - examples/event.rb
151
+ - examples/mass-query.rb
129
152
  - examples/query.rb
130
153
  - examples/responder.rb
131
154
  - examples/subscriber.rb
@@ -133,6 +156,8 @@ files:
133
156
  - lib/mimi/messaging/sqs_sns/adapter.rb
134
157
  - lib/mimi/messaging/sqs_sns/consumer.rb
135
158
  - lib/mimi/messaging/sqs_sns/reply_consumer.rb
159
+ - lib/mimi/messaging/sqs_sns/temporary_queue_consumer.rb
160
+ - lib/mimi/messaging/sqs_sns/timeout_queue.rb
136
161
  - lib/mimi/messaging/sqs_sns/version.rb
137
162
  - mimi-messaging-sqs_sns.gemspec
138
163
  homepage: https://github.com/kukushkin/mimi-messaging-sqs_sns
@@ -157,8 +182,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
157
182
  - !ruby/object:Gem::Version
158
183
  version: '0'
159
184
  requirements: []
160
- rubyforge_project:
161
- rubygems_version: 2.6.14.4
185
+ rubygems_version: 3.1.2
162
186
  signing_key:
163
187
  specification_version: 4
164
188
  summary: AWS SQS/SNS adapter for mimi-messaging