mimi-messaging-sqs_sns 0.5.0 → 0.8.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 297b98675dafdc02dc9cbc9256339b3ecb696875
4
- data.tar.gz: db1e2efc5ac9f8b98b1de5fb5dff0c76ee21930c
2
+ SHA256:
3
+ metadata.gz: e106fbe54e012ffd59a6126292242b23c28b3086ff5dfd4866acb64bad559ccf
4
+ data.tar.gz: 2fce6feb750d6cf27925e79646b8e76359188343f663ee8d20f30f09eedc2ba0
5
5
  SHA512:
6
- metadata.gz: c9d87b2c28ed31a1836070e09adc8732d3becc46364b3d04836e031bb9ae424e02aad22519a1632a1c205900a10a8ba7e2650c1e80b9947b40fa8dea5cd95331
7
- data.tar.gz: 0d64bb39d6858fddb0645561180695fc9c6bb4ae44cc2f79027e36f9bab4bc4ff5efa69814da79a3046a0ead1412897ec00c0a9902f9d0b8a85c08f124029ee8
6
+ metadata.gz: a830a70afd7b0a5f268d600ca9673a8f3fc36566201510263008f517d32354a020213de0b51497c446e99a768089f2fd9a1392dccdf5bbbc5fc352d3557a2a4e
7
+ data.tar.gz: 41dc62d85b21f8736011d974b2749e5f6ce093d3f3cc3746f037f8a986ef01621bc2a61ae60f8f96a5405dcc6672bd1468fec0e3ea695e0b1f1e4a123d71c962
data/CHANGELOG.md ADDED
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ ## UNRELEASED
4
+
5
+ ## v0.8.2
6
+
7
+ * [#7](https://github.com/kukushkin/mimi-messaging-sqs_sns/pull/7)
8
+ * Fixed an issue in the error processing, which could potentially crash the consumer threads
9
+
10
+ ## v0.8.1
11
+
12
+ * [#5](https://github.com/kukushkin/mimi-messaging-sqs_sns/pull/5)
13
+ * Refactored worker pool based message processing and error handling
14
+
15
+ ## v0.8.0
16
+
17
+ * [#3](https://github.com/kukushkin/mimi-messaging-sqs_sns/pull/3)
18
+ * Added a worker pool:
19
+ * now processing of messages from a single queue can be done in multiple parallel threads (workers)
20
+ * the worker threads are shared across all message consumers which read messages from different queues
21
+ * the size of the worker pool is limited, to have at most `mq_worker_pool_max_threads` processing messages in parallel
22
+ * `mq_worker_pool_min_threads` determines the target minimal number of threads in the pool, waiting for new messages
23
+ * `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)
24
+ * Improved thread-safety of the adapter: reply consumer, TimeoutQueue#pop
25
+
26
+ ## v0.7.0
27
+
28
+ * [#1](https://github.com/kukushkin/mimi-messaging-sqs_sns/pull/1)
29
+ * Added KMS support for creating queues/topics with sever-side encryption enabled
30
+ * Optimized stopping of the adapter: stopping all consumers in parallel
31
+
32
+
33
+ ## v0.6.x
34
+
35
+ * 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
+ * [X] Threadsafe TimeoutQueue
7
+ * [X] 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
-
@@ -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,7 @@ 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, {})
196
- end
189
+ process_request_message(processor, m)
197
190
  end
198
191
  end
199
192
 
@@ -210,12 +203,7 @@ module Mimi
210
203
  queue_url = find_or_create_queue(queue_name)
211
204
  subscribe_topic_queue(topic_arn, queue_url)
212
205
  @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, {})
206
+ process_event_message(processor, m)
219
207
  end
220
208
  end
221
209
 
@@ -227,13 +215,16 @@ module Mimi
227
215
  def create_queue(queue_name)
228
216
  fqn = sqs_sns_converted_full_name(queue_name)
229
217
  Mimi::Messaging.log "Creating a queue: #{fqn}"
230
- result = sqs_client.create_queue(queue_name: fqn)
218
+ attrs = {}
219
+ if options[:mq_aws_sqs_sns_kms_master_key_id]
220
+ attrs["KmsMasterKeyId"] = options[:mq_aws_sqs_sns_kms_master_key_id]
221
+ end
222
+ result = sqs_client.create_queue(queue_name: fqn, attributes: attrs)
231
223
  result.queue_url
232
224
  rescue StandardError => e
233
225
  raise Mimi::Messaging::ConnectionError, "Failed to create queue '#{queue_name}': #{e}"
234
226
  end
235
227
 
236
-
237
228
  # Finds a queue URL for a queue with given name.
238
229
  #
239
230
  # If an existing queue with this name is not found,
@@ -315,6 +306,7 @@ module Mimi
315
306
  unless message.is_a?(Mimi::Messaging::Message)
316
307
  raise ArgumentError, "Message is expected as argument"
317
308
  end
309
+
318
310
  Mimi::Messaging.log "Delivering message to: #{queue_url}, headers: #{message.headers}"
319
311
  sqs_client.send_message(
320
312
  queue_url: queue_url,
@@ -327,6 +319,47 @@ module Mimi
327
319
  raise Mimi::Messaging::ConnectionError, "Failed to deliver message to '#{queue_url}': #{e}"
328
320
  end
329
321
 
322
+ # Processes an incoming COMMAND or QUERY message
323
+ #
324
+ # @param processor [#call_query(),#call_command()] request processor object
325
+ # @param sqs_message
326
+ #
327
+ def process_request_message(processor, sqs_message)
328
+ message = Mimi::Messaging::Message.new(
329
+ deserialize(sqs_message.body),
330
+ deserialize_headers(sqs_message)
331
+ )
332
+ method_name = message.headers[:__method]
333
+ reply_to = message.headers[:__reply_queue_url]
334
+ if reply_to
335
+ response = processor.call_query(method_name, message, {})
336
+ response_message = Mimi::Messaging::Message.new(
337
+ response,
338
+ __request_id: message.headers[:__request_id]
339
+ )
340
+ deliver_query_response(reply_to, response_message)
341
+ else
342
+ processor.call_command(method_name, message, {})
343
+ end
344
+ end
345
+
346
+ # Delivers a message as a response to a QUERY
347
+ #
348
+ # Responses are allowed to fail. There can be a number of reasons
349
+ # why responses fail: reply queue does not exist (anymore?),
350
+ # response message is too big. In any case the error is reported,
351
+ # but the QUERY message is acknowledged as a successfully processed.
352
+ #
353
+ # @param queue_url [String]
354
+ # @param message [Mimi::Messaging::Message]
355
+ #
356
+ def deliver_query_response(queue_url, message)
357
+ deliver_message_queue(queue_url, message)
358
+ rescue Mimi::Messaging::ConnectionError => e
359
+ Mimi::Messaging.logger&.warn("Failed to deliver QRY response: #{e}")
360
+ # NOTE: error is recovered
361
+ end
362
+
330
363
  # Returns URL of a queue with a given name.
331
364
  #
332
365
  # If the queue with given name does not exist, returns nil
@@ -379,9 +412,11 @@ module Mimi
379
412
  # @return [ReplyConsumer]
380
413
  #
381
414
  def reply_consumer
382
- @reply_consumer ||= begin
383
- reply_queue_name = options[:mq_reply_queue_prefix] + SecureRandom.hex(8)
384
- Mimi::Messaging::SQS_SNS::ReplyConsumer.new(self, reply_queue_name)
415
+ @reply_consumer_mutex.synchronize do
416
+ @reply_consumer ||= begin
417
+ reply_queue_name = options[:mq_reply_queue_prefix] + SecureRandom.hex(8)
418
+ Mimi::Messaging::SQS_SNS::ReplyConsumer.new(self, reply_queue_name)
419
+ end
385
420
  end
386
421
  end
387
422
 
@@ -462,7 +497,11 @@ module Mimi
462
497
  def create_topic(topic_name)
463
498
  fqn = sqs_sns_converted_full_name(topic_name)
464
499
  Mimi::Messaging.log "Creating a topic: #{fqn}"
465
- result = sns_client.create_topic(name: fqn)
500
+ attrs = {}
501
+ if options[:mq_aws_sqs_sns_kms_master_key_id]
502
+ attrs["KmsMasterKeyId"] = options[:mq_aws_sqs_sns_kms_master_key_id]
503
+ end
504
+ result = sns_client.create_topic(name: fqn, attributes: attrs)
466
505
  result.topic_arn
467
506
  rescue StandardError => e
468
507
  raise Mimi::Messaging::ConnectionError, "Failed to create topic '#{topic_name}': #{e}"
@@ -479,7 +518,7 @@ module Mimi
479
518
  )
480
519
  queue_arn = result.attributes["QueueArn"]
481
520
  Mimi::Messaging.log "Subscribing queue to a topic: '#{topic_arn}'->'#{queue_url}'"
482
- result = sns_client.subscribe(
521
+ _result = sns_client.subscribe(
483
522
  topic_arn: topic_arn,
484
523
  protocol: "sqs",
485
524
  endpoint: queue_arn,
@@ -501,6 +540,7 @@ module Mimi
501
540
  unless message.is_a?(Mimi::Messaging::Message)
502
541
  raise ArgumentError, "Message is expected as argument"
503
542
  end
543
+
504
544
  Mimi::Messaging.log "Delivering message to: #{topic_arn}"
505
545
  sns_client.publish(
506
546
  topic_arn: topic_arn,
@@ -512,6 +552,46 @@ module Mimi
512
552
  rescue StandardError => e
513
553
  raise Mimi::Messaging::ConnectionError, "Failed to deliver message to '#{topic_arn}': #{e}"
514
554
  end
555
+
556
+ # Processes an incoming EVENT message
557
+ #
558
+ # @param processor [#call_event()] event processor object
559
+ # @param sqs_message []
560
+ #
561
+ def process_event_message(processor, sqs_message)
562
+ message = Mimi::Messaging::Message.new(
563
+ deserialize(sqs_message.body),
564
+ deserialize_headers(sqs_message)
565
+ )
566
+ event_type = message.headers[:__event_type]
567
+ processor.call_event(event_type, message, {})
568
+ end
569
+
570
+ # Starts the worker pool using current configuration
571
+ #
572
+ # @return [Concurrent::ThreadPoolExecutor]
573
+ #
574
+ def start_worker_pool!
575
+ Mimi::Messaging.log "Starting worker pool, " \
576
+ "min_threads:#{options[:mq_worker_pool_min_threads]}, " \
577
+ "max_threads:#{options[:mq_worker_pool_max_threads]}, " \
578
+ "max_backlog:#{options[:mq_worker_pool_max_backlog]}"
579
+
580
+ @worker_pool = Concurrent::ThreadPoolExecutor.new(
581
+ min_threads: options[:mq_worker_pool_min_threads],
582
+ max_threads: options[:mq_worker_pool_max_threads],
583
+ max_queue: options[:mq_worker_pool_max_backlog],
584
+ fallback_policy: :abort
585
+ )
586
+ end
587
+
588
+ # Gracefully stops the worker pool, allowing all threads to finish their jobs
589
+ #
590
+ def stop_worker_pool!
591
+ Mimi::Messaging.log "Stopping worker pool"
592
+ @worker_pool.shutdown
593
+ @worker_pool.wait_for_termination
594
+ end
515
595
  end # class Adapter
516
596
  end # module SQS_SNS
517
597
  end # module Messaging
@@ -7,17 +7,28 @@ module Mimi
7
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
14
  @stop_requested = false
12
15
  Mimi::Messaging.log "Starting consumer for: #{queue_url}"
13
16
  @consumer_thread = Thread.new do
14
- while not @stop_requested do
17
+ while not @stop_requested
15
18
  read_and_process_message(adapter, queue_url, block)
16
19
  end
17
20
  Mimi::Messaging.log "Stopping consumer for: #{queue_url}"
18
21
  end
19
22
  end
20
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
+ #
21
32
  def stop
22
33
  @stop_requested = true
23
34
  @consumer_thread.join
@@ -35,12 +46,21 @@ module Mimi
35
46
  def read_and_process_message(adapter, queue_url, block)
36
47
  message = read_message(adapter, queue_url)
37
48
  return unless message
49
+
38
50
  Mimi::Messaging.log "Read message from: #{queue_url}"
39
- block.call(message)
40
- ack_message(adapter, queue_url, message)
51
+ begin
52
+ adapter.worker_pool.post do
53
+ process_message(adapter, queue_url, message, block)
54
+ end
55
+ rescue Concurrent::RejectedExecutionError
56
+ # the backlog is overflown, put the message back
57
+ Mimi::Messaging.log "Worker pool backlog is full, nack-ing the message " \
58
+ "(workers:#{adapter.worker_pool.length}, backlog:#{adapter.worker_pool.queue_length})"
59
+ nack_message(adapter, queue_url, message)
60
+ end
41
61
  rescue StandardError => e
42
62
  Mimi::Messaging.logger&.error(
43
- "#{self.class}: failed to read and process message from: #{queue_url}," \
63
+ "#{self.class}: failed to read or process message from: #{queue_url}," \
44
64
  " error: (#{e.class}) #{e}"
45
65
  )
46
66
  end
@@ -54,14 +74,49 @@ module Mimi
54
74
  )
55
75
  return nil if result.messages.count == 0
56
76
  return result.messages.first if result.messages.count == 1
77
+
57
78
  raise Mimi::Messaging::ConnectionError, "Unexpected number of messages read"
58
79
  end
59
80
 
81
+ def process_message(adapter, queue_url, message, block)
82
+ block.call(message)
83
+ ack_message(adapter, queue_url, message)
84
+ rescue Mimi::Messaging::NACK
85
+ Mimi::Messaging.log "NACK-ing message from: #{queue_url}"
86
+ nack_message(adapter, queue_url, message)
87
+ rescue StandardError => e
88
+ Mimi::Messaging.logger&.error(
89
+ "#{self.class}: failed to process message from: #{queue_url}," \
90
+ " error: (#{e.class}) #{e}"
91
+ )
92
+ # NOTE: error is recovered and the message is neither ACKed or NACKed
93
+ end
94
+
95
+ # ACK-ing the message indicates successfull processing of it
96
+ # and removes the message from the queue
97
+ #
60
98
  def ack_message(adapter, queue_url, msg)
61
99
  adapter.sqs_client.delete_message(
62
100
  queue_url: queue_url, receipt_handle: msg.receipt_handle
63
101
  )
64
102
  end
103
+
104
+ # NACK-ing the message indicates a failure to process the message.
105
+ # The message becomes immediately available to other consumers.
106
+ #
107
+ def nack_message(adapter, queue_url, msg)
108
+ adapter.sqs_client.change_message_visibility(
109
+ queue_url: queue_url,
110
+ receipt_handle: msg.receipt_handle,
111
+ visibility_timeout: NACK_VISIBILITY_TIMEOUT
112
+ )
113
+ rescue StandardError => e
114
+ Mimi::Messaging.logger&.error(
115
+ "#{self.class}: failed to NACK message from: #{queue_url}," \
116
+ " error: (#{e.class}) #{e}"
117
+ )
118
+ # NOTE: error is recovered and the message is neither ACKed or NACKed
119
+ end
65
120
  end # class Consumer
66
121
  end # module SQS_SNS
67
122
  end # module Messaging
@@ -36,7 +36,7 @@ module Mimi
36
36
  # @return [Queue] a new Queue object registered for this request_id
37
37
  #
38
38
  def register_request_id(request_id)
39
- queue = Queue.new
39
+ queue = TimeoutQueue.new
40
40
  @mutex.synchronize do
41
41
  queue = @queues[request_id] ||= queue
42
42
  end
@@ -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.5.0"
6
+ VERSION = "0.8.2"
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.5.0
4
+ version: 0.8.2
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-11-17 00:00:00.000000000 Z
11
+ date: 2021-03-15 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
@@ -134,6 +157,7 @@ files:
134
157
  - lib/mimi/messaging/sqs_sns/consumer.rb
135
158
  - lib/mimi/messaging/sqs_sns/reply_consumer.rb
136
159
  - lib/mimi/messaging/sqs_sns/temporary_queue_consumer.rb
160
+ - lib/mimi/messaging/sqs_sns/timeout_queue.rb
137
161
  - lib/mimi/messaging/sqs_sns/version.rb
138
162
  - mimi-messaging-sqs_sns.gemspec
139
163
  homepage: https://github.com/kukushkin/mimi-messaging-sqs_sns
@@ -158,8 +182,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
158
182
  - !ruby/object:Gem::Version
159
183
  version: '0'
160
184
  requirements: []
161
- rubyforge_project:
162
- rubygems_version: 2.6.14.4
185
+ rubygems_version: 3.1.2
163
186
  signing_key:
164
187
  specification_version: 4
165
188
  summary: AWS SQS/SNS adapter for mimi-messaging