aws-sdk-sqs 1.0.0.rc1
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 +7 -0
- data/lib/aws-sdk-sqs.rb +47 -0
- data/lib/aws-sdk-sqs/client.rb +1604 -0
- data/lib/aws-sdk-sqs/client_api.rb +480 -0
- data/lib/aws-sdk-sqs/customizations.rb +2 -0
- data/lib/aws-sdk-sqs/errors.rb +23 -0
- data/lib/aws-sdk-sqs/plugins/md5s.rb +166 -0
- data/lib/aws-sdk-sqs/plugins/queue_urls.rb +43 -0
- data/lib/aws-sdk-sqs/queue_poller.rb +521 -0
- data/lib/aws-sdk-sqs/resource.rb +25 -0
- data/lib/aws-sdk-sqs/types.rb +1642 -0
- metadata +83 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
# WARNING ABOUT GENERATED CODE
|
2
|
+
#
|
3
|
+
# This file is generated. See the contributing for info on making contributions:
|
4
|
+
# https://github.com/aws/aws-sdk-ruby/blob/master/CONTRIBUTING.md
|
5
|
+
#
|
6
|
+
# WARNING ABOUT GENERATED CODE
|
7
|
+
|
8
|
+
module Aws
|
9
|
+
module SQS
|
10
|
+
module Errors
|
11
|
+
|
12
|
+
extend Aws::Errors::DynamicErrors
|
13
|
+
|
14
|
+
# Raised when calling #load or #data on a resource class that can not be
|
15
|
+
# loaded. This can happen when:
|
16
|
+
#
|
17
|
+
# * A resource class has identifiers, but no data attributes.
|
18
|
+
# * Resource data is only available when making an API call that
|
19
|
+
# enumerates all resources of that type.
|
20
|
+
class ResourceNotLoadable < RuntimeError; end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module Aws
|
4
|
+
module SQS
|
5
|
+
module Plugins
|
6
|
+
class Md5s < Seahorse::Client::Plugin
|
7
|
+
|
8
|
+
# @api private
|
9
|
+
class Handler < Seahorse::Client::Handler
|
10
|
+
def call(context)
|
11
|
+
@handler.call(context).on_success do |response|
|
12
|
+
case context.operation_name
|
13
|
+
when :send_message
|
14
|
+
validate_send_message(context, response)
|
15
|
+
when :send_message_batch
|
16
|
+
validate_send_message_batch(context, response)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
TRANSPORT_TYPE_ENCODINGS = {
|
24
|
+
'String' => 1,
|
25
|
+
'Binary' => 2,
|
26
|
+
'Number' => 1
|
27
|
+
}
|
28
|
+
|
29
|
+
DATA_TYPE = /\A(String|Binary|Number)(\..+)?\z/
|
30
|
+
|
31
|
+
NORMALIZED_ENCODING = Encoding::UTF_8
|
32
|
+
|
33
|
+
def validate_send_message(context, response)
|
34
|
+
body = context.params[:message_body]
|
35
|
+
attributes = context.params[:message_attributes]
|
36
|
+
validate_single_message(body, attributes, response)
|
37
|
+
end
|
38
|
+
|
39
|
+
def validate_send_message_batch(context, response)
|
40
|
+
context.params[:entries].each do |entry|
|
41
|
+
id = entry[:id]
|
42
|
+
body = entry[:message_body]
|
43
|
+
attributes = entry[:message_attributes]
|
44
|
+
message_response = response.successful.select { |r| r.id == id }[0]
|
45
|
+
unless message_response.nil?
|
46
|
+
validate_single_message(body, attributes, message_response)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def validate_single_message(body, attributes, response)
|
52
|
+
validate_body(body, response)
|
53
|
+
validate_attributes(attributes, response) unless attributes.nil?
|
54
|
+
end
|
55
|
+
|
56
|
+
def validate_body(body, response)
|
57
|
+
calculated_md5 = md5_of_message_body(body)
|
58
|
+
returned_md5 = response.md5_of_message_body
|
59
|
+
if calculated_md5 != returned_md5
|
60
|
+
error_message = mismatch_error_message(
|
61
|
+
'message body',
|
62
|
+
calculated_md5,
|
63
|
+
returned_md5,
|
64
|
+
response)
|
65
|
+
raise Aws::Errors::ChecksumError, error_message
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def validate_attributes(attributes, response)
|
70
|
+
calculated_md5 = md5_of_message_attributes(attributes)
|
71
|
+
returned_md5 = response.md5_of_message_attributes
|
72
|
+
if returned_md5 != calculated_md5
|
73
|
+
error_message = mismatch_error_message(
|
74
|
+
'message atributes',
|
75
|
+
calculated_md5,
|
76
|
+
returned_md5,
|
77
|
+
response)
|
78
|
+
raise Aws::Errors::ChecksumError, error_message
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def md5_of_message_body(message_body)
|
83
|
+
OpenSSL::Digest::MD5.hexdigest(message_body)
|
84
|
+
end
|
85
|
+
|
86
|
+
def md5_of_message_attributes(message_attributes)
|
87
|
+
encoded = { }
|
88
|
+
message_attributes.each do |name, attribute|
|
89
|
+
name = name.to_s
|
90
|
+
encoded[name] = String.new
|
91
|
+
data_type_without_label = DATA_TYPE.match(attribute[:data_type])[1]
|
92
|
+
encoded[name] << encode_length_and_bytes(name) <<
|
93
|
+
encode_length_and_bytes(attribute[:data_type]) <<
|
94
|
+
[TRANSPORT_TYPE_ENCODINGS[data_type_without_label]].pack('C'.freeze)
|
95
|
+
|
96
|
+
if attribute[:string_value] != nil
|
97
|
+
encoded[name] << encode_length_and_string(attribute[:string_value])
|
98
|
+
elsif attribute[:binary_value] != nil
|
99
|
+
encoded[name] << encode_length_and_bytes(attribute[:binary_value])
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
buffer = encoded.keys.sort.reduce(String.new) do |string, name|
|
104
|
+
string << encoded[name]
|
105
|
+
end
|
106
|
+
OpenSSL::Digest::MD5.hexdigest(buffer)
|
107
|
+
end
|
108
|
+
|
109
|
+
def encode_length_and_string(string)
|
110
|
+
string = String.new(string)
|
111
|
+
string.encode!(NORMALIZED_ENCODING)
|
112
|
+
encode_length_and_bytes(string)
|
113
|
+
end
|
114
|
+
|
115
|
+
def encode_length_and_bytes(bytes)
|
116
|
+
[bytes.bytesize, bytes].pack('L>a*'.freeze)
|
117
|
+
end
|
118
|
+
|
119
|
+
def mismatch_error_message(section, local_md5, returned_md5, response)
|
120
|
+
m = "MD5 returned by SQS does not match " <<
|
121
|
+
"the calculation on the original request. ("
|
122
|
+
|
123
|
+
if response.respond_to?(:id) && !response.id.nil?
|
124
|
+
m << "Message ID: #{response.id}, "
|
125
|
+
end
|
126
|
+
|
127
|
+
m << "MD5 calculated by the #{section}: " <<
|
128
|
+
"'#{local_md5}', MD5 checksum returned: '#{returned_md5}')"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
option(:verify_checksums,
|
133
|
+
doc_default: true,
|
134
|
+
doc_type: 'Boolean',
|
135
|
+
docstring: <<-DOCS
|
136
|
+
When `true` MD5 checksums will be computed for messages sent to
|
137
|
+
an SQS queue and matched against MD5 checksums returned by Amazon SQS.
|
138
|
+
`Aws::Errors::Checksum` errors are raised for cases where checksums do
|
139
|
+
not match.
|
140
|
+
DOCS
|
141
|
+
) do |config|
|
142
|
+
# By default, we will disable checksum verification when response
|
143
|
+
# stubbing is enable. If a user decides to enable both features,
|
144
|
+
# then they will need to stub the MD5s in the response.
|
145
|
+
# See the spec/aws/sqs/client/verify_checksums_spec.rb for
|
146
|
+
# examples of how to do this.
|
147
|
+
if config.respond_to?(:stub_responses)
|
148
|
+
!config.stub_responses
|
149
|
+
else
|
150
|
+
config.verify_checksums
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def add_handlers(handlers, config)
|
155
|
+
if config.verify_checksums
|
156
|
+
handlers.add(Handler, {
|
157
|
+
priority: 10 ,
|
158
|
+
step: :validate,
|
159
|
+
operations: [:send_message, :send_message_batch]
|
160
|
+
})
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Aws
|
2
|
+
module SQS
|
3
|
+
module Plugins
|
4
|
+
# @api private
|
5
|
+
class QueueUrls < Seahorse::Client::Plugin
|
6
|
+
|
7
|
+
class Handler < Seahorse::Client::Handler
|
8
|
+
|
9
|
+
def call(context)
|
10
|
+
if queue_url = context.params[:queue_url]
|
11
|
+
update_endpoint(context, queue_url)
|
12
|
+
update_region(context, queue_url)
|
13
|
+
end
|
14
|
+
@handler.call(context)
|
15
|
+
end
|
16
|
+
|
17
|
+
def update_endpoint(context, url)
|
18
|
+
context.http_request.endpoint = url
|
19
|
+
end
|
20
|
+
|
21
|
+
# If the region in the queue url is not the configured
|
22
|
+
# region, then we will modify the request to have
|
23
|
+
# a sigv4 signer for the proper region.
|
24
|
+
def update_region(context, queue_url)
|
25
|
+
if queue_region = queue_url.to_s.split('.')[1]
|
26
|
+
if queue_region != context.config.region
|
27
|
+
config = context.config.dup
|
28
|
+
config.region = queue_region
|
29
|
+
config.sigv4_region = queue_region
|
30
|
+
config.sigv4_signer = Aws::Plugins::SignatureV4.build_signer(config)
|
31
|
+
context.config = config
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
handler(Handler)
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,521 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Aws
|
4
|
+
module SQS
|
5
|
+
|
6
|
+
# A utility class for long polling messages in a loop. **Messages are
|
7
|
+
# automatically deleted from the queue at the end of the given block.**
|
8
|
+
#
|
9
|
+
# poller = Aws::SQS::QueuePoller.new(queue_url)
|
10
|
+
#
|
11
|
+
# poller.poll do |msg|
|
12
|
+
# puts msg.body
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# ## Long Polling
|
16
|
+
#
|
17
|
+
# By default, messages are received using long polling. This
|
18
|
+
# method will force a default `:wait_time_seconds` of 20 seconds.
|
19
|
+
# If you prefer to use the queue default wait time, then pass
|
20
|
+
# a `nil` value for `:wait_time_seconds`.
|
21
|
+
#
|
22
|
+
# # disables 20 second default, use queue ReceiveMessageWaitTimeSeconds
|
23
|
+
# poller.poll(wait_time_seconds:nil) do |msg|
|
24
|
+
# # ...
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# When disabling `:wait_time_seconds` by passing `nil`, you must
|
28
|
+
# ensure the queue `ReceiveMessageWaitTimeSeconds` attribute is
|
29
|
+
# set to a non-zero value, or you will be short-polling.
|
30
|
+
# This will trigger significantly more API calls.
|
31
|
+
#
|
32
|
+
# ## Batch Receiving Messages
|
33
|
+
#
|
34
|
+
# You can specify a maximum number of messages to receive with
|
35
|
+
# each polling attempt via `:max_number_of_messages`. When this is
|
36
|
+
# set to a positive value, greater than 1, the block will receive
|
37
|
+
# an array of messages, instead of a single message.
|
38
|
+
#
|
39
|
+
# # receives and yields 1 message at a time
|
40
|
+
# poller.poll do |msg|
|
41
|
+
# # ...
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# # receives and yields up to 10 messages at a time
|
45
|
+
# poller.poll(max_number_of_messages:10) do |messages|
|
46
|
+
# messages.each do |msg|
|
47
|
+
# # ...
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# The maximum value for `:max_number_of_messages` is enforced by
|
52
|
+
# Amazon SQS.
|
53
|
+
#
|
54
|
+
# ## Visibility Timeouts
|
55
|
+
#
|
56
|
+
# When receiving messages, you have a fixed amount of time to process
|
57
|
+
# and delete the message before it is added back into the queue. This
|
58
|
+
# is the visibility timeout. By default, the queue's `VisibilityTimeout`
|
59
|
+
# attribute is used. You can provide an alternative visibility timeout
|
60
|
+
# when polling.
|
61
|
+
#
|
62
|
+
# # queue default VisibilityTimeout
|
63
|
+
# poller.poll do |msg|
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# # custom visibility timeout
|
67
|
+
# poller.poll(visibility_timeout:10) do |msg|
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
#
|
71
|
+
# You can reset the visibility timeout of a single message by calling
|
72
|
+
# {#change_message_visibility_timeout}. This is useful when you need
|
73
|
+
# more time to finish processing the message.
|
74
|
+
#
|
75
|
+
# poller.poll do |msg|
|
76
|
+
#
|
77
|
+
# # do work ...
|
78
|
+
#
|
79
|
+
# # need more time for processing
|
80
|
+
# poller.change_message_visibility_timeout(msg, 60)
|
81
|
+
#
|
82
|
+
# # finish work ...
|
83
|
+
#
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# If you change the visibility timeout of a message to zero, it will
|
87
|
+
# return to the queue immediately.
|
88
|
+
#
|
89
|
+
# ## Deleting Messages
|
90
|
+
#
|
91
|
+
# Messages are deleted from the queue when the block returns normally.
|
92
|
+
#
|
93
|
+
# poller.poll do |msg|
|
94
|
+
# # do work
|
95
|
+
# end # messages deleted here
|
96
|
+
#
|
97
|
+
# You can skip message deletion by passing `skip_delete: true`.
|
98
|
+
# This allows you to manually delete the messages using
|
99
|
+
# {#delete_message}, or {#delete_messages}.
|
100
|
+
#
|
101
|
+
# # single message
|
102
|
+
# poller.poll(skip_delete: true) do |msg|
|
103
|
+
# poller.delete_message(msg) # if successful
|
104
|
+
# end
|
105
|
+
#
|
106
|
+
# # batch delete messages
|
107
|
+
# poller.poll(skip_delete: true, max_number_of_messages:10) do |messages|
|
108
|
+
# poller.delete_messages(messages)
|
109
|
+
# end
|
110
|
+
#
|
111
|
+
# Another way to manage message deletion is to throw `:skip_delete`
|
112
|
+
# from the poll block. You can use this to choose when a message, or
|
113
|
+
# message batch is deleted on an individual basis. This can be very
|
114
|
+
# useful when you are capturing temporal errors and wish for the
|
115
|
+
# message to timeout.
|
116
|
+
#
|
117
|
+
# poller.poll do |msg|
|
118
|
+
# begin
|
119
|
+
# # do work
|
120
|
+
# rescue
|
121
|
+
# # unexpected error occurred while processing messages,
|
122
|
+
# # log it, and skip delete so it can be re-processed later
|
123
|
+
# throw :skip_delete
|
124
|
+
# end
|
125
|
+
# end
|
126
|
+
#
|
127
|
+
# ## Terminating the Polling Loop
|
128
|
+
#
|
129
|
+
# By default, polling will continue indefinitely. You can stop
|
130
|
+
# the poller by providing an idle timeout or by throwing `:stop_polling`
|
131
|
+
# from the {#before_request} callback.
|
132
|
+
#
|
133
|
+
# ### `:idle_timeout` Option
|
134
|
+
#
|
135
|
+
# This is a configurable, maximum number of seconds to wait for a
|
136
|
+
# new message before the polling loop exists. By default, there is
|
137
|
+
# no idle timeout.
|
138
|
+
#
|
139
|
+
# # stops polling after a minute of no received messages
|
140
|
+
# poller.poll(idle_timeout: 60) do |msg|
|
141
|
+
# # ...
|
142
|
+
# end
|
143
|
+
#
|
144
|
+
# ### Throw `:stop_polling`
|
145
|
+
#
|
146
|
+
# If you want more fine grained control, you can configure a
|
147
|
+
# before request callback to trigger before each long poll. Throwing
|
148
|
+
# `:stop_polling` from this callback will cause the poller to exit
|
149
|
+
# normally without making the next request.
|
150
|
+
#
|
151
|
+
# # stop after processing 100 messages
|
152
|
+
# poller.before_request do |stats|
|
153
|
+
# throw :stop_polling if stats.received_message_count >= 100
|
154
|
+
# end
|
155
|
+
#
|
156
|
+
# poller.poll do |msg|
|
157
|
+
# # do work ...
|
158
|
+
# end
|
159
|
+
#
|
160
|
+
# ## Tracking Progress
|
161
|
+
#
|
162
|
+
# The poller will automatically track a few statistics client-side in
|
163
|
+
# a {PollerStats} object. You can access the poller stats
|
164
|
+
# three ways:
|
165
|
+
#
|
166
|
+
# * The first block argument of {#before_request}
|
167
|
+
# * The second block argument of {#poll}.
|
168
|
+
# * The return value from {#poll}.
|
169
|
+
#
|
170
|
+
# Here are examples of accessing the statistics.
|
171
|
+
#
|
172
|
+
# * Configure a {#before_request} callback.
|
173
|
+
#
|
174
|
+
# ```
|
175
|
+
# poller.before_request do |stats|
|
176
|
+
# logger.info("requests: #{stats.request_count}")
|
177
|
+
# logger.info("messages: #{stats.received_message_count}")
|
178
|
+
# logger.info("last-timestamp: #{stats.last_message_received_at}")
|
179
|
+
# end
|
180
|
+
# ```
|
181
|
+
#
|
182
|
+
# * Accept a 2nd argument in the poll block, for example:
|
183
|
+
#
|
184
|
+
# ```
|
185
|
+
# poller.poll do |msg, stats|
|
186
|
+
# logger.info("requests: #{stats.request_count}")
|
187
|
+
# logger.info("messages: #{stats.received_message_count}")
|
188
|
+
# logger.info("last-timestamp: #{stats.last_message_received_at}")
|
189
|
+
# end
|
190
|
+
# ```
|
191
|
+
#
|
192
|
+
# * Return value:
|
193
|
+
#
|
194
|
+
# ```
|
195
|
+
# stats = poller.poll(idle_timeout:10) do |msg|
|
196
|
+
# # do work ...
|
197
|
+
# end
|
198
|
+
# logger.info("requests: #{stats.request_count}")
|
199
|
+
# logger.info("messages: #{stats.received_message_count}")
|
200
|
+
# logger.info("last-timestamp: #{stats.last_message_received_at}")
|
201
|
+
# ```
|
202
|
+
#
|
203
|
+
class QueuePoller
|
204
|
+
|
205
|
+
# @param [String] queue_url
|
206
|
+
# @option options [Client] :client
|
207
|
+
# @option (see #poll)
|
208
|
+
def initialize(queue_url, options = {})
|
209
|
+
@queue_url = queue_url
|
210
|
+
@client = options.delete(:client) || Client.new
|
211
|
+
@default_config = PollerConfig.new(options)
|
212
|
+
end
|
213
|
+
|
214
|
+
# @return [String]
|
215
|
+
attr_reader :queue_url
|
216
|
+
|
217
|
+
# @return [Client]
|
218
|
+
attr_reader :client
|
219
|
+
|
220
|
+
# @return [PollerConfig]
|
221
|
+
attr_reader :default_config
|
222
|
+
|
223
|
+
# Registers a callback that is invoked once before every polling
|
224
|
+
# attempt.
|
225
|
+
#
|
226
|
+
# poller.before_request do |stats|
|
227
|
+
# logger.info("requests: #{stats.request_count}")
|
228
|
+
# logger.info("messages: #{stats.received_message_count}")
|
229
|
+
# logger.info("last-timestamp: #{stats.last_message_received_at}")
|
230
|
+
# end
|
231
|
+
#
|
232
|
+
# poller.poll do |msg|
|
233
|
+
# # do work ...
|
234
|
+
# end
|
235
|
+
#
|
236
|
+
# ## `:stop_polling`
|
237
|
+
#
|
238
|
+
# If you throw `:stop_polling` from the {#before_request} callback,
|
239
|
+
# then the poller will exit normally before making the next long
|
240
|
+
# poll request.
|
241
|
+
#
|
242
|
+
# poller.before_request do |stats|
|
243
|
+
# throw :stop_polling if stats.received_messages >= 100
|
244
|
+
# end
|
245
|
+
#
|
246
|
+
# # at most 100 messages will be yielded
|
247
|
+
# poller.poll do |msg|
|
248
|
+
# # do work ...
|
249
|
+
# end
|
250
|
+
#
|
251
|
+
# @yieldparam [PollerStats] stats An object that tracks a few
|
252
|
+
# client-side statistics about the queue polling.
|
253
|
+
#
|
254
|
+
# @return [void]
|
255
|
+
def before_request(&block)
|
256
|
+
@default_config = @default_config.with(before_request: Proc.new)
|
257
|
+
end
|
258
|
+
|
259
|
+
# Polls the queue, yielded a message, or an array of messages.
|
260
|
+
# Messages are automatically deleted from the queue at the
|
261
|
+
# end of the given block. See the class documentation on
|
262
|
+
# {QueuePoller} for more examples.
|
263
|
+
#
|
264
|
+
# @example Basic example, loops indefinitely
|
265
|
+
#
|
266
|
+
# poller.poll do |msg|
|
267
|
+
# # ...
|
268
|
+
# end
|
269
|
+
#
|
270
|
+
# @example Receives and deletes messages as a batch
|
271
|
+
#
|
272
|
+
# poller.poll(max_number_of_messages:10) do |messages|
|
273
|
+
# messages.each do |msg|
|
274
|
+
# # ...
|
275
|
+
# end
|
276
|
+
# end
|
277
|
+
#
|
278
|
+
# @option options [Integer] :wait_time_seconds (20) The
|
279
|
+
# long polling interval. Messages are yielded as soon as they are
|
280
|
+
# received. The `:wait_time_seconds` option specifies the max
|
281
|
+
# duration for each polling attempt before a new request is
|
282
|
+
# sent to receive messages.
|
283
|
+
#
|
284
|
+
# @option options [Integer] :max_number_of_messages (1) The maximum
|
285
|
+
# number of messages to yield from each polling attempt.
|
286
|
+
# Values can be from 1 to 10.
|
287
|
+
#
|
288
|
+
# @option options [Integer] :visibility_timeout (nil)
|
289
|
+
# The number of seconds you have to process a message before
|
290
|
+
# it is put back into the queue and can be received again.
|
291
|
+
# By default, the queue's
|
292
|
+
#
|
293
|
+
# @option options [Array<String>] :attribute_names ([])
|
294
|
+
# The list of attributes that need to be returned along with each
|
295
|
+
# message. Valid attribute names include:
|
296
|
+
#
|
297
|
+
# * `All` - All attributes.
|
298
|
+
# * `ApproximateFirstReceiveTimestamp` - The time when the message
|
299
|
+
# was first received from the queue (epoch time in milliseconds).
|
300
|
+
# * `ApproximateReceiveCount` - The number of times a message has
|
301
|
+
# been received from the queue but not deleted.
|
302
|
+
# * `SenderId` - The AWS account number (or the IP address, if
|
303
|
+
# anonymous access is allowed) of the sender.
|
304
|
+
# * `SentTimestamp` - The time when the message was sent to the
|
305
|
+
# queue (epoch time in milliseconds).
|
306
|
+
#
|
307
|
+
# @option options [Array<String>] :message_attribute_names ([])
|
308
|
+
# A list of message attributes to receive. You can receive
|
309
|
+
# all messages by using `All` or `.*`. You can also use
|
310
|
+
# `foo.*` to return all message attributes starting with the
|
311
|
+
# `foo` prefix.
|
312
|
+
#
|
313
|
+
# @option options [Integer] :idle_timeout (nil) Polling terminates
|
314
|
+
# gracefully when `:idle_timeout` seconds have passed without
|
315
|
+
# receiving any messages.
|
316
|
+
#
|
317
|
+
# @option options [Boolean] :skip_delete (false) When `true`, messages
|
318
|
+
# are not deleted after polling block. If you wish to delete
|
319
|
+
# received messages, you will need to call `#delete_message` or
|
320
|
+
# `#delete_messages` manually.
|
321
|
+
#
|
322
|
+
# @option options [Proc] :before_request (nil) Called before each
|
323
|
+
# polling attempt. This proc receives a single argument, an
|
324
|
+
# instance of {PollerStats}.
|
325
|
+
#
|
326
|
+
# @return [PollerStats]
|
327
|
+
def poll(options = {}, &block)
|
328
|
+
config = @default_config.with(options)
|
329
|
+
stats = PollerStats.new
|
330
|
+
catch(:stop_polling) do
|
331
|
+
loop do
|
332
|
+
messages = get_messages(config, stats)
|
333
|
+
if messages.empty?
|
334
|
+
check_idle_timeout(config, stats, messages)
|
335
|
+
else
|
336
|
+
process_messages(config, stats, messages, &block)
|
337
|
+
end
|
338
|
+
end
|
339
|
+
end
|
340
|
+
stats.polling_stopped_at = Time.now
|
341
|
+
stats
|
342
|
+
end
|
343
|
+
|
344
|
+
# @note This method should be called from inside a {#poll} block.
|
345
|
+
# @param [#receipt_handle] message An object that responds to
|
346
|
+
# `#receipt_handle`.
|
347
|
+
# @param [Integer] seconds
|
348
|
+
def change_message_visibility_timeout(message, seconds)
|
349
|
+
@client.change_message_visibility({
|
350
|
+
queue_url: @queue_url,
|
351
|
+
receipt_handle: message.receipt_handle,
|
352
|
+
visibility_timeout: seconds,
|
353
|
+
})
|
354
|
+
end
|
355
|
+
|
356
|
+
# @note This method should be called from inside a {#poll} block.
|
357
|
+
# @param [#receipt_handle] message An object that responds to
|
358
|
+
# `#receipt_handle`.
|
359
|
+
def delete_message(message)
|
360
|
+
@client.delete_message({
|
361
|
+
queue_url: @queue_url,
|
362
|
+
receipt_handle: message.receipt_handle,
|
363
|
+
})
|
364
|
+
end
|
365
|
+
|
366
|
+
# @note This method should be called from inside a {#poll} block.
|
367
|
+
# @param [Array<#message_id, #receipt_handle>] messages An array of received
|
368
|
+
# messages. Each object must respond to `#message_id` and
|
369
|
+
# `#receipt_handle`.
|
370
|
+
def delete_messages(messages)
|
371
|
+
@client.delete_message_batch(
|
372
|
+
queue_url: @queue_url,
|
373
|
+
entries: messages.map { |msg|
|
374
|
+
{ id: msg.message_id, receipt_handle: msg.receipt_handle }
|
375
|
+
}
|
376
|
+
)
|
377
|
+
end
|
378
|
+
|
379
|
+
private
|
380
|
+
|
381
|
+
def get_messages(config, stats)
|
382
|
+
config.before_request.call(stats) if config.before_request
|
383
|
+
messages = send_request(config).messages
|
384
|
+
stats.request_count += 1
|
385
|
+
messages
|
386
|
+
end
|
387
|
+
|
388
|
+
def send_request(config)
|
389
|
+
params = config.request_params.merge(queue_url: @queue_url)
|
390
|
+
@client.receive_message(params)
|
391
|
+
end
|
392
|
+
|
393
|
+
def check_idle_timeout(config, stats, messages)
|
394
|
+
if config.idle_timeout
|
395
|
+
since = stats.last_message_received_at || stats.polling_started_at
|
396
|
+
idle_time = Time.now - since
|
397
|
+
throw :stop_polling if idle_time > config.idle_timeout
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
def process_messages(config, stats, messages, &block)
|
402
|
+
stats.received_message_count += messages.count
|
403
|
+
stats.last_message_received_at = Time.now
|
404
|
+
catch(:skip_delete) do
|
405
|
+
yield_messages(config, messages, stats, &block)
|
406
|
+
delete_messages(messages) unless config.skip_delete
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
def yield_messages(config, messages, stats, &block)
|
411
|
+
if config.request_params[:max_number_of_messages] == 1
|
412
|
+
messages.each do |msg|
|
413
|
+
yield(msg, stats)
|
414
|
+
end
|
415
|
+
else
|
416
|
+
yield(messages, stats)
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
# Statistics tracked client-side by the {QueuePoller}.
|
421
|
+
class PollerStats
|
422
|
+
|
423
|
+
def initialize
|
424
|
+
@request_count = 0
|
425
|
+
@received_message_count = 0
|
426
|
+
@last_message_received_at = nil
|
427
|
+
@polling_started_at = Time.now
|
428
|
+
@polling_stopped_at = nil
|
429
|
+
end
|
430
|
+
|
431
|
+
# @return [Integer]
|
432
|
+
attr_accessor :request_count
|
433
|
+
|
434
|
+
# @return [Integer]
|
435
|
+
attr_accessor :received_message_count
|
436
|
+
|
437
|
+
# @return [Time,nil]
|
438
|
+
attr_accessor :last_message_received_at
|
439
|
+
|
440
|
+
# @return [Time]
|
441
|
+
attr_accessor :polling_started_at
|
442
|
+
|
443
|
+
# @return [Time,nil]
|
444
|
+
attr_accessor :polling_stopped_at
|
445
|
+
|
446
|
+
end
|
447
|
+
|
448
|
+
# A read-only set of configuration used by the QueuePoller.
|
449
|
+
class PollerConfig
|
450
|
+
|
451
|
+
# @api private
|
452
|
+
CONFIG_OPTIONS = Set.new([
|
453
|
+
:idle_timeout,
|
454
|
+
:skip_delete,
|
455
|
+
:before_request,
|
456
|
+
])
|
457
|
+
|
458
|
+
# @api private
|
459
|
+
PARAM_OPTIONS = Set.new([
|
460
|
+
:wait_time_seconds,
|
461
|
+
:max_number_of_messages,
|
462
|
+
:visibility_timeout,
|
463
|
+
:attribute_names,
|
464
|
+
:message_attribute_names,
|
465
|
+
])
|
466
|
+
|
467
|
+
# @return [Integer,nil]
|
468
|
+
attr_reader :idle_timeout
|
469
|
+
|
470
|
+
# @return [Boolean]
|
471
|
+
attr_reader :skip_delete
|
472
|
+
|
473
|
+
# @return [Proc,nil]
|
474
|
+
attr_reader :before_request
|
475
|
+
|
476
|
+
# @return [Hash]
|
477
|
+
attr_reader :request_params
|
478
|
+
|
479
|
+
def initialize(options)
|
480
|
+
@idle_timeout = nil
|
481
|
+
@skip_delete = false
|
482
|
+
@before_request = nil
|
483
|
+
@request_params = {
|
484
|
+
wait_time_seconds: 20,
|
485
|
+
max_number_of_messages: 1,
|
486
|
+
visibility_timeout: nil,
|
487
|
+
attribute_names: ['All'],
|
488
|
+
message_attribute_names: ['All'],
|
489
|
+
}
|
490
|
+
options.each do |opt_name, value|
|
491
|
+
if CONFIG_OPTIONS.include?(opt_name)
|
492
|
+
instance_variable_set("@#{opt_name}", value)
|
493
|
+
elsif PARAM_OPTIONS.include?(opt_name)
|
494
|
+
@request_params[opt_name] = value
|
495
|
+
else
|
496
|
+
raise ArgumentError, "invalid option #{opt_name.inspect}"
|
497
|
+
end
|
498
|
+
end
|
499
|
+
@request_params.freeze
|
500
|
+
freeze
|
501
|
+
end
|
502
|
+
|
503
|
+
# @return [PollerConfig] Returns a new {PollerConfig} instance
|
504
|
+
# with the given options applied.
|
505
|
+
def with(options)
|
506
|
+
self.class.new(to_h.merge(options))
|
507
|
+
end
|
508
|
+
|
509
|
+
private
|
510
|
+
|
511
|
+
def to_h
|
512
|
+
hash = {}
|
513
|
+
CONFIG_OPTIONS.each { |key| hash[key] = send(key) }
|
514
|
+
PARAM_OPTIONS.each { |key| hash[key] = @request_params[key] }
|
515
|
+
hash
|
516
|
+
end
|
517
|
+
|
518
|
+
end
|
519
|
+
end
|
520
|
+
end
|
521
|
+
end
|