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