aws-activejob-sqs 0.1.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/VERSION +1 -1
- data/bin/aws_active_job_sqs +17 -0
- data/lib/active_job/queue_adapters/sqs_adapter/params.rb +5 -5
- data/lib/active_job/queue_adapters/sqs_adapter.rb +29 -18
- data/lib/active_job/queue_adapters/sqs_async_adapter.rb +2 -3
- data/lib/aws/active_job/sqs/cli_options.rb +112 -0
- data/lib/aws/active_job/sqs/configuration.rb +171 -48
- data/lib/aws/active_job/sqs/deduplication.rb +11 -8
- data/lib/aws/active_job/sqs/executor.rb +65 -29
- data/lib/aws/active_job/sqs/job_runner.rb +1 -1
- data/lib/aws/active_job/sqs/lambda_handler.rb +51 -46
- data/lib/aws/active_job/sqs/poller.rb +99 -106
- data/lib/aws-activejob-sqs.rb +3 -1
- metadata +5 -4
- data/bin/aws_sqs_active_job +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d11b67b0f50d2a026a3c64f8289d98c8b8a6cd37e5a670346807a29061f183a0
|
4
|
+
data.tar.gz: 503d0cbb51a915877b89dcbcf23a97a62fbf50599daddb56120e010dc4e8b612
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 69a4e6b7028efff40930d0f2d2ee76131878399b92d9ea1543a7461c624ef7b2d83d43a8243877a41321af428e3e65ec9e448cfeea0f378903609704110f9f59
|
7
|
+
data.tar.gz: 3375ea92eb2f0923eca14e90a326210c027bde82879933fa6fd4bbd54270aeb1519188fcf85a11d28b41731b7090f79d82796b543dfe6b208ecbb8dd4956d492
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
1.0.0 (2024-12-13)
|
2
|
+
------------------
|
3
|
+
|
4
|
+
* Feature - Support polling on multiple queues. (#4)
|
5
|
+
* Feature - Support running without Rails. (#5)
|
6
|
+
* Feature - Replace `retry_standard_errors` with `poller_error_handler`. (#6)
|
7
|
+
* Feature - Support per queue configuration. (#4)
|
8
|
+
* Feature - Support loading global and queue specific configuration from ENV. (#3)
|
9
|
+
|
1
10
|
0.1.1 (2024-12-02)
|
2
11
|
------------------
|
3
12
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
1.0.0
|
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative '../lib/aws-activejob-sqs'
|
5
|
+
require_relative '../lib/aws/active_job/sqs/cli_options'
|
6
|
+
require_relative '../lib/aws/active_job/sqs/poller'
|
7
|
+
|
8
|
+
opts = Aws::ActiveJob::SQS::CliOptions.parse(ARGV)
|
9
|
+
|
10
|
+
if opts[:boot_rails]
|
11
|
+
require 'rails'
|
12
|
+
require File.expand_path('config/environment.rb')
|
13
|
+
end
|
14
|
+
|
15
|
+
require File.join(Dir.pwd, opts[:require]) if opts[:require]
|
16
|
+
|
17
|
+
Aws::ActiveJob::SQS::Poller.new(opts.to_h.compact).run
|
@@ -22,7 +22,7 @@ module ActiveJob
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def queue_url
|
25
|
-
@queue_url ||= Aws::ActiveJob::SQS.config.
|
25
|
+
@queue_url ||= Aws::ActiveJob::SQS.config.url_for(@job.queue_name)
|
26
26
|
end
|
27
27
|
|
28
28
|
def entry
|
@@ -37,7 +37,7 @@ module ActiveJob
|
|
37
37
|
|
38
38
|
def default_entry
|
39
39
|
{
|
40
|
-
message_body:
|
40
|
+
message_body: ActiveSupport::JSON.dump(@body),
|
41
41
|
message_attributes: message_attributes
|
42
42
|
}
|
43
43
|
end
|
@@ -58,10 +58,10 @@ module ActiveJob
|
|
58
58
|
def options_for_fifo
|
59
59
|
options = {}
|
60
60
|
options[:message_deduplication_id] =
|
61
|
-
Digest::SHA256.hexdigest(
|
61
|
+
Digest::SHA256.hexdigest(ActiveSupport::JSON.dump(deduplication_body))
|
62
62
|
|
63
63
|
message_group_id = @job.message_group_id if @job.respond_to?(:message_group_id)
|
64
|
-
message_group_id ||= Aws::ActiveJob::SQS.config.
|
64
|
+
message_group_id ||= Aws::ActiveJob::SQS.config.message_group_id_for(@job.queue_name)
|
65
65
|
|
66
66
|
options[:message_group_id] = message_group_id
|
67
67
|
options
|
@@ -69,7 +69,7 @@ module ActiveJob
|
|
69
69
|
|
70
70
|
def deduplication_body
|
71
71
|
ex_dedup_keys = @job.excluded_deduplication_keys if @job.respond_to?(:excluded_deduplication_keys)
|
72
|
-
ex_dedup_keys ||= Aws::ActiveJob::SQS.config.
|
72
|
+
ex_dedup_keys ||= Aws::ActiveJob::SQS.config.excluded_deduplication_keys_for(@job.queue_name)
|
73
73
|
|
74
74
|
@body.except(*ex_dedup_keys)
|
75
75
|
end
|
@@ -8,7 +8,7 @@ module ActiveJob
|
|
8
8
|
#
|
9
9
|
# To use this adapter, set up as:
|
10
10
|
#
|
11
|
-
# config.active_job.queue_adapter = :
|
11
|
+
# config.active_job.queue_adapter = :sqs
|
12
12
|
class SqsAdapter
|
13
13
|
def enqueue_after_transaction_commit?
|
14
14
|
# can be removed after Rails 8
|
@@ -27,29 +27,40 @@ module ActiveJob
|
|
27
27
|
def enqueue_all(jobs)
|
28
28
|
enqueued_count = 0
|
29
29
|
jobs.group_by(&:queue_name).each do |queue_name, same_queue_jobs|
|
30
|
-
|
31
|
-
base_send_message_opts = { queue_url: queue_url }
|
32
|
-
|
33
|
-
same_queue_jobs.each_slice(10) do |chunk|
|
34
|
-
entries = chunk.map do |job|
|
35
|
-
entry = Params.new(job, nil).entry
|
36
|
-
entry[:id] = job.job_id
|
37
|
-
entry[:delay_seconds] = Params.assured_delay_seconds(job.scheduled_at) if job.scheduled_at
|
38
|
-
entry
|
39
|
-
end
|
40
|
-
|
41
|
-
send_message_opts = base_send_message_opts.deep_dup
|
42
|
-
send_message_opts[:entries] = entries
|
43
|
-
|
44
|
-
send_message_batch_result = Aws::ActiveJob::SQS.config.client.send_message_batch(send_message_opts)
|
45
|
-
enqueued_count += send_message_batch_result.successful.count
|
46
|
-
end
|
30
|
+
enqueued_count += enqueue_batches(queue_name, same_queue_jobs)
|
47
31
|
end
|
48
32
|
enqueued_count
|
49
33
|
end
|
50
34
|
|
51
35
|
private
|
52
36
|
|
37
|
+
def enqueue_batches(queue_name, same_queue_jobs)
|
38
|
+
enqueued_count = 0
|
39
|
+
queue_url = Aws::ActiveJob::SQS.config.url_for(queue_name)
|
40
|
+
|
41
|
+
same_queue_jobs.each_slice(10) do |chunk|
|
42
|
+
enqueued_count += enqueue_batch(queue_url, chunk)
|
43
|
+
end
|
44
|
+
enqueued_count
|
45
|
+
end
|
46
|
+
|
47
|
+
def enqueue_batch(queue_url, chunk)
|
48
|
+
entries = chunk.map do |job|
|
49
|
+
entry = Params.new(job, nil).entry
|
50
|
+
entry[:id] = job.job_id
|
51
|
+
entry[:delay_seconds] = Params.assured_delay_seconds(job.scheduled_at) if job.scheduled_at
|
52
|
+
entry
|
53
|
+
end
|
54
|
+
|
55
|
+
send_message_opts = {
|
56
|
+
queue_url: queue_url,
|
57
|
+
entries: entries
|
58
|
+
}
|
59
|
+
|
60
|
+
send_message_batch_result = Aws::ActiveJob::SQS.config.client.send_message_batch(send_message_opts)
|
61
|
+
send_message_batch_result.successful.count
|
62
|
+
end
|
63
|
+
|
53
64
|
def _enqueue(job, body = nil, send_message_opts = {})
|
54
65
|
body ||= job.serialize
|
55
66
|
params = Params.new(job, body)
|
@@ -19,7 +19,7 @@ module ActiveJob
|
|
19
19
|
|
20
20
|
def _enqueue(job, body = nil, send_message_opts = {})
|
21
21
|
# FIFO jobs must be queued in order, so do not queue async
|
22
|
-
queue_url = Aws::ActiveJob::SQS.config.
|
22
|
+
queue_url = Aws::ActiveJob::SQS.config.url_for(job.queue_name)
|
23
23
|
if Aws::ActiveJob::SQS.fifo?(queue_url)
|
24
24
|
super
|
25
25
|
else
|
@@ -29,8 +29,7 @@ module ActiveJob
|
|
29
29
|
Concurrent::Promises
|
30
30
|
.future { super(job, body, send_message_opts) }
|
31
31
|
.rescue do |e|
|
32
|
-
|
33
|
-
Rails.logger.error "Failed to queue job #{job}. Reason: #{e}"
|
32
|
+
Aws::ActiveJob::SQS.config.logger.error "Failed to queue job #{job}. Reason: #{e}"
|
34
33
|
error_handler = Aws::ActiveJob::SQS.config.async_queue_error_handler
|
35
34
|
error_handler&.call(e, job, send_message_opts)
|
36
35
|
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
module Aws
|
6
|
+
module ActiveJob
|
7
|
+
module SQS
|
8
|
+
# options for the aws_active_job_sqs CLI.
|
9
|
+
# @api private
|
10
|
+
module CliOptions
|
11
|
+
def self.option_parser(out)
|
12
|
+
::OptionParser.new do |opts|
|
13
|
+
queues_option(opts, out)
|
14
|
+
threads_option(opts, out)
|
15
|
+
backpressure_option(opts, out)
|
16
|
+
max_messages_option(opts, out)
|
17
|
+
visibility_timeout_option(opts, out)
|
18
|
+
shutdown_timeout_option(opts, out)
|
19
|
+
boot_rails_option(opts, out)
|
20
|
+
require_option(opts, out)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
private_class_method :option_parser
|
24
|
+
|
25
|
+
def self.parse(argv)
|
26
|
+
out = { boot_rails: true, queues: [] }
|
27
|
+
parser = option_parser(out)
|
28
|
+
|
29
|
+
parser.banner = 'aws_active_job_sqs [options]'
|
30
|
+
parser.on_tail '-h', '--help', 'Show help' do
|
31
|
+
puts parser
|
32
|
+
exit 1
|
33
|
+
end
|
34
|
+
|
35
|
+
parser.parse(argv)
|
36
|
+
out
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.require_option(opts, out)
|
40
|
+
doc = 'Additional file to require before starting the poller. ' \
|
41
|
+
'Can be used to define/load job classes with --no-rails.'
|
42
|
+
opts.on('-r', '--require STRING', String, doc) do |a|
|
43
|
+
out[:require] = a
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.boot_rails_option(opts, out)
|
48
|
+
doc = 'When set boots rails before running the poller.'
|
49
|
+
opts.on('--[no-]rails [FLAG]', TrueClass, doc) do |a|
|
50
|
+
out[:boot_rails] = a.nil? ? true : a
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.shutdown_timeout_option(opts, out)
|
55
|
+
doc = 'The amount of time to wait for a clean shutdown. Jobs that ' \
|
56
|
+
'are unable to complete in this time will not be deleted from ' \
|
57
|
+
'the SQS queue and will be retryable after the visibility timeout.'
|
58
|
+
opts.on('-s', '--shutdown_timeout INTEGER', Integer, doc) do |a|
|
59
|
+
out[:shutdown_timeout] = a
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.visibility_timeout_option(opts, out)
|
64
|
+
doc = 'The visibility timeout is the number of seconds that a ' \
|
65
|
+
'message will not be processable by any other consumers. ' \
|
66
|
+
'You should set this value to be longer than your expected ' \
|
67
|
+
'job runtime to prevent other processes from picking up an ' \
|
68
|
+
'running job. See the SQS Visibility Timeout Documentation ' \
|
69
|
+
'at https://docs.aws.amazon.com/AWSSimpleQueueService/latest/' \
|
70
|
+
'SQSDeveloperGuide/sqs-visibility-timeout.html.'
|
71
|
+
opts.on('-v', '--visibility_timeout INTEGER', Integer, doc) do |a|
|
72
|
+
out[:visibility_timeout] = a
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.max_messages_option(opts, out)
|
77
|
+
doc = 'Max number of messages to receive in a batch from SQS.'
|
78
|
+
opts.on('-m', '--max_messages INTEGER', Integer, doc) do |a|
|
79
|
+
out[:max_messages] = a
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.backpressure_option(opts, out)
|
84
|
+
doc = 'The maximum number of messages to have waiting in ' \
|
85
|
+
'the Executor queue. This should be a low, but non zero number. ' \
|
86
|
+
'Messages in the Executor queue cannot be picked up by other ' \
|
87
|
+
'processes and will slow down shutdown.'
|
88
|
+
opts.on('-b', '--backpressure INTEGER', Integer, doc) do |a|
|
89
|
+
out[:backpressure] = a
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.threads_option(opts, out)
|
94
|
+
doc = 'The maximum number of worker threads to create. ' \
|
95
|
+
'Defaults to 2x the number of processors available on this system.'
|
96
|
+
opts.on('-t', '--threads INTEGER', Integer, doc) do |a|
|
97
|
+
out[:threads] = a
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.queues_option(opts, out)
|
102
|
+
doc = 'Queue(s) to poll. You may specify this argument multiple ' \
|
103
|
+
'times to poll multiple queues. If not specified, will ' \
|
104
|
+
'start pollers for all queues defined.'
|
105
|
+
opts.on('-q', '--queue STRING', doc) do |a|
|
106
|
+
out[:queues] << a.to_sym
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -3,34 +3,98 @@
|
|
3
3
|
module Aws
|
4
4
|
module ActiveJob
|
5
5
|
module SQS
|
6
|
-
#
|
6
|
+
# This class provides a Configuration object for AWS ActiveJob
|
7
|
+
# by pulling configuration options from runtime code, the ENV, a YAML file,
|
8
|
+
# and default settings, in that order. Values set on queues are used
|
9
|
+
# preferentially to global values.
|
10
|
+
#
|
11
|
+
# Use {Aws::ActiveJob::SQS.config Aws::ActiveJob::SQS.config}
|
12
|
+
# to access the singleton config instance and use
|
13
|
+
# {Aws::ActiveJob::SQS.configure Aws::ActiveJob::SQS.configure} to
|
14
|
+
# configure in code:
|
15
|
+
#
|
16
|
+
# Aws::ActiveJob::SQS.configure do |config|
|
17
|
+
# config.logger = Rails.logger
|
18
|
+
# config.max_messages = 5
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# # Configuation YAML File
|
22
|
+
# By default, this class will load configuration from the
|
23
|
+
# `config/aws_active_job_sqs/<RAILS_ENV}.yml` or
|
24
|
+
# `config/aws_active_job_sqs.yml` files. You may specify the file used
|
25
|
+
# through the `:config_file` option in code or the
|
26
|
+
# `AWS_ACTIVE_JOB_SQS_CONFIG_FILE` environment variable.
|
27
|
+
#
|
28
|
+
# # Global and queue specific options
|
29
|
+
# Values configured for specific queues are used preferentially to
|
30
|
+
# global values. See: {QUEUE_CONFIGS} for supported queue specific
|
31
|
+
# options.
|
32
|
+
#
|
33
|
+
# # Environment Variables
|
34
|
+
# The Configuration loads global and qubeue specific values from your
|
35
|
+
# environment. Global keys take the form of:
|
36
|
+
# `AWS_ACTIVE_JOB_SQS_<KEY_NAME>` and queue specific keys take the
|
37
|
+
# form of: `AWS_ACTIVE_JOB_SQS_<QUEUE_NAME>_<KEY_NAME>`.
|
38
|
+
# <QUEUE_NAME> is case-insensitive and is always down cased. Configuring
|
39
|
+
# non-snake case queues (containing upper case) through ENV is
|
40
|
+
# not supported.
|
41
|
+
#
|
42
|
+
# Example:
|
43
|
+
#
|
44
|
+
# export AWS_ACTIVE_JOB_SQS_MAX_MESSAGES = 5
|
45
|
+
# export AWS_ACTIVE_JOB_SQS_DEFAULT_URL = https://my-queue.aws
|
46
|
+
#
|
47
|
+
# For supported global ENV configurations see
|
48
|
+
# {GLOBAL_ENV_CONFIGS}. For supported queue specific ENV configurations
|
49
|
+
# see: {QUEUE_ENV_CONFIGS}.
|
50
|
+
#
|
7
51
|
class Configuration
|
8
52
|
# Default configuration options
|
9
53
|
# @api private
|
10
54
|
DEFAULTS = {
|
55
|
+
threads: 2 * Concurrent.processor_count,
|
56
|
+
backpressure: 10,
|
11
57
|
max_messages: 10,
|
12
58
|
shutdown_timeout: 15,
|
13
|
-
|
14
|
-
|
15
|
-
logger: ::Rails.logger,
|
16
|
-
message_group_id: 'SqsActiveJobGroup',
|
59
|
+
queues: Hash.new { |h, k| h[k] = {} },
|
60
|
+
message_group_id: 'ActiveJobSqsGroup',
|
17
61
|
excluded_deduplication_keys: ['job_id']
|
18
62
|
}.freeze
|
19
63
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
64
|
+
GLOBAL_ENV_CONFIGS = %i[
|
65
|
+
config_file
|
66
|
+
threads
|
67
|
+
backpressure
|
68
|
+
max_messages
|
69
|
+
shutdown_timeout
|
70
|
+
visibility_timeout
|
71
|
+
message_group_id
|
72
|
+
].freeze
|
73
|
+
|
74
|
+
QUEUE_ENV_CONFIGS = %i[
|
75
|
+
url
|
76
|
+
max_messages
|
77
|
+
visibility_timeout
|
78
|
+
message_group_id
|
79
|
+
].freeze
|
80
|
+
|
81
|
+
QUEUE_CONFIGS = QUEUE_ENV_CONFIGS + %i[excluded_deduplication_keys]
|
24
82
|
|
25
|
-
|
83
|
+
QUEUE_KEY_REGEX =
|
84
|
+
/AWS_ACTIVE_JOB_SQS_([\w]+)_(#{QUEUE_ENV_CONFIGS.map(&:upcase).join('|')})/.freeze
|
26
85
|
|
27
|
-
# Don't use this method directly: Configuration is a singleton class,
|
28
|
-
#
|
86
|
+
# Don't use this method directly: Configuration is a singleton class,
|
87
|
+
# use {Aws::ActiveJob::SQS.config Aws::ActiveJob::SQS.config}
|
88
|
+
# to access the singleton config instance and use
|
89
|
+
# {Aws::ActiveJob::SQS.configure Aws::ActiveJob::SQS.configure} to
|
90
|
+
# configure in code:
|
29
91
|
#
|
30
92
|
# @param [Hash] options
|
31
|
-
# @option options [Hash
|
32
|
-
# active job queue name and the
|
33
|
-
#
|
93
|
+
# @option options [Hash<Symbol, Hash>] :queues A mapping between the
|
94
|
+
# active job queue name and the queue properties. Values
|
95
|
+
# configured on the queue are used preferentially to the global
|
96
|
+
# values. See: {QUEUE_CONFIGS} for supported queue specific options.
|
97
|
+
# Note: multiple active job queues can map to the same SQS Queue URL.
|
34
98
|
#
|
35
99
|
# @option options [Integer] :max_messages
|
36
100
|
# The max number of messages to poll for in a batch.
|
@@ -50,22 +114,22 @@ module Aws
|
|
50
114
|
# will not be deleted from the SQS queue and will be retryable after
|
51
115
|
# the visibility timeout.
|
52
116
|
#
|
53
|
-
# @
|
54
|
-
#
|
55
|
-
#
|
56
|
-
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
#
|
60
|
-
#
|
61
|
-
#
|
117
|
+
# @option options [Callable] :poller_error_handler and error handler to
|
118
|
+
# be called when the poller encounters an error running a job. Called
|
119
|
+
# with exception, sqs_message. You may re-raise the exception to
|
120
|
+
# terminate the poller. You may also choose whether to delete the
|
121
|
+
# sqs_message or not. If the message is not explicitly deleted
|
122
|
+
# then the message will be left on the queue and will be
|
123
|
+
# retried (pending the SQS Queue's redrive/DLQ/maximum
|
124
|
+
# receive settings). Retries provided by this mechanism are
|
125
|
+
# after any retries configured on the job with `retry_on`.
|
62
126
|
#
|
63
127
|
# @option options [ActiveSupport::Logger] :logger Logger to use
|
64
128
|
# for the poller.
|
65
129
|
#
|
66
130
|
# @option options [String] :config_file
|
67
131
|
# Override file to load configuration from. If not specified will
|
68
|
-
# attempt to load from config/
|
132
|
+
# attempt to load from config/aws_active_job_sqs.yml.
|
69
133
|
#
|
70
134
|
# @option options [String] :message_group_id (SqsActiveJobGroup)
|
71
135
|
# The message_group_id to use for queueing messages on a fifo queues.
|
@@ -73,7 +137,7 @@ module Aws
|
|
73
137
|
# See the (SQS FIFO Documentation)[https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues.html]
|
74
138
|
#
|
75
139
|
# @option options [Callable] :async_queue_error_handler An error handler
|
76
|
-
# to be called when the async active job adapter
|
140
|
+
# to be called when the async active job adapter experiences an error
|
77
141
|
# queueing a job. Only applies when
|
78
142
|
# +active_job.queue_adapter = :sqs_async+. Called with:
|
79
143
|
# [error, job, job_options]
|
@@ -86,17 +150,31 @@ module Aws
|
|
86
150
|
# Using this option, job_id is implicitly added to the keys.
|
87
151
|
|
88
152
|
def initialize(options = {})
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
153
|
+
opts = env_options.deep_merge(options)
|
154
|
+
opts = file_options(opts).deep_merge(opts)
|
155
|
+
opts = DEFAULTS.merge(logger: default_logger).deep_merge(opts)
|
156
|
+
|
157
|
+
apply_attributes(opts)
|
94
158
|
end
|
95
159
|
|
160
|
+
# @api private
|
161
|
+
attr_accessor :queues, :threads, :backpressure,
|
162
|
+
:shutdown_timeout, :logger,
|
163
|
+
:async_queue_error_handler
|
164
|
+
|
165
|
+
# @api private
|
166
|
+
attr_writer :max_messages, :message_group_id, :visibility_timeout,
|
167
|
+
:poller_error_handler, :client
|
168
|
+
|
96
169
|
def excluded_deduplication_keys=(keys)
|
97
170
|
@excluded_deduplication_keys = keys.map(&:to_s) | ['job_id']
|
98
171
|
end
|
99
172
|
|
173
|
+
def poller_error_handler(&block)
|
174
|
+
@poller_error_handler = block if block_given?
|
175
|
+
@poller_error_handler
|
176
|
+
end
|
177
|
+
|
100
178
|
def client
|
101
179
|
@client ||= begin
|
102
180
|
client = Aws::SQS::Client.new
|
@@ -105,12 +183,10 @@ module Aws
|
|
105
183
|
end
|
106
184
|
end
|
107
185
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
queues[job_queue]
|
186
|
+
QUEUE_CONFIGS.each do |key|
|
187
|
+
define_method(:"#{key}_for") do |job_queue|
|
188
|
+
queue_attribute_for(key, job_queue)
|
189
|
+
end
|
114
190
|
end
|
115
191
|
|
116
192
|
# @api private
|
@@ -131,26 +207,68 @@ module Aws
|
|
131
207
|
|
132
208
|
private
|
133
209
|
|
210
|
+
def queue_attribute_for(attribute, job_queue)
|
211
|
+
job_queue = job_queue.to_sym
|
212
|
+
raise ArgumentError, "No queue defined for #{job_queue}" unless queues.key? job_queue
|
213
|
+
|
214
|
+
queues[job_queue][attribute] || instance_variable_get("@#{attribute}")
|
215
|
+
end
|
216
|
+
|
134
217
|
# Set accessible attributes after merged options.
|
135
|
-
def
|
218
|
+
def apply_attributes(options)
|
136
219
|
options.each_key do |opt_name|
|
137
220
|
instance_variable_set("@#{opt_name}", options[opt_name])
|
138
221
|
client.config.user_agent_frameworks << 'aws-activejob-sqs' if opt_name == :client
|
139
222
|
end
|
140
223
|
end
|
141
224
|
|
225
|
+
# resolve ENV for global and queue specific options
|
226
|
+
def env_options
|
227
|
+
resolved = { queues: {} }
|
228
|
+
resolve_global_env_options(resolved)
|
229
|
+
resolve_queue_env__options(resolved)
|
230
|
+
resolved
|
231
|
+
end
|
232
|
+
|
233
|
+
def resolve_queue_env__options(resolved)
|
234
|
+
ENV.each_key do |key|
|
235
|
+
next unless (match = QUEUE_KEY_REGEX.match(key))
|
236
|
+
|
237
|
+
queue_name = match[1].downcase.to_sym
|
238
|
+
resolved[:queues][queue_name] ||= {}
|
239
|
+
resolved[:queues][queue_name][match[2].downcase.to_sym] =
|
240
|
+
parse_env_value(key)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def resolve_global_env_options(resolved)
|
245
|
+
GLOBAL_ENV_CONFIGS.each do |cfg|
|
246
|
+
env_name = "AWS_ACTIVE_JOB_SQS_#{cfg.to_s.upcase}"
|
247
|
+
resolved[cfg] = parse_env_value(env_name) if ENV.key? env_name
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def parse_env_value(key)
|
252
|
+
val = ENV.fetch(key, nil)
|
253
|
+
Integer(val)
|
254
|
+
rescue ArgumentError, TypeError
|
255
|
+
%w[true false].include?(val) ? val == 'true' : val
|
256
|
+
end
|
257
|
+
|
142
258
|
def file_options(options = {})
|
143
|
-
file_path =
|
259
|
+
file_path = options[:config_file] || default_config_file
|
144
260
|
if file_path
|
145
261
|
load_from_file(file_path)
|
146
262
|
else
|
147
|
-
|
263
|
+
options
|
148
264
|
end
|
149
265
|
end
|
150
266
|
|
151
|
-
def
|
152
|
-
|
153
|
-
|
267
|
+
def default_config_file
|
268
|
+
return unless defined?(::Rails)
|
269
|
+
|
270
|
+
file = ::Rails.root.join("config/aws_active_job_sqs/#{::Rails.env}.yml")
|
271
|
+
file = ::Rails.root.join('config/aws_active_job_sqs.yml') unless File.exist?(file)
|
154
272
|
file
|
155
273
|
end
|
156
274
|
|
@@ -160,12 +278,9 @@ module Aws
|
|
160
278
|
opts.deep_symbolize_keys
|
161
279
|
end
|
162
280
|
|
163
|
-
# @return [String] Configuration path found in environment or YAML file.
|
164
|
-
def config_file_path(options)
|
165
|
-
options[:config_file] || ENV.fetch('AWS_SQS_ACTIVE_JOB_CONFIG_FILE', nil)
|
166
|
-
end
|
167
|
-
|
168
281
|
def load_yaml(file_path)
|
282
|
+
return {} unless File.exist?(file_path)
|
283
|
+
|
169
284
|
require 'erb'
|
170
285
|
source = ERB.new(File.read(file_path)).result
|
171
286
|
|
@@ -177,6 +292,14 @@ module Aws
|
|
177
292
|
YAML.safe_load(source) || {}
|
178
293
|
end
|
179
294
|
end
|
295
|
+
|
296
|
+
def default_logger
|
297
|
+
if defined?(::Rails)
|
298
|
+
::Rails.logger
|
299
|
+
else
|
300
|
+
ActiveSupport::Logger.new($stdout)
|
301
|
+
end
|
302
|
+
end
|
180
303
|
end
|
181
304
|
end
|
182
305
|
end
|
@@ -3,16 +3,19 @@
|
|
3
3
|
module Aws
|
4
4
|
module ActiveJob
|
5
5
|
module SQS
|
6
|
-
|
6
|
+
# Mixin module to configure job level deduplication keys
|
7
|
+
module Deduplication
|
8
|
+
extend ActiveSupport::Concern
|
7
9
|
|
8
|
-
|
9
|
-
|
10
|
-
|
10
|
+
included do
|
11
|
+
class_attribute :excluded_deduplication_keys
|
12
|
+
end
|
11
13
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
14
|
+
# class methods for SQS ActiveJob.
|
15
|
+
module ClassMethods
|
16
|
+
def deduplicate_without(*keys)
|
17
|
+
self.excluded_deduplication_keys = keys.map(&:to_s) | ['job_id']
|
18
|
+
end
|
16
19
|
end
|
17
20
|
end
|
18
21
|
end
|
@@ -31,21 +31,27 @@ module Aws
|
|
31
31
|
|
32
32
|
def initialize(options = {})
|
33
33
|
@executor = Concurrent::ThreadPoolExecutor.new(DEFAULTS.merge(options))
|
34
|
-
@retry_standard_errors = options[:retry_standard_errors]
|
35
34
|
@logger = options[:logger] || ActiveSupport::Logger.new($stdout)
|
36
35
|
@task_complete = Concurrent::Event.new
|
36
|
+
@post_mutex = Mutex.new
|
37
|
+
|
38
|
+
@error_handler = options[:error_handler]
|
39
|
+
@error_queue = Thread::Queue.new
|
40
|
+
@error_handler_thread = Thread.new(&method(:handle_errors))
|
41
|
+
@error_handler_thread.abort_on_exception = true
|
42
|
+
@error_handler_thread.report_on_exception = false
|
43
|
+
@shutting_down = Concurrent::AtomicBoolean.new(false)
|
37
44
|
end
|
38
45
|
|
39
46
|
def execute(message)
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
@task_complete.reset
|
44
|
-
@task_complete.wait
|
45
|
-
retry
|
47
|
+
@post_mutex.synchronize do
|
48
|
+
_execute(message)
|
49
|
+
end
|
46
50
|
end
|
47
51
|
|
48
52
|
def shutdown(timeout = nil)
|
53
|
+
@shutting_down.make_true
|
54
|
+
|
49
55
|
run_hooks_for(:stop)
|
50
56
|
@executor.shutdown
|
51
57
|
clean_shutdown = @executor.wait_for_termination(timeout)
|
@@ -57,41 +63,71 @@ module Aws
|
|
57
63
|
'the queue and can be ru-run once their visibility timeout ' \
|
58
64
|
'passes.'
|
59
65
|
end
|
66
|
+
@error_queue.push(nil) # process any remaining errors and then terminate
|
67
|
+
@error_handler_thread.join unless @error_handler_thread == Thread.current
|
68
|
+
@shutting_down.make_false
|
60
69
|
end
|
61
70
|
|
62
71
|
private
|
63
72
|
|
73
|
+
def _execute(message)
|
74
|
+
post_task(message)
|
75
|
+
rescue Concurrent::RejectedExecutionError
|
76
|
+
# no capacity, wait for a task to complete
|
77
|
+
@task_complete.reset
|
78
|
+
@task_complete.wait
|
79
|
+
retry
|
80
|
+
end
|
81
|
+
|
64
82
|
def post_task(message)
|
65
|
-
@executor.post(message) do |
|
66
|
-
|
67
|
-
@logger.info("Running job: #{job.id}[#{job.class_name}]")
|
68
|
-
job.run
|
69
|
-
message.delete
|
70
|
-
rescue Aws::Json::ParseError => e
|
71
|
-
@logger.error "Unable to parse message body: #{message.data.body}. Error: #{e}."
|
72
|
-
rescue StandardError => e
|
73
|
-
job_msg = job ? "#{job.id}[#{job.class_name}]" : 'unknown job'
|
74
|
-
@logger.info "Error processing job #{job_msg}: #{e}"
|
75
|
-
@logger.debug e.backtrace.join("\n")
|
76
|
-
|
77
|
-
if @retry_standard_errors && !job.exception_executions?
|
78
|
-
@logger.info(
|
79
|
-
'retry_standard_errors is enabled and job has not ' \
|
80
|
-
"been retried by Rails. Leaving #{job_msg} in the queue."
|
81
|
-
)
|
82
|
-
else
|
83
|
-
message.delete
|
84
|
-
end
|
85
|
-
ensure
|
86
|
-
@task_complete.set
|
83
|
+
@executor.post(message) do |msg|
|
84
|
+
execute_task(msg)
|
87
85
|
end
|
88
86
|
end
|
89
87
|
|
88
|
+
def execute_task(message)
|
89
|
+
job = JobRunner.new(message)
|
90
|
+
@logger.info("Running job: #{job.id}[#{job.class_name}]")
|
91
|
+
job.run
|
92
|
+
message.delete
|
93
|
+
rescue JSON::ParserError => e
|
94
|
+
@logger.error "Unable to parse message body: #{message.data.body}. Error: #{e}."
|
95
|
+
rescue StandardError => e
|
96
|
+
handle_standard_error(e, job, message)
|
97
|
+
ensure
|
98
|
+
@task_complete.set
|
99
|
+
end
|
100
|
+
|
101
|
+
def handle_standard_error(error, job, message)
|
102
|
+
job_msg = job ? "#{job.id}[#{job.class_name}]" : 'unknown job'
|
103
|
+
@logger.info "Error processing job #{job_msg}: #{error}"
|
104
|
+
@logger.debug error.backtrace.join("\n")
|
105
|
+
|
106
|
+
@error_queue.push([error, message])
|
107
|
+
end
|
108
|
+
|
90
109
|
def run_hooks_for(event_name)
|
91
110
|
return unless (hooks = self.class.lifecycle_hooks[event_name])
|
92
111
|
|
93
112
|
hooks.each(&:call)
|
94
113
|
end
|
114
|
+
|
115
|
+
# run in the @error_handler_thread
|
116
|
+
def handle_errors
|
117
|
+
# wait until errors are placed in the error queue
|
118
|
+
while ((exception, message) = @error_queue.pop)
|
119
|
+
raise exception unless @error_handler
|
120
|
+
|
121
|
+
@error_handler.call(exception, message)
|
122
|
+
|
123
|
+
end
|
124
|
+
rescue StandardError => e
|
125
|
+
@logger.info("Unhandled exception executing jobs in poller: #{e}.")
|
126
|
+
@logger.info('Shutting down executor')
|
127
|
+
shutdown unless @shutting_down.true?
|
128
|
+
|
129
|
+
raise e # re-raise the error, terminating the application
|
130
|
+
end
|
95
131
|
end
|
96
132
|
end
|
97
133
|
end
|
@@ -8,7 +8,7 @@ module Aws
|
|
8
8
|
attr_reader :id, :class_name
|
9
9
|
|
10
10
|
def initialize(message)
|
11
|
-
@job_data =
|
11
|
+
@job_data = ActiveSupport::JSON.load(message.data.body)
|
12
12
|
@class_name = @job_data['job_class'].constantize
|
13
13
|
@id = @job_data['job_id']
|
14
14
|
end
|
@@ -5,60 +5,65 @@ require 'aws-sdk-sqs'
|
|
5
5
|
module Aws
|
6
6
|
module ActiveJob
|
7
7
|
module SQS
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
8
|
+
# Lambda event handler to run jobs from an SQS queue trigger
|
9
|
+
module LambdaHandler
|
10
|
+
class << self
|
11
|
+
# A lambda event handler to run jobs from an SQS queue trigger.
|
12
|
+
# Configure the entrypoint to: +config/environment.Aws::ActiveJob::SQS::LambdaHandler.job_handler+
|
13
|
+
# This will load your Rails environment, and then use this method as the handler.
|
14
|
+
def job_handler(event:, context:)
|
15
|
+
return 'no records to process' unless event['Records']
|
14
16
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
17
|
+
puts "job_handler running for #{event} with context: #{context}"
|
18
|
+
|
19
|
+
event['Records'].each do |record|
|
20
|
+
sqs_msg = to_sqs_msg(record)
|
21
|
+
job = Aws::ActiveJob::SQS::JobRunner.new(sqs_msg)
|
22
|
+
puts "Running job: #{job.id}[#{job.class_name}]"
|
23
|
+
job.run
|
24
|
+
sqs_msg.delete
|
25
|
+
end
|
26
|
+
"Processed #{event['Records'].length} jobs."
|
21
27
|
end
|
22
|
-
"Processed #{event['Records'].length} jobs."
|
23
|
-
end
|
24
28
|
|
25
|
-
|
29
|
+
private
|
26
30
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
31
|
+
def to_sqs_msg(record)
|
32
|
+
msg = Aws::SQS::Types::Message.new(
|
33
|
+
body: record['body'],
|
34
|
+
md5_of_body: record['md5OfBody'],
|
35
|
+
message_attributes: to_message_attributes(record),
|
36
|
+
message_id: record['messageId'],
|
37
|
+
receipt_handle: record['receiptHandle']
|
38
|
+
)
|
39
|
+
Aws::SQS::Message.new(
|
40
|
+
queue_url: to_queue_url(record),
|
41
|
+
receipt_handle: msg.receipt_handle,
|
42
|
+
data: msg,
|
43
|
+
client: Aws::ActiveJob::SQS.config.client
|
44
|
+
)
|
45
|
+
end
|
42
46
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
47
|
+
def to_message_attributes(record)
|
48
|
+
record['messageAttributes'].transform_values do |value|
|
49
|
+
{
|
50
|
+
string_value: value['stringValue'],
|
51
|
+
binary_value: value['binaryValue'],
|
52
|
+
string_list_values: ['stringListValues'],
|
53
|
+
binary_list_values: value['binaryListValues'],
|
54
|
+
data_type: value['dataType']
|
55
|
+
}
|
56
|
+
end
|
52
57
|
end
|
53
|
-
end
|
54
58
|
|
55
|
-
|
56
|
-
|
57
|
-
|
59
|
+
def to_queue_url(record)
|
60
|
+
source_arn = record['eventSourceARN']
|
61
|
+
raise ArgumentError, "Invalid queue arn: #{source_arn}" unless Aws::ARNParser.arn?(source_arn)
|
58
62
|
|
59
|
-
|
60
|
-
|
61
|
-
|
63
|
+
arn = Aws::ARNParser.parse(source_arn)
|
64
|
+
sfx = Aws::Partitions::EndpointProvider.dns_suffix_for(arn.region)
|
65
|
+
"https://sqs.#{arn.region}.#{sfx}/#{arn.account_id}/#{arn.resource}"
|
66
|
+
end
|
62
67
|
end
|
63
68
|
end
|
64
69
|
end
|
@@ -1,158 +1,151 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'aws-sdk-sqs'
|
4
|
-
require 'optparse'
|
5
4
|
require 'concurrent'
|
6
5
|
|
7
6
|
module Aws
|
8
7
|
module ActiveJob
|
9
8
|
module SQS
|
10
9
|
# CLI runner for polling for SQS ActiveJobs
|
11
|
-
# Use `
|
10
|
+
# Use `aws_active_job_sqs --help` for detailed usage
|
12
11
|
class Poller
|
13
12
|
class Interrupt < StandardError; end
|
14
13
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
shutdown_timeout: 15,
|
19
|
-
backpressure: 10,
|
20
|
-
retry_standard_errors: true
|
21
|
-
}.freeze
|
22
|
-
|
23
|
-
def initialize(args = ARGV)
|
24
|
-
@options = parse_args(args)
|
25
|
-
# Set_environment must be run before we boot_rails
|
26
|
-
set_environment
|
27
|
-
end
|
28
|
-
|
29
|
-
def set_environment
|
30
|
-
@environment = @options[:environment] || ENV['APP_ENV'] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
|
14
|
+
def initialize(options = {})
|
15
|
+
@queues = options.delete(:queues)
|
16
|
+
@options = options
|
31
17
|
end
|
32
18
|
|
33
19
|
def run
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
# cannot load config (from file or initializers) until after
|
38
|
-
# rails has been booted.
|
39
|
-
@options = DEFAULT_OPTS
|
40
|
-
.merge(Aws::ActiveJob::SQS.config.to_h)
|
41
|
-
.merge(@options.to_h)
|
42
|
-
validate_config
|
43
|
-
# ensure we have a logger configured
|
44
|
-
@logger = @options[:logger] || ActiveSupport::Logger.new($stdout)
|
45
|
-
@logger.info("Starting Poller with options=#{@options}")
|
20
|
+
init_config
|
21
|
+
|
22
|
+
config = Aws::ActiveJob::SQS.config
|
46
23
|
|
47
24
|
Signal.trap('INT') { raise Interrupt }
|
48
25
|
Signal.trap('TERM') { raise Interrupt }
|
49
26
|
@executor = Executor.new(
|
50
|
-
max_threads:
|
27
|
+
max_threads: config.threads,
|
51
28
|
logger: @logger,
|
52
|
-
max_queue:
|
53
|
-
|
29
|
+
max_queue: config.backpressure,
|
30
|
+
error_handler: config.poller_error_handler
|
54
31
|
)
|
55
32
|
|
56
33
|
poll
|
57
34
|
rescue Interrupt
|
58
35
|
@logger.info 'Process Interrupted or killed - attempting to shutdown cleanly.'
|
59
|
-
shutdown
|
36
|
+
shutdown(config.shutdown_timeout)
|
60
37
|
exit
|
61
38
|
end
|
62
39
|
|
63
40
|
private
|
64
41
|
|
65
|
-
def
|
66
|
-
|
42
|
+
def init_config
|
43
|
+
Aws::ActiveJob::SQS.configure do |cfg|
|
44
|
+
@options.each_pair do |key, value|
|
45
|
+
cfg.send(:"#{key}=", value) if cfg.respond_to?(:"#{key}=")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# ensure we have a logger configured
|
50
|
+
config = Aws::ActiveJob::SQS.config
|
51
|
+
@logger = config.logger || ActiveSupport::Logger.new($stdout)
|
52
|
+
@logger.info("Starting Poller with config=#{config.to_h}")
|
53
|
+
end
|
54
|
+
|
55
|
+
def shutdown(timeout)
|
56
|
+
@executor.shutdown(timeout)
|
67
57
|
end
|
68
58
|
|
69
59
|
def poll
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
60
|
+
config = Aws::ActiveJob::SQS.config
|
61
|
+
if @queues && !@queues.empty?
|
62
|
+
if @queues.size == 1
|
63
|
+
# single queue, use main thread
|
64
|
+
poll_foreground(@queues.first)
|
65
|
+
else
|
66
|
+
poll_background(@queues)
|
67
|
+
end
|
68
|
+
else
|
69
|
+
# poll on all configured queues
|
70
|
+
@logger.info("No queues specified - polling on all configured queues: #{config.queues.keys}")
|
71
|
+
poll_background(config.queues.keys)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def poll_foreground(queue)
|
76
|
+
config = Aws::ActiveJob::SQS.config
|
77
|
+
validate_config(queue)
|
78
|
+
queue_url = config.url_for(queue)
|
79
|
+
|
80
|
+
poller_options = poller_options(queue)
|
81
|
+
@logger.info "Foreground Polling on: #{queue} => #{queue_url} with options=#{poller_options}"
|
82
|
+
|
83
|
+
_poll(poller_options, queue_url)
|
84
|
+
end
|
85
|
+
|
86
|
+
def poll_background(queues)
|
87
|
+
config = Aws::ActiveJob::SQS.config
|
88
|
+
queues.each { |q| validate_config(q) }
|
89
|
+
poller_threads = queues.map do |queue|
|
90
|
+
Thread.new do
|
91
|
+
queue_url = config.url_for(queue)
|
92
|
+
|
93
|
+
poller_options = poller_options(queue)
|
94
|
+
@logger.info "Background Polling on: #{queue} => #{queue_url} with options=#{poller_options}"
|
95
|
+
|
96
|
+
_poll(poller_options, queue_url)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
poller_threads.each(&:join)
|
100
|
+
end
|
101
|
+
|
102
|
+
def validate_config(queue)
|
103
|
+
return if Aws::ActiveJob::SQS.config.queues[queue]&.fetch(:url, nil)
|
104
|
+
|
105
|
+
raise ArgumentError, "No URL configured for queue #{queue}"
|
106
|
+
end
|
107
|
+
|
108
|
+
def poller_options(queue)
|
109
|
+
config = Aws::ActiveJob::SQS.config
|
110
|
+
queue_url = config.url_for(queue)
|
74
111
|
poller_options = {
|
75
112
|
skip_delete: true,
|
76
|
-
max_number_of_messages:
|
77
|
-
visibility_timeout:
|
113
|
+
max_number_of_messages: config.max_messages_for(queue),
|
114
|
+
visibility_timeout: config.visibility_timeout_for(queue)
|
78
115
|
}
|
116
|
+
|
79
117
|
# Limit max_number_of_messages for FIFO queues to 1
|
80
118
|
# this ensures jobs with the same message_group_id are processed
|
81
119
|
# in order
|
82
120
|
# Jobs with different message_group_id will be processed in
|
83
121
|
# parallel and may be out of order.
|
84
122
|
poller_options[:max_number_of_messages] = 1 if Aws::ActiveJob::SQS.fifo?(queue_url)
|
123
|
+
poller_options
|
124
|
+
end
|
85
125
|
|
126
|
+
def _poll(poller_options, queue_url)
|
127
|
+
poller = Aws::SQS::QueuePoller.new(
|
128
|
+
queue_url,
|
129
|
+
client: Aws::ActiveJob::SQS.config.client
|
130
|
+
)
|
86
131
|
single_message = poller_options[:max_number_of_messages] == 1
|
87
|
-
|
88
|
-
@poller.poll(poller_options) do |msgs|
|
132
|
+
poller.poll(poller_options) do |msgs|
|
89
133
|
msgs = [msgs] if single_message
|
90
|
-
|
91
|
-
msgs.each do |msg|
|
92
|
-
@executor.execute(Aws::SQS::Message.new(
|
93
|
-
queue_url: queue_url,
|
94
|
-
receipt_handle: msg.receipt_handle,
|
95
|
-
data: msg,
|
96
|
-
client: client
|
97
|
-
))
|
98
|
-
end
|
134
|
+
execute_messages(msgs, queue_url)
|
99
135
|
end
|
100
136
|
end
|
101
137
|
|
102
|
-
def
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
opts.on('-q', '--queue STRING', '[Required] Queue to poll') { |a| out[:queue] = a }
|
113
|
-
opts.on('-e', '--environment STRING',
|
114
|
-
'Rails environment (defaults to development). You can also use the APP_ENV or RAILS_ENV environment variables to specify the environment.') do |a|
|
115
|
-
out[:environment] = a
|
116
|
-
end
|
117
|
-
opts.on('-t', '--threads INTEGER', Integer,
|
118
|
-
'The maximum number of worker threads to create. Defaults to 2x the number of processors available on this system.') do |a|
|
119
|
-
out[:threads] = a
|
120
|
-
end
|
121
|
-
opts.on('-b', '--backpressure INTEGER', Integer,
|
122
|
-
'The maximum number of messages to have waiting in the Executor queue. This should be a low, but non zero number. Messages in the Executor queue cannot be picked up by other processes and will slow down shutdown.') do |a|
|
123
|
-
out[:backpressure] = a
|
124
|
-
end
|
125
|
-
opts.on('-m', '--max_messages INTEGER', Integer,
|
126
|
-
'Max number of messages to receive in a batch from SQS.') do |a|
|
127
|
-
out[:max_messages] = a
|
128
|
-
end
|
129
|
-
opts.on('-v', '--visibility_timeout INTEGER', Integer,
|
130
|
-
'The visibility timeout is the number of seconds that a message will not be processable by any other consumers. You should set this value to be longer than your expected job runtime to prevent other processes from picking up an running job. See the SQS Visibility Timeout Documentation at https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html.') do |a|
|
131
|
-
out[:visibility_timeout] = a
|
132
|
-
end
|
133
|
-
opts.on('-s', '--shutdown_timeout INTEGER', Integer,
|
134
|
-
'The amount of time to wait for a clean shutdown. Jobs that are unable to complete in this time will not be deleted from the SQS queue and will be retryable after the visibility timeout.') do |a|
|
135
|
-
out[:shutdown_timeout] = a
|
136
|
-
end
|
137
|
-
opts.on('--[no-]retry_standard_errors [FLAG]', TrueClass,
|
138
|
-
'When set, retry all StandardErrors (leaving failed messages on the SQS Queue). These retries are ON TOP of standard Rails ActiveJob retries set by retry_on in the ActiveJob.') do |a|
|
139
|
-
out[:retry_standard_errors] = a.nil? ? true : a
|
140
|
-
end
|
138
|
+
def execute_messages(msgs, queue_url)
|
139
|
+
@logger.info "Processing batch of #{msgs.length} messages"
|
140
|
+
msgs.each do |msg|
|
141
|
+
sqs_message = Aws::SQS::Message.new(
|
142
|
+
queue_url: queue_url,
|
143
|
+
receipt_handle: msg.receipt_handle,
|
144
|
+
data: msg,
|
145
|
+
client: Aws::ActiveJob::SQS.config.client
|
146
|
+
)
|
147
|
+
@executor.execute(sqs_message)
|
141
148
|
end
|
142
|
-
|
143
|
-
parser.banner = 'aws_sqs_active_job [options]'
|
144
|
-
parser.on_tail '-h', '--help', 'Show help' do
|
145
|
-
puts parser
|
146
|
-
exit 1
|
147
|
-
end
|
148
|
-
|
149
|
-
parser.parse(argv)
|
150
|
-
out
|
151
|
-
end
|
152
|
-
# rubocop:enable Metrics
|
153
|
-
|
154
|
-
def validate_config
|
155
|
-
raise ArgumentError, 'You must specify the name of the queue to process jobs from' unless @options[:queue]
|
156
149
|
end
|
157
150
|
end
|
158
151
|
end
|
data/lib/aws-activejob-sqs.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'active_job'
|
3
4
|
require_relative 'active_job/queue_adapters/sqs_adapter'
|
4
5
|
require_relative 'active_job/queue_adapters/sqs_adapter/params'
|
5
6
|
require_relative 'active_job/queue_adapters/sqs_async_adapter'
|
@@ -11,6 +12,7 @@ require_relative 'aws/active_job/sqs/lambda_handler'
|
|
11
12
|
|
12
13
|
module Aws
|
13
14
|
module ActiveJob
|
15
|
+
# ActiveJob Adapter and backend queueing using AWS SQS.
|
14
16
|
module SQS
|
15
17
|
VERSION = File.read(File.expand_path('../VERSION', __dir__)).strip
|
16
18
|
|
@@ -19,7 +21,7 @@ module Aws
|
|
19
21
|
@config ||= Configuration.new
|
20
22
|
end
|
21
23
|
|
22
|
-
# @yield Configuration
|
24
|
+
# @yield [Configuration] the (singleton) Configuration
|
23
25
|
def self.configure
|
24
26
|
yield(config)
|
25
27
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: aws-activejob-sqs
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Amazon Web Services
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-12-
|
11
|
+
date: 2024-12-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sdk-sqs
|
@@ -62,18 +62,19 @@ description: Amazon Simple Queue Service as an ActiveJob adapter
|
|
62
62
|
email:
|
63
63
|
- aws-dr-rubygems@amazon.com
|
64
64
|
executables:
|
65
|
-
-
|
65
|
+
- aws_active_job_sqs
|
66
66
|
extensions: []
|
67
67
|
extra_rdoc_files: []
|
68
68
|
files:
|
69
69
|
- CHANGELOG.md
|
70
70
|
- LICENSE
|
71
71
|
- VERSION
|
72
|
-
- bin/
|
72
|
+
- bin/aws_active_job_sqs
|
73
73
|
- lib/active_job/queue_adapters/sqs_adapter.rb
|
74
74
|
- lib/active_job/queue_adapters/sqs_adapter/params.rb
|
75
75
|
- lib/active_job/queue_adapters/sqs_async_adapter.rb
|
76
76
|
- lib/aws-activejob-sqs.rb
|
77
|
+
- lib/aws/active_job/sqs/cli_options.rb
|
77
78
|
- lib/aws/active_job/sqs/configuration.rb
|
78
79
|
- lib/aws/active_job/sqs/deduplication.rb
|
79
80
|
- lib/aws/active_job/sqs/executor.rb
|