aws-sdk-rails 4.1.0 → 4.2.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 +239 -0
- data/LICENSE.txt +12 -0
- data/VERSION +1 -1
- data/lib/aws/rails/action_mailbox/rspec.rb +6 -65
- data/lib/aws/rails/middleware/elastic_beanstalk_sqsd.rb +134 -0
- data/lib/aws/rails/notifications.rb +3 -6
- data/lib/aws/rails/railtie.rb +39 -40
- data/lib/aws-sdk-rails.rb +16 -8
- metadata +26 -152
- data/app/controllers/action_mailbox/ingresses/ses/inbound_emails_controller.rb +0 -79
- data/bin/aws_sqs_active_job +0 -6
- data/config/routes.rb +0 -8
- data/lib/action_dispatch/session/dynamodb_store.rb +0 -38
- data/lib/active_job/queue_adapters/sqs_adapter/params.rb +0 -78
- data/lib/active_job/queue_adapters/sqs_adapter.rb +0 -58
- data/lib/active_job/queue_adapters/sqs_async_adapter.rb +0 -39
- data/lib/aws/rails/action_mailbox/engine.rb +0 -21
- data/lib/aws/rails/action_mailbox/rspec/email.rb +0 -50
- data/lib/aws/rails/action_mailbox/rspec/subscription_confirmation.rb +0 -37
- data/lib/aws/rails/action_mailbox/s3_client.rb +0 -28
- data/lib/aws/rails/action_mailbox/sns_message_verifier.rb +0 -18
- data/lib/aws/rails/action_mailbox/sns_notification.rb +0 -99
- data/lib/aws/rails/middleware/ebs_sqs_active_job_middleware.rb +0 -118
- data/lib/aws/rails/ses_mailer.rb +0 -49
- data/lib/aws/rails/sesv2_mailer.rb +0 -60
- data/lib/aws/rails/sqs_active_job/configuration.rb +0 -184
- data/lib/aws/rails/sqs_active_job/deduplication.rb +0 -21
- data/lib/aws/rails/sqs_active_job/executor.rb +0 -77
- data/lib/aws/rails/sqs_active_job/job_runner.rb +0 -27
- data/lib/aws/rails/sqs_active_job/lambda_handler.rb +0 -63
- data/lib/aws/rails/sqs_active_job/poller.rb +0 -160
- data/lib/aws/rails/sqs_active_job.rb +0 -33
- data/lib/generators/aws_record/base.rb +0 -213
- data/lib/generators/aws_record/generated_attribute.rb +0 -138
- data/lib/generators/aws_record/model/USAGE +0 -24
- data/lib/generators/aws_record/model/model_generator.rb +0 -25
- data/lib/generators/aws_record/model/templates/model.erb +0 -48
- data/lib/generators/aws_record/model/templates/table_config.erb +0 -18
- data/lib/generators/aws_record/secondary_index.rb +0 -66
- data/lib/generators/dynamo_db/session_store_migration/USAGE +0 -13
- data/lib/generators/dynamo_db/session_store_migration/session_store_migration_generator.rb +0 -48
- data/lib/generators/dynamo_db/session_store_migration/templates/dynamo_db_session_store.yml +0 -70
- data/lib/generators/dynamo_db/session_store_migration/templates/session_store_migration.erb +0 -9
- data/lib/tasks/aws_record/migrate.rake +0 -14
- data/lib/tasks/dynamo_db/session_store.rake +0 -10
@@ -1,37 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Aws
|
4
|
-
module Rails
|
5
|
-
module ActionMailbox
|
6
|
-
module RSpec
|
7
|
-
# @api private
|
8
|
-
class SubscriptionConfirmation
|
9
|
-
def initialize(authentic: true, topic: 'topic:arn:default')
|
10
|
-
@authentic = authentic
|
11
|
-
@topic = topic
|
12
|
-
end
|
13
|
-
|
14
|
-
def url
|
15
|
-
'/rails/action_mailbox/ses/inbound_emails'
|
16
|
-
end
|
17
|
-
|
18
|
-
def headers
|
19
|
-
{ 'content-type' => 'application/json' }
|
20
|
-
end
|
21
|
-
|
22
|
-
def params
|
23
|
-
{
|
24
|
-
'Type' => 'SubscriptionConfirmation',
|
25
|
-
'TopicArn' => @topic,
|
26
|
-
'SubscribeURL' => 'http://example.com/subscribe'
|
27
|
-
}
|
28
|
-
end
|
29
|
-
|
30
|
-
def authentic?
|
31
|
-
@authentic
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
@@ -1,28 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'aws-sdk-s3'
|
4
|
-
|
5
|
-
module Aws
|
6
|
-
module Rails
|
7
|
-
module ActionMailbox
|
8
|
-
# @api private
|
9
|
-
class S3Client
|
10
|
-
class << self
|
11
|
-
def client
|
12
|
-
@client ||= build_client
|
13
|
-
end
|
14
|
-
|
15
|
-
private
|
16
|
-
|
17
|
-
def build_client
|
18
|
-
client = Aws::S3::Client.new(
|
19
|
-
**::Rails.configuration.action_mailbox.ses.s3_client_options
|
20
|
-
)
|
21
|
-
client.config.user_agent_frameworks << 'aws-sdk-rails'
|
22
|
-
client
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
@@ -1,18 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'aws-sdk-sns'
|
4
|
-
|
5
|
-
module Aws
|
6
|
-
module Rails
|
7
|
-
module ActionMailbox
|
8
|
-
# @api private
|
9
|
-
class SnsMessageVerifier
|
10
|
-
class << self
|
11
|
-
def verifier
|
12
|
-
@verifier ||= Aws::SNS::MessageVerifier.new
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
@@ -1,99 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'aws-sdk-sns'
|
4
|
-
require 'aws/rails/action_mailbox/s3_client'
|
5
|
-
require 'aws/rails/action_mailbox/sns_message_verifier'
|
6
|
-
|
7
|
-
module Aws
|
8
|
-
module Rails
|
9
|
-
module ActionMailbox
|
10
|
-
# @api private
|
11
|
-
class SnsNotification
|
12
|
-
class MessageContentError < StandardError; end
|
13
|
-
|
14
|
-
def initialize(request_body)
|
15
|
-
@request_body = request_body
|
16
|
-
end
|
17
|
-
|
18
|
-
def subscription_confirmed?
|
19
|
-
(200..299).cover?(confirmation_response.code.to_i)
|
20
|
-
end
|
21
|
-
|
22
|
-
def verified?
|
23
|
-
SnsMessageVerifier.verifier.authentic?(@request_body)
|
24
|
-
end
|
25
|
-
|
26
|
-
def topic
|
27
|
-
notification.fetch(:TopicArn)
|
28
|
-
end
|
29
|
-
|
30
|
-
def type
|
31
|
-
notification.fetch(:Type)
|
32
|
-
end
|
33
|
-
|
34
|
-
def message_content
|
35
|
-
raise MessageContentError, 'Incoming emails must have notificationType `Received`' unless receipt?
|
36
|
-
|
37
|
-
if content_in_s3?
|
38
|
-
s3_content
|
39
|
-
else
|
40
|
-
return message[:content] unless destination
|
41
|
-
|
42
|
-
"X-Original-To: #{destination}\n#{message[:content]}"
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
private
|
47
|
-
|
48
|
-
def notification
|
49
|
-
@notification ||= JSON.parse(@request_body, symbolize_names: true)
|
50
|
-
rescue JSON::ParserError => e
|
51
|
-
Rails.logger.warn("Unable to parse SNS notification: #{e}")
|
52
|
-
nil
|
53
|
-
end
|
54
|
-
|
55
|
-
def s3_content
|
56
|
-
S3Client
|
57
|
-
.client
|
58
|
-
.get_object(key: key, bucket: bucket)
|
59
|
-
.body
|
60
|
-
.string
|
61
|
-
end
|
62
|
-
|
63
|
-
def message
|
64
|
-
@message ||= JSON.parse(notification[:Message], symbolize_names: true)
|
65
|
-
end
|
66
|
-
|
67
|
-
def destination
|
68
|
-
message.dig(:mail, :destination)&.first
|
69
|
-
end
|
70
|
-
|
71
|
-
def action
|
72
|
-
return unless message[:receipt]
|
73
|
-
|
74
|
-
message.fetch(:receipt).fetch(:action)
|
75
|
-
end
|
76
|
-
|
77
|
-
def bucket
|
78
|
-
action.fetch(:bucketName)
|
79
|
-
end
|
80
|
-
|
81
|
-
def key
|
82
|
-
action.fetch(:objectKey)
|
83
|
-
end
|
84
|
-
|
85
|
-
def content_in_s3?
|
86
|
-
action&.fetch(:type) == 'S3'
|
87
|
-
end
|
88
|
-
|
89
|
-
def receipt?
|
90
|
-
message.fetch(:notificationType) == 'Received'
|
91
|
-
end
|
92
|
-
|
93
|
-
def confirmation_response
|
94
|
-
@confirmation_response ||= Net::HTTP.get_response(URI(notification[:SubscribeURL]))
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|
98
|
-
end
|
99
|
-
end
|
@@ -1,118 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Aws
|
4
|
-
module Rails
|
5
|
-
# Middleware to handle requests from the SQS Daemon present on Elastic Beanstalk worker environments.
|
6
|
-
class EbsSqsActiveJobMiddleware
|
7
|
-
INTERNAL_ERROR_MESSAGE = 'Failed to execute job - see Rails log for more details.'
|
8
|
-
INTERNAL_ERROR_RESPONSE = [500, { 'Content-Type' => 'text/plain' }, [INTERNAL_ERROR_MESSAGE]].freeze
|
9
|
-
FORBIDDEN_MESSAGE = 'Request with aws-sqsd user agent was made from untrusted address.'
|
10
|
-
FORBIDDEN_RESPONSE = [403, { 'Content-Type' => 'text/plain' }, [FORBIDDEN_MESSAGE]].freeze
|
11
|
-
|
12
|
-
def initialize(app)
|
13
|
-
@app = app
|
14
|
-
@logger = ::Rails.logger
|
15
|
-
end
|
16
|
-
|
17
|
-
def call(env)
|
18
|
-
request = ActionDispatch::Request.new(env)
|
19
|
-
|
20
|
-
# Pass through unless user agent is the SQS Daemon
|
21
|
-
return @app.call(env) unless from_sqs_daemon?(request)
|
22
|
-
|
23
|
-
@logger.debug('aws-sdk-rails middleware detected call from Elastic Beanstalk SQS Daemon.')
|
24
|
-
|
25
|
-
# Only accept requests from this user agent if it is from localhost or a docker host in case of forgery.
|
26
|
-
unless request.local? || sent_from_docker_host?(request)
|
27
|
-
@logger.warn("SQSD request detected from untrusted address #{request.ip}; returning 403 forbidden.")
|
28
|
-
return FORBIDDEN_RESPONSE
|
29
|
-
end
|
30
|
-
|
31
|
-
# Execute job or periodic task based on HTTP request context
|
32
|
-
periodic_task?(request) ? execute_periodic_task(request) : execute_job(request)
|
33
|
-
end
|
34
|
-
|
35
|
-
private
|
36
|
-
|
37
|
-
def execute_job(request)
|
38
|
-
# Jobs queued from the Active Job SQS adapter contain the JSON message in the request body.
|
39
|
-
job = Aws::Json.load(request.body.string)
|
40
|
-
job_name = job['job_class']
|
41
|
-
@logger.debug("Executing job: #{job_name}")
|
42
|
-
|
43
|
-
begin
|
44
|
-
ActiveJob::Base.execute(job)
|
45
|
-
rescue NoMethodError, NameError => e
|
46
|
-
@logger.error("Job #{job_name} could not resolve to a class that inherits from Active Job.")
|
47
|
-
@logger.error("Error: #{e}")
|
48
|
-
return INTERNAL_ERROR_RESPONSE
|
49
|
-
end
|
50
|
-
|
51
|
-
[200, { 'Content-Type' => 'text/plain' }, ["Successfully ran job #{job_name}."]]
|
52
|
-
end
|
53
|
-
|
54
|
-
def execute_periodic_task(request)
|
55
|
-
# The beanstalk worker SQS Daemon will add the 'X-Aws-Sqsd-Taskname' for periodic tasks set in cron.yaml.
|
56
|
-
job_name = request.headers['X-Aws-Sqsd-Taskname']
|
57
|
-
@logger.debug("Creating and executing periodic task: #{job_name}")
|
58
|
-
|
59
|
-
begin
|
60
|
-
job = job_name.constantize.new
|
61
|
-
job.perform_now
|
62
|
-
rescue NoMethodError, NameError => e
|
63
|
-
@logger.error("Periodic task #{job_name} could not resolve to an Active Job class - check the spelling in cron.yaml.")
|
64
|
-
@logger.error("Error: #{e}.")
|
65
|
-
return INTERNAL_ERROR_RESPONSE
|
66
|
-
end
|
67
|
-
|
68
|
-
[200, { 'Content-Type' => 'text/plain' }, ["Successfully ran periodic task #{job_name}."]]
|
69
|
-
end
|
70
|
-
|
71
|
-
# The beanstalk worker SQS Daemon sets a specific User-Agent headers that begins with 'aws-sqsd'.
|
72
|
-
def from_sqs_daemon?(request)
|
73
|
-
current_user_agent = request.headers['User-Agent']
|
74
|
-
|
75
|
-
!current_user_agent.nil? && current_user_agent.start_with?('aws-sqsd')
|
76
|
-
end
|
77
|
-
|
78
|
-
# The beanstalk worker SQS Daemon will add the custom 'X-Aws-Sqsd-Taskname' header for periodic tasks set in cron.yaml.
|
79
|
-
def periodic_task?(request)
|
80
|
-
!request.headers['X-Aws-Sqsd-Taskname'].nil? && request.headers['X-Aws-Sqsd-Taskname'].present?
|
81
|
-
end
|
82
|
-
|
83
|
-
def sent_from_docker_host?(request)
|
84
|
-
app_runs_in_docker_container? && default_gw_ips.include?(request.ip)
|
85
|
-
end
|
86
|
-
|
87
|
-
def app_runs_in_docker_container?
|
88
|
-
@app_runs_in_docker_container ||= in_docker_container_with_cgroup1? || in_docker_container_with_cgroup2?
|
89
|
-
end
|
90
|
-
|
91
|
-
def in_docker_container_with_cgroup1?
|
92
|
-
File.exist?('/proc/1/cgroup') && File.read('/proc/1/cgroup') =~ %r{/docker/}
|
93
|
-
end
|
94
|
-
|
95
|
-
def in_docker_container_with_cgroup2?
|
96
|
-
File.exist?('/proc/self/mountinfo') && File.read('/proc/self/mountinfo') =~ %r{/docker/containers/}
|
97
|
-
end
|
98
|
-
|
99
|
-
def default_gw_ips
|
100
|
-
default_gw_ips = ['172.17.0.1']
|
101
|
-
|
102
|
-
if File.exist?('/proc/net/route')
|
103
|
-
File.open('/proc/net/route').each_line do |line|
|
104
|
-
fields = line.strip.split
|
105
|
-
next if fields.size != 11
|
106
|
-
|
107
|
-
# Destination == 0.0.0.0 and Flags & RTF_GATEWAY != 0
|
108
|
-
if fields[1] == '00000000' && (fields[3].hex & 0x2) != 0
|
109
|
-
default_gw_ips << IPAddr.new_ntoh([fields[2].hex].pack('L')).to_s
|
110
|
-
end
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
default_gw_ips
|
115
|
-
end
|
116
|
-
end
|
117
|
-
end
|
118
|
-
end
|
data/lib/aws/rails/ses_mailer.rb
DELETED
@@ -1,49 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'aws-sdk-ses'
|
4
|
-
|
5
|
-
module Aws
|
6
|
-
module Rails
|
7
|
-
# Provides a delivery method for ActionMailer that uses Amazon Simple Email
|
8
|
-
# Service.
|
9
|
-
#
|
10
|
-
# Once you have an SES delivery method you can configure Rails to
|
11
|
-
# use this for ActionMailer in your environment configuration
|
12
|
-
# (e.g. RAILS_ROOT/config/environments/production.rb)
|
13
|
-
#
|
14
|
-
# config.action_mailer.delivery_method = :ses
|
15
|
-
#
|
16
|
-
# Uses the AWS SDK for Ruby's credential provider chain when creating an SES
|
17
|
-
# client instance.
|
18
|
-
class SesMailer
|
19
|
-
# @param [Hash] options Passes along initialization options to
|
20
|
-
# [Aws::SES::Client.new](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/SES/Client.html#initialize-instance_method).
|
21
|
-
def initialize(options = {})
|
22
|
-
@client = SES::Client.new(options)
|
23
|
-
@client.config.user_agent_frameworks << 'aws-sdk-rails'
|
24
|
-
end
|
25
|
-
|
26
|
-
# Rails expects this method to exist, and to handle a Mail::Message object
|
27
|
-
# correctly. Called during mail delivery.
|
28
|
-
def deliver!(message)
|
29
|
-
params = {
|
30
|
-
raw_message: { data: message.to_s },
|
31
|
-
source: message.smtp_envelope_from, # defaults to From header
|
32
|
-
destinations: message.smtp_envelope_to # defaults to destinations (To,Cc,Bcc)
|
33
|
-
}
|
34
|
-
@client.send_raw_email(params).tap do |response|
|
35
|
-
message.header[:ses_message_id] = response.message_id
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
# ActionMailer expects this method to be present and to return a hash.
|
40
|
-
def settings
|
41
|
-
{}
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
# This is for backwards compatibility after introducing support for SESv2.
|
48
|
-
# The old mailer is now replaced with the new SES (v1) mailer.
|
49
|
-
Aws::Rails::Mailer = Aws::Rails::SesMailer
|
@@ -1,60 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'aws-sdk-sesv2'
|
4
|
-
|
5
|
-
module Aws
|
6
|
-
module Rails
|
7
|
-
# Provides a delivery method for ActionMailer that uses Amazon Simple Email
|
8
|
-
# Service V2.
|
9
|
-
#
|
10
|
-
# Once you have an SESv2 delivery method you can configure Rails to
|
11
|
-
# use this for ActionMailer in your environment configuration
|
12
|
-
# (e.g. RAILS_ROOT/config/environments/production.rb)
|
13
|
-
#
|
14
|
-
# config.action_mailer.delivery_method = :sesv2
|
15
|
-
#
|
16
|
-
# Uses the AWS SDK for Ruby's credential provider chain when creating an SESV2
|
17
|
-
# client instance.
|
18
|
-
class Sesv2Mailer
|
19
|
-
# @param [Hash] options Passes along initialization options to
|
20
|
-
# [Aws::SESV2::Client.new](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/SESV2/Client.html#initialize-instance_method).
|
21
|
-
def initialize(options = {})
|
22
|
-
@client = SESV2::Client.new(options)
|
23
|
-
@client.config.user_agent_frameworks << 'aws-sdk-rails'
|
24
|
-
end
|
25
|
-
|
26
|
-
# Rails expects this method to exist, and to handle a Mail::Message object
|
27
|
-
# correctly. Called during mail delivery.
|
28
|
-
def deliver!(message)
|
29
|
-
params = { content: { raw: { data: message.to_s } } }
|
30
|
-
# smtp_envelope_from will default to the From address *without* sender names.
|
31
|
-
# By omitting this param, SESv2 will correctly use sender names from the mail headers.
|
32
|
-
# We should only use smtp_envelope_from when it was explicitly set (instance variable set)
|
33
|
-
params[:from_email_address] = message.smtp_envelope_from if message.instance_variable_get(:@smtp_envelope_from)
|
34
|
-
params[:destination] = {
|
35
|
-
to_addresses: to_addresses(message),
|
36
|
-
cc_addresses: message.cc,
|
37
|
-
bcc_addresses: message.bcc
|
38
|
-
}
|
39
|
-
|
40
|
-
@client.send_email(params).tap do |response|
|
41
|
-
message.header[:ses_message_id] = response.message_id
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
# ActionMailer expects this method to be present and to return a hash.
|
46
|
-
def settings
|
47
|
-
{}
|
48
|
-
end
|
49
|
-
|
50
|
-
private
|
51
|
-
|
52
|
-
# smtp_envelope_to will default to the full destinations (To, Cc, Bcc)
|
53
|
-
# SES v2 API prefers each component split out into a destination hash.
|
54
|
-
# When smtp_envelope_to was set, use it explicitly for to_address only.
|
55
|
-
def to_addresses(message)
|
56
|
-
message.instance_variable_get(:@smtp_envelope_to) ? message.smtp_envelope_to : message.to
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
@@ -1,184 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Aws
|
4
|
-
module Rails
|
5
|
-
# Configuration for AWS SQS ActiveJob.
|
6
|
-
module SqsActiveJob
|
7
|
-
# Use +Aws::Rails::SqsActiveJob.config+ to access the singleton config instance.
|
8
|
-
class Configuration
|
9
|
-
# Default configuration options
|
10
|
-
# @api private
|
11
|
-
DEFAULTS = {
|
12
|
-
max_messages: 10,
|
13
|
-
shutdown_timeout: 15,
|
14
|
-
retry_standard_errors: true, # TODO: Remove in next MV
|
15
|
-
queues: {},
|
16
|
-
logger: ::Rails.logger,
|
17
|
-
message_group_id: 'SqsActiveJobGroup',
|
18
|
-
excluded_deduplication_keys: ['job_id']
|
19
|
-
}.freeze
|
20
|
-
|
21
|
-
# @api private
|
22
|
-
attr_accessor :queues, :max_messages, :visibility_timeout,
|
23
|
-
:shutdown_timeout, :client, :logger,
|
24
|
-
:async_queue_error_handler, :message_group_id
|
25
|
-
|
26
|
-
attr_reader :excluded_deduplication_keys
|
27
|
-
|
28
|
-
# Don't use this method directly: Configuration is a singleton class, use
|
29
|
-
# +Aws::Rails::SqsActiveJob.config+ to access the singleton config.
|
30
|
-
#
|
31
|
-
# @param [Hash] options
|
32
|
-
# @option options [Hash[Symbol, String]] :queues A mapping between the
|
33
|
-
# active job queue name and the SQS Queue URL. Note: multiple active
|
34
|
-
# job queues can map to the same SQS Queue URL.
|
35
|
-
#
|
36
|
-
# @option options [Integer] :max_messages
|
37
|
-
# The max number of messages to poll for in a batch.
|
38
|
-
#
|
39
|
-
# @option options [Integer] :visibility_timeout
|
40
|
-
# If unset, the visibility timeout configured on the
|
41
|
-
# SQS queue will be used.
|
42
|
-
# The visibility timeout is the number of seconds
|
43
|
-
# that a message will not be processable by any other consumers.
|
44
|
-
# You should set this value to be longer than your expected job runtime
|
45
|
-
# to prevent other processes from picking up an running job.
|
46
|
-
# See the (SQS Visibility Timeout Documentation)[https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html]
|
47
|
-
#
|
48
|
-
# @option options [Integer] :shutdown_timeout
|
49
|
-
# the amount of time to wait
|
50
|
-
# for a clean shutdown. Jobs that are unable to complete in this time
|
51
|
-
# will not be deleted from the SQS queue and will be retryable after
|
52
|
-
# the visibility timeout.
|
53
|
-
#
|
54
|
-
# @ option options [Boolean] :retry_standard_errors
|
55
|
-
# If `true`, StandardErrors raised by ActiveJobs are left on the queue
|
56
|
-
# and will be retried (pending the SQS Queue's redrive/DLQ/maximum receive settings).
|
57
|
-
# This behavior overrides the standard Rails ActiveJob
|
58
|
-
# [Retry/Discard for failed jobs](https://guides.rubyonrails.org/active_job_basics.html#retrying-or-discarding-failed-jobs)
|
59
|
-
# behavior. When set to `true` the retries provided by this will be
|
60
|
-
# on top of any retries configured on the job with `retry_on`.
|
61
|
-
# When `false`, retry behavior is fully configured
|
62
|
-
# through `retry_on`/`discard_on` on the ActiveJobs.
|
63
|
-
#
|
64
|
-
# @option options [ActiveSupport::Logger] :logger Logger to use
|
65
|
-
# for the poller.
|
66
|
-
#
|
67
|
-
# @option options [String] :config_file
|
68
|
-
# Override file to load configuration from. If not specified will
|
69
|
-
# attempt to load from config/aws_sqs_active_job.yml.
|
70
|
-
#
|
71
|
-
# @option options [String] :message_group_id (SqsActiveJobGroup)
|
72
|
-
# The message_group_id to use for queueing messages on a fifo queues.
|
73
|
-
# Applies only to jobs queued on FIFO queues.
|
74
|
-
# See the (SQS FIFO Documentation)[https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues.html]
|
75
|
-
#
|
76
|
-
# @option options [Callable] :async_queue_error_handler An error handler
|
77
|
-
# to be called when the async active job adapter experiances an error
|
78
|
-
# queueing a job. Only applies when
|
79
|
-
# +active_job.queue_adapter = :sqs_async+. Called with:
|
80
|
-
# [error, job, job_options]
|
81
|
-
#
|
82
|
-
# @option options [SQS::Client] :client SQS Client to use. A default
|
83
|
-
# client will be created if none is provided.
|
84
|
-
#
|
85
|
-
# @option options [Array] :excluded_deduplication_keys (['job_id'])
|
86
|
-
# The type of keys stored in the array should be String or Symbol.
|
87
|
-
# Using this option, job_id is implicitly added to the keys.
|
88
|
-
|
89
|
-
def initialize(options = {})
|
90
|
-
options[:config_file] ||= config_file if File.exist?(config_file)
|
91
|
-
options = DEFAULTS
|
92
|
-
.merge(file_options(options))
|
93
|
-
.merge(options)
|
94
|
-
set_attributes(options)
|
95
|
-
end
|
96
|
-
|
97
|
-
def excluded_deduplication_keys=(keys)
|
98
|
-
@excluded_deduplication_keys = keys.map(&:to_s) | ['job_id']
|
99
|
-
end
|
100
|
-
|
101
|
-
def client
|
102
|
-
@client ||= begin
|
103
|
-
client = Aws::SQS::Client.new
|
104
|
-
client.config.user_agent_frameworks << 'aws-sdk-rails'
|
105
|
-
client
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
# Return the queue_url for a given job_queue name
|
110
|
-
def queue_url_for(job_queue)
|
111
|
-
job_queue = job_queue.to_sym
|
112
|
-
raise ArgumentError, "No queue defined for #{job_queue}" unless queues.key? job_queue
|
113
|
-
|
114
|
-
queues[job_queue]
|
115
|
-
end
|
116
|
-
|
117
|
-
# @api private
|
118
|
-
def to_s
|
119
|
-
to_h.to_s
|
120
|
-
end
|
121
|
-
|
122
|
-
# @api private
|
123
|
-
def to_h
|
124
|
-
h = {}
|
125
|
-
instance_variables.each do |v|
|
126
|
-
v_sym = v.to_s.delete('@').to_sym
|
127
|
-
val = instance_variable_get(v)
|
128
|
-
h[v_sym] = val
|
129
|
-
end
|
130
|
-
h
|
131
|
-
end
|
132
|
-
|
133
|
-
private
|
134
|
-
|
135
|
-
# Set accessible attributes after merged options.
|
136
|
-
def set_attributes(options)
|
137
|
-
options.each_key do |opt_name|
|
138
|
-
instance_variable_set("@#{opt_name}", options[opt_name])
|
139
|
-
client.config.user_agent_frameworks << 'aws-sdk-rails' if opt_name == :client
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
def file_options(options = {})
|
144
|
-
file_path = config_file_path(options)
|
145
|
-
if file_path
|
146
|
-
load_from_file(file_path)
|
147
|
-
else
|
148
|
-
{}
|
149
|
-
end
|
150
|
-
end
|
151
|
-
|
152
|
-
def config_file
|
153
|
-
file = ::Rails.root.join("config/aws_sqs_active_job/#{::Rails.env}.yml")
|
154
|
-
file = ::Rails.root.join('config/aws_sqs_active_job.yml') unless File.exist?(file)
|
155
|
-
file
|
156
|
-
end
|
157
|
-
|
158
|
-
# Load options from YAML file
|
159
|
-
def load_from_file(file_path)
|
160
|
-
opts = load_yaml(file_path) || {}
|
161
|
-
opts.deep_symbolize_keys
|
162
|
-
end
|
163
|
-
|
164
|
-
# @return [String] Configuration path found in environment or YAML file.
|
165
|
-
def config_file_path(options)
|
166
|
-
options[:config_file] || ENV.fetch('AWS_SQS_ACTIVE_JOB_CONFIG_FILE', nil)
|
167
|
-
end
|
168
|
-
|
169
|
-
def load_yaml(file_path)
|
170
|
-
require 'erb'
|
171
|
-
source = ERB.new(File.read(file_path)).result
|
172
|
-
|
173
|
-
# Avoid incompatible changes with Psych 4.0.0
|
174
|
-
# https://bugs.ruby-lang.org/issues/17866
|
175
|
-
begin
|
176
|
-
YAML.safe_load(source, aliases: true) || {}
|
177
|
-
rescue ArgumentError
|
178
|
-
YAML.safe_load(source) || {}
|
179
|
-
end
|
180
|
-
end
|
181
|
-
end
|
182
|
-
end
|
183
|
-
end
|
184
|
-
end
|
@@ -1,21 +0,0 @@
|
|
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
|
@@ -1,77 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'concurrent'
|
4
|
-
|
5
|
-
module Aws
|
6
|
-
module Rails
|
7
|
-
module SqsActiveJob
|
8
|
-
# CLI runner for polling for SQS ActiveJobs
|
9
|
-
class Executor
|
10
|
-
DEFAULTS = {
|
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
|
16
|
-
}.freeze
|
17
|
-
|
18
|
-
def initialize(options = {})
|
19
|
-
@executor = Concurrent::ThreadPoolExecutor.new(DEFAULTS.merge(options))
|
20
|
-
@retry_standard_errors = options[:retry_standard_errors]
|
21
|
-
@logger = options[:logger] || ActiveSupport::Logger.new($stdout)
|
22
|
-
@task_complete = Concurrent::Event.new
|
23
|
-
end
|
24
|
-
|
25
|
-
def execute(message)
|
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
|
32
|
-
end
|
33
|
-
|
34
|
-
def shutdown(timeout = nil)
|
35
|
-
@executor.shutdown
|
36
|
-
clean_shutdown = @executor.wait_for_termination(timeout)
|
37
|
-
if clean_shutdown
|
38
|
-
@logger.info 'Clean shutdown complete. All executing jobs finished.'
|
39
|
-
else
|
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
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|