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.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/bin/aws_sqs_active_job +1 -0
  4. data/lib/action_dispatch/session/dynamodb_store.rb +9 -3
  5. data/lib/active_job/queue_adapters/amazon_sqs_adapter/params.rb +78 -0
  6. data/lib/active_job/queue_adapters/amazon_sqs_adapter.rb +33 -37
  7. data/lib/active_job/queue_adapters/amazon_sqs_async_adapter.rb +12 -11
  8. data/lib/aws/rails/middleware/ebs_sqs_active_job_middleware.rb +31 -5
  9. data/lib/aws/rails/notifications.rb +1 -4
  10. data/lib/aws/rails/railtie.rb +9 -4
  11. data/lib/aws/rails/{mailer.rb → ses_mailer.rb} +12 -10
  12. data/lib/aws/rails/sesv2_mailer.rb +60 -0
  13. data/lib/aws/rails/sqs_active_job/configuration.rb +61 -24
  14. data/lib/aws/rails/sqs_active_job/deduplication.rb +21 -0
  15. data/lib/aws/rails/sqs_active_job/executor.rb +47 -28
  16. data/lib/aws/rails/sqs_active_job/job_runner.rb +5 -1
  17. data/lib/aws/rails/sqs_active_job/lambda_handler.rb +3 -6
  18. data/lib/aws/rails/sqs_active_job/poller.rb +56 -32
  19. data/lib/aws-sdk-rails.rb +4 -1
  20. data/lib/generators/aws_record/base.rb +164 -168
  21. data/lib/generators/aws_record/generated_attribute.rb +50 -41
  22. data/lib/generators/aws_record/model/model_generator.rb +8 -4
  23. data/lib/generators/aws_record/secondary_index.rb +31 -25
  24. data/lib/generators/dynamo_db/session_store_migration/session_store_migration_generator.rb +3 -1
  25. data/lib/tasks/aws_record/migrate.rake +2 -0
  26. data/lib/tasks/dynamo_db/session_store.rake +2 -0
  27. metadata +52 -18
  28. /data/lib/generators/aws_record/model/templates/{model.rb → model.erb} +0 -0
  29. /data/lib/generators/aws_record/model/templates/{table_config.rb → table_config.erb} +0 -0
  30. /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
- min_threads: 0,
13
- max_threads: Concurrent.processor_count,
14
- auto_terminate: true,
15
- idletime: 60, # 1 minute
16
- fallback_policy: :caller_runs # slow down the producer thread
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
- @logger = options[:logger] || ActiveSupport::Logger.new(STDOUT)
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
- @executor.post(message) do |message|
27
- begin
28
- job = JobRunner.new(message)
29
- @logger.info("Running job: #{job.id}[#{job.class_name}]")
30
- job.run
31
- message.delete
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
- " finished cleanly. Unfinished jobs will not be removed from"\
51
- " the queue and can be ru-run once their visibility timeout"\
52
- " passes."
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: self.to_message_attributes(record),
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'].each_with_object({}) do |(key, value), acc|
46
- acc[key] = {
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["APP_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
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(STDOUT)
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(max_threads: @options[:threads], logger: @logger, max_queue: @options[:backpressure])
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
- queue_url: queue_url,
94
- receipt_handle: msg.receipt_handle,
95
- data: msg,
96
- client: client
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 "rails"
105
- require File.expand_path("config/environment.rb")
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 { |opts|
111
- opts.on("-q", "--queue STRING", "[Required] Queue to poll") { |a| out[:queue] = a }
112
- opts.on("-e", "--environment STRING", "Rails environment (defaults to development). You can also use the APP_ENV or RAILS_ENV environment variables to specify the environment.") { |a| out[:environment] = a }
113
- opts.on("-t", "--threads INTEGER", Integer, "The maximum number of worker threads to create. Defaults to 2x the number of processors available on this system.") { |a| out[:threads] = a }
114
- opts.on("-b", "--backpressure INTEGER", Integer, "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.") { |a| out[:backpressure] = a }
115
- opts.on("-m", "--max_messages INTEGER", Integer, "Max number of messages to receive in a batch from SQS.") { |a| out[:max_messages] = a }
116
- opts.on("-v", "--visibility_timeout INTEGER", Integer, "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.") { |a| out[:visibility_timeout] = a }
117
- opts.on("-s", "--shutdown_timeout INTEGER", Integer, "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.") { |a| out[:shutdown_timeout] = a }
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 = "aws_sqs_active_job [options]"
121
- parser.on_tail "-h", "--help", "Show help" do
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/mailer'
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'