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 +5 -5
- data/CHANGELOG.md +35 -0
- data/README.md +3 -1
- data/TODO.md +9 -0
- data/examples/event.rb +3 -3
- data/examples/mass-query.rb +53 -0
- data/examples/query.rb +5 -3
- data/examples/responder.rb +13 -7
- data/examples/subscriber.rb +6 -3
- data/lib/mimi/messaging/sqs_sns/adapter.rb +117 -37
- data/lib/mimi/messaging/sqs_sns/consumer.rb +59 -4
- data/lib/mimi/messaging/sqs_sns/reply_consumer.rb +1 -1
- data/lib/mimi/messaging/sqs_sns/timeout_queue.rb +65 -0
- data/lib/mimi/messaging/sqs_sns/version.rb +1 -1
- data/mimi-messaging-sqs_sns.gemspec +2 -1
- metadata +29 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e106fbe54e012ffd59a6126292242b23c28b3086ff5dfd4866acb64bad559ccf
|
4
|
+
data.tar.gz: 2fce6feb750d6cf27925e79646b8e76359188343f663ee8d20f30f09eedc2ba0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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:
|
8
|
-
AWS_SNS_ENDPOINT_URL = "http://localstack:
|
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:
|
8
|
-
AWS_SNS_ENDPOINT_URL = "http://localstack:
|
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
|
|
data/examples/responder.rb
CHANGED
@@ -2,11 +2,12 @@
|
|
2
2
|
|
3
3
|
require "mimi/messaging/sqs_sns"
|
4
4
|
|
5
|
-
AWS_REGION
|
6
|
-
AWS_SQS_ENDPOINT_URL
|
7
|
-
AWS_SNS_ENDPOINT_URL
|
8
|
-
AWS_ACCESS_KEY_ID
|
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::
|
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
|
-
|
data/examples/subscriber.rb
CHANGED
@@ -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
|
7
|
-
AWS_SNS_ENDPOINT_URL
|
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
|
-
|
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,
|
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 =
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
@
|
383
|
-
|
384
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
40
|
-
|
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
|
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
|
@@ -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
|
@@ -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", "~>
|
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
|
+
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:
|
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: '
|
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: '
|
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
|
-
|
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
|