aws-sdk-rails 3.6.1 → 3.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/bin/aws_sqs_active_job +1 -0
- data/lib/action_dispatch/session/dynamodb_store.rb +9 -3
- data/lib/active_job/queue_adapters/amazon_sqs_adapter/params.rb +78 -0
- data/lib/active_job/queue_adapters/amazon_sqs_adapter.rb +33 -37
- data/lib/active_job/queue_adapters/amazon_sqs_async_adapter.rb +12 -11
- data/lib/aws/rails/middleware/ebs_sqs_active_job_middleware.rb +31 -5
- data/lib/aws/rails/notifications.rb +1 -4
- data/lib/aws/rails/railtie.rb +9 -4
- data/lib/aws/rails/{mailer.rb → ses_mailer.rb} +12 -10
- data/lib/aws/rails/sesv2_mailer.rb +60 -0
- data/lib/aws/rails/sqs_active_job/configuration.rb +61 -24
- data/lib/aws/rails/sqs_active_job/deduplication.rb +21 -0
- data/lib/aws/rails/sqs_active_job/executor.rb +47 -28
- data/lib/aws/rails/sqs_active_job/job_runner.rb +5 -1
- data/lib/aws/rails/sqs_active_job/lambda_handler.rb +3 -6
- data/lib/aws/rails/sqs_active_job/poller.rb +56 -32
- data/lib/aws-sdk-rails.rb +4 -1
- data/lib/generators/aws_record/base.rb +164 -168
- data/lib/generators/aws_record/generated_attribute.rb +50 -41
- data/lib/generators/aws_record/model/model_generator.rb +8 -4
- data/lib/generators/aws_record/secondary_index.rb +31 -25
- data/lib/generators/dynamo_db/session_store_migration/session_store_migration_generator.rb +3 -1
- data/lib/tasks/aws_record/migrate.rake +2 -0
- data/lib/tasks/dynamo_db/session_store.rake +2 -0
- metadata +52 -18
- /data/lib/generators/aws_record/model/templates/{model.rb → model.erb} +0 -0
- /data/lib/generators/aws_record/model/templates/{table_config.rb → table_config.erb} +0 -0
- /data/lib/generators/dynamo_db/session_store_migration/templates/{session_store_migration.rb → session_store_migration.erb} +0 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aws
|
4
|
+
module Rails
|
5
|
+
# SQS ActiveJob modules
|
6
|
+
module SqsActiveJob
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
class_attribute :excluded_deduplication_keys
|
11
|
+
end
|
12
|
+
|
13
|
+
# class methods for SQS ActiveJob.
|
14
|
+
module ClassMethods
|
15
|
+
def deduplicate_without(*keys)
|
16
|
+
self.excluded_deduplication_keys = keys.map(&:to_s) | ['job_id']
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -7,49 +7,68 @@ module Aws
|
|
7
7
|
module SqsActiveJob
|
8
8
|
# CLI runner for polling for SQS ActiveJobs
|
9
9
|
class Executor
|
10
|
-
|
11
10
|
DEFAULTS = {
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
11
|
+
min_threads: 0,
|
12
|
+
max_threads: Integer(Concurrent.available_processor_count || Concurrent.processor_count),
|
13
|
+
auto_terminate: true,
|
14
|
+
idletime: 60, # 1 minute
|
15
|
+
fallback_policy: :abort # Concurrent::RejectedExecutionError must be handled
|
17
16
|
}.freeze
|
18
17
|
|
19
18
|
def initialize(options = {})
|
20
19
|
@executor = Concurrent::ThreadPoolExecutor.new(DEFAULTS.merge(options))
|
21
|
-
@
|
20
|
+
@retry_standard_errors = options[:retry_standard_errors]
|
21
|
+
@logger = options[:logger] || ActiveSupport::Logger.new($stdout)
|
22
|
+
@task_complete = Concurrent::Event.new
|
22
23
|
end
|
23
24
|
|
24
|
-
# TODO: Consider catching the exception and sleeping instead of using :caller_runs
|
25
25
|
def execute(message)
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
rescue Aws::Json::ParseError => e
|
33
|
-
@logger.error "Unable to parse message body: #{message.data.body}. Error: #{e}."
|
34
|
-
rescue StandardError => e
|
35
|
-
# message will not be deleted and will be retried
|
36
|
-
job_msg = job ? "#{job.id}[#{job.class_name}]" : 'unknown job'
|
37
|
-
@logger.info "Error processing job #{job_msg}: #{e}"
|
38
|
-
@logger.debug e.backtrace.join("\n")
|
39
|
-
end
|
40
|
-
end
|
26
|
+
post_task(message)
|
27
|
+
rescue Concurrent::RejectedExecutionError
|
28
|
+
# no capacity, wait for a task to complete
|
29
|
+
@task_complete.reset
|
30
|
+
@task_complete.wait
|
31
|
+
retry
|
41
32
|
end
|
42
33
|
|
43
|
-
def shutdown(timeout=nil)
|
34
|
+
def shutdown(timeout = nil)
|
44
35
|
@executor.shutdown
|
45
36
|
clean_shutdown = @executor.wait_for_termination(timeout)
|
46
37
|
if clean_shutdown
|
47
38
|
@logger.info 'Clean shutdown complete. All executing jobs finished.'
|
48
39
|
else
|
49
|
-
@logger.info "Timeout (#{timeout}) exceeded. Some jobs may not have"\
|
50
|
-
|
51
|
-
|
52
|
-
|
40
|
+
@logger.info "Timeout (#{timeout}) exceeded. Some jobs may not have " \
|
41
|
+
'finished cleanly. Unfinished jobs will not be removed from ' \
|
42
|
+
'the queue and can be ru-run once their visibility timeout ' \
|
43
|
+
'passes.'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def post_task(message)
|
50
|
+
@executor.post(message) do |message|
|
51
|
+
job = JobRunner.new(message)
|
52
|
+
@logger.info("Running job: #{job.id}[#{job.class_name}]")
|
53
|
+
job.run
|
54
|
+
message.delete
|
55
|
+
rescue Aws::Json::ParseError => e
|
56
|
+
@logger.error "Unable to parse message body: #{message.data.body}. Error: #{e}."
|
57
|
+
rescue StandardError => e
|
58
|
+
job_msg = job ? "#{job.id}[#{job.class_name}]" : 'unknown job'
|
59
|
+
@logger.info "Error processing job #{job_msg}: #{e}"
|
60
|
+
@logger.debug e.backtrace.join("\n")
|
61
|
+
|
62
|
+
if @retry_standard_errors && !job.exception_executions?
|
63
|
+
@logger.info(
|
64
|
+
'retry_standard_errors is enabled and job has not ' \
|
65
|
+
"been retried by Rails. Leaving #{job_msg} in the queue."
|
66
|
+
)
|
67
|
+
else
|
68
|
+
message.delete
|
69
|
+
end
|
70
|
+
ensure
|
71
|
+
@task_complete.set
|
53
72
|
end
|
54
73
|
end
|
55
74
|
end
|
@@ -3,7 +3,6 @@
|
|
3
3
|
module Aws
|
4
4
|
module Rails
|
5
5
|
module SqsActiveJob
|
6
|
-
|
7
6
|
class JobRunner
|
8
7
|
attr_reader :id, :class_name
|
9
8
|
|
@@ -16,6 +15,11 @@ module Aws
|
|
16
15
|
def run
|
17
16
|
ActiveJob::Base.execute @job_data
|
18
17
|
end
|
18
|
+
|
19
|
+
def exception_executions?
|
20
|
+
@job_data['exception_executions'] &&
|
21
|
+
!@job_data['exception_executions'].empty?
|
22
|
+
end
|
19
23
|
end
|
20
24
|
end
|
21
25
|
end
|
@@ -5,7 +5,6 @@ require 'aws-sdk-sqs'
|
|
5
5
|
module Aws
|
6
6
|
module Rails
|
7
7
|
module SqsActiveJob
|
8
|
-
|
9
8
|
# A lambda event handler to run jobs from an SQS queue trigger
|
10
9
|
# Trigger the lambda from your SQS queue
|
11
10
|
# Configure the entrypoint to: +config/environment.Aws::Rails::SqsActiveJob.lambda_job_handler+
|
@@ -23,13 +22,11 @@ module Aws
|
|
23
22
|
"Processed #{event['Records'].length} jobs."
|
24
23
|
end
|
25
24
|
|
26
|
-
private
|
27
|
-
|
28
25
|
def self.to_sqs_msg(record)
|
29
26
|
msg = Aws::SQS::Types::Message.new(
|
30
27
|
body: record['body'],
|
31
28
|
md5_of_body: record['md5OfBody'],
|
32
|
-
message_attributes:
|
29
|
+
message_attributes: to_message_attributes(record),
|
33
30
|
message_id: record['messageId'],
|
34
31
|
receipt_handle: record['receiptHandle']
|
35
32
|
)
|
@@ -42,8 +39,8 @@ module Aws
|
|
42
39
|
end
|
43
40
|
|
44
41
|
def self.to_message_attributes(record)
|
45
|
-
record['messageAttributes'].
|
46
|
-
|
42
|
+
record['messageAttributes'].transform_values do |value|
|
43
|
+
{
|
47
44
|
string_value: value['stringValue'],
|
48
45
|
binary_value: value['binaryValue'],
|
49
46
|
string_list_values: ['stringListValues'],
|
@@ -7,20 +7,18 @@ require 'concurrent'
|
|
7
7
|
module Aws
|
8
8
|
module Rails
|
9
9
|
module SqsActiveJob
|
10
|
-
|
11
|
-
class Interrupt < Exception; end
|
10
|
+
class Interrupt < StandardError; end
|
12
11
|
|
13
12
|
# CLI runner for polling for SQS ActiveJobs
|
14
13
|
# Use `aws_sqs_active_job --help` for detailed usage
|
15
14
|
class Poller
|
16
|
-
|
17
15
|
DEFAULT_OPTS = {
|
18
|
-
threads: 2*Concurrent.processor_count,
|
16
|
+
threads: 2 * Concurrent.processor_count,
|
19
17
|
max_messages: 10,
|
20
|
-
visibility_timeout: 60,
|
21
18
|
shutdown_timeout: 15,
|
22
|
-
backpressure: 10
|
23
|
-
|
19
|
+
backpressure: 10,
|
20
|
+
retry_standard_errors: true
|
21
|
+
}.freeze
|
24
22
|
|
25
23
|
def initialize(args = ARGV)
|
26
24
|
@options = parse_args(args)
|
@@ -29,7 +27,7 @@ module Aws
|
|
29
27
|
end
|
30
28
|
|
31
29
|
def set_environment
|
32
|
-
@environment = @options[:environment] || ENV[
|
30
|
+
@environment = @options[:environment] || ENV['APP_ENV'] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
|
33
31
|
end
|
34
32
|
|
35
33
|
def run
|
@@ -43,13 +41,17 @@ module Aws
|
|
43
41
|
.merge(@options.to_h)
|
44
42
|
validate_config
|
45
43
|
# ensure we have a logger configured
|
46
|
-
@logger = @options[:logger] || ActiveSupport::Logger.new(
|
44
|
+
@logger = @options[:logger] || ActiveSupport::Logger.new($stdout)
|
47
45
|
@logger.info("Starting Poller with options=#{@options}")
|
48
46
|
|
49
|
-
|
50
47
|
Signal.trap('INT') { raise Interrupt }
|
51
48
|
Signal.trap('TERM') { raise Interrupt }
|
52
|
-
@executor = Executor.new(
|
49
|
+
@executor = Executor.new(
|
50
|
+
max_threads: @options[:threads],
|
51
|
+
logger: @logger,
|
52
|
+
max_queue: @options[:backpressure],
|
53
|
+
retry_standard_errors: @options[:retry_standard_errors]
|
54
|
+
)
|
53
55
|
|
54
56
|
poll
|
55
57
|
rescue Interrupt
|
@@ -79,9 +81,7 @@ module Aws
|
|
79
81
|
# in order
|
80
82
|
# Jobs with different message_group_id will be processed in
|
81
83
|
# parallel and may be out of order.
|
82
|
-
if Aws::Rails::SqsActiveJob.fifo?(queue_url)
|
83
|
-
poller_options[:max_number_of_messages] = 1
|
84
|
-
end
|
84
|
+
poller_options[:max_number_of_messages] = 1 if Aws::Rails::SqsActiveJob.fifo?(queue_url)
|
85
85
|
|
86
86
|
single_message = poller_options[:max_number_of_messages] == 1
|
87
87
|
|
@@ -90,35 +90,58 @@ module Aws
|
|
90
90
|
@logger.info "Processing batch of #{msgs.length} messages"
|
91
91
|
msgs.each do |msg|
|
92
92
|
@executor.execute(Aws::SQS::Message.new(
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
93
|
+
queue_url: queue_url,
|
94
|
+
receipt_handle: msg.receipt_handle,
|
95
|
+
data: msg,
|
96
|
+
client: client
|
97
|
+
))
|
98
98
|
end
|
99
99
|
end
|
100
100
|
end
|
101
101
|
|
102
102
|
def boot_rails
|
103
103
|
ENV['RACK_ENV'] = ENV['RAILS_ENV'] = @environment
|
104
|
-
require
|
105
|
-
require File.expand_path(
|
104
|
+
require 'rails'
|
105
|
+
require File.expand_path('config/environment.rb')
|
106
106
|
end
|
107
107
|
|
108
|
+
# rubocop:disable Metrics
|
108
109
|
def parse_args(argv)
|
109
110
|
out = {}
|
110
|
-
parser = ::OptionParser.new
|
111
|
-
opts.on(
|
112
|
-
opts.on(
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
opts.on(
|
117
|
-
|
118
|
-
|
111
|
+
parser = ::OptionParser.new do |opts|
|
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
|
141
|
+
end
|
119
142
|
|
120
|
-
parser.banner =
|
121
|
-
parser.on_tail
|
143
|
+
parser.banner = 'aws_sqs_active_job [options]'
|
144
|
+
parser.on_tail '-h', '--help', 'Show help' do
|
122
145
|
puts parser
|
123
146
|
exit 1
|
124
147
|
end
|
@@ -126,6 +149,7 @@ module Aws
|
|
126
149
|
parser.parse(argv)
|
127
150
|
out
|
128
151
|
end
|
152
|
+
# rubocop:enable Metrics
|
129
153
|
|
130
154
|
def validate_config
|
131
155
|
raise ArgumentError, 'You must specify the name of the queue to process jobs from' unless @options[:queue]
|
data/lib/aws-sdk-rails.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative 'aws/rails/
|
3
|
+
require_relative 'aws/rails/ses_mailer'
|
4
|
+
require_relative 'aws/rails/sesv2_mailer'
|
4
5
|
require_relative 'aws/rails/railtie'
|
5
6
|
require_relative 'aws/rails/notifications'
|
6
7
|
require_relative 'aws/rails/sqs_active_job/configuration'
|
8
|
+
require_relative 'aws/rails/sqs_active_job/deduplication'
|
7
9
|
require_relative 'aws/rails/sqs_active_job/executor'
|
8
10
|
require_relative 'aws/rails/sqs_active_job/job_runner'
|
9
11
|
require_relative 'aws/rails/sqs_active_job/lambda_handler'
|
@@ -11,6 +13,7 @@ require_relative 'aws/rails/middleware/ebs_sqs_active_job_middleware'
|
|
11
13
|
|
12
14
|
require_relative 'action_dispatch/session/dynamodb_store'
|
13
15
|
require_relative 'active_job/queue_adapters/amazon_sqs_adapter'
|
16
|
+
require_relative 'active_job/queue_adapters/amazon_sqs_adapter/params'
|
14
17
|
require_relative 'active_job/queue_adapters/amazon_sqs_async_adapter'
|
15
18
|
|
16
19
|
require_relative 'generators/aws_record/base'
|