active_elastic_job_gunner 3.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/active-elastic-job.gemspec +37 -0
- data/lib/active_elastic_job.rb +12 -0
- data/lib/active_elastic_job/md5_message_digest_calculation.rb +72 -0
- data/lib/active_elastic_job/message_verifier.rb +34 -0
- data/lib/active_elastic_job/rack/sqs_message_consumer.rb +129 -0
- data/lib/active_elastic_job/railtie.rb +25 -0
- data/lib/active_elastic_job/version.rb +18 -0
- data/lib/active_job/queue_adapters/active_elastic_job_adapter.rb +228 -0
- metadata +208 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8d1a2ff186013ae24c5908c9c3f995fd4f98f284ac80bcb9d50ad4615d30d226
|
4
|
+
data.tar.gz: 8847a8c8627c257c45d16440ce31fe2cdc98b6281fbb9debf6421841354791a7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c95ff1e79cb015d2db00fc81ff0d30fc2cb21ef9a1b61fe5e3979ed2084a6f2c38fbb397aa7ca2cee9f659f64ca04e356dd5d9d99e991cc806c34ae2f388cbc5
|
7
|
+
data.tar.gz: bcee34b110f398ffc2412a7035d5f5c2acbf840e57754bd832b57c531bd4d8b22f137e1dc3080bd4cdabf1c7c9780e8467f2389fa864152ce72e1d8a0299d9f6
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
require 'active_elastic_job/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.platform = Gem::Platform::RUBY
|
9
|
+
spec.name = 'active_elastic_job_gunner'
|
10
|
+
spec.version = ActiveElasticJob.version
|
11
|
+
spec.authors = ['Cody Swann']
|
12
|
+
spec.email = ['cody@gunnertech.com']
|
13
|
+
spec.summary = 'Active Elastic Job is a simple to use Active Job backend for Rails applications deployed on the Amazon Elastic Beanstalk platform.'
|
14
|
+
spec.description = 'Run background jobs / tasks of Rails applications deployed in Amazon Elastic Beanstalk environments. Active Elastic Job is an Active Job backend which is easy to setup. No need for customised container commands or other workarounds.'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
spec.homepage = 'https://github.com/gunnertech/active-elastic-job'
|
17
|
+
|
18
|
+
spec.files = Dir.glob('lib/**/*') + [ 'active-elastic-job.gemspec' ]
|
19
|
+
spec.executables = []
|
20
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
21
|
+
spec.require_paths = ['lib']
|
22
|
+
|
23
|
+
spec.required_ruby_version = '>= 2.5'
|
24
|
+
|
25
|
+
spec.add_dependency 'aws-sdk-sqs'
|
26
|
+
spec.add_dependency 'rails', '>= 5.0'
|
27
|
+
|
28
|
+
spec.add_development_dependency 'sqlite3'
|
29
|
+
spec.add_development_dependency 'bundler'
|
30
|
+
spec.add_development_dependency 'rspec', '~> 3.4'
|
31
|
+
spec.add_development_dependency 'dotenv'
|
32
|
+
spec.add_development_dependency 'fuubar'
|
33
|
+
spec.add_development_dependency 'rdoc'
|
34
|
+
spec.add_development_dependency 'byebug'
|
35
|
+
spec.add_development_dependency 'benchmark-ips'
|
36
|
+
spec.add_development_dependency 'climate_control'
|
37
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'aws-sdk-sqs'
|
2
|
+
require 'active_elastic_job/version'
|
3
|
+
require 'active_elastic_job/md5_message_digest_calculation'
|
4
|
+
require 'active_job/queue_adapters/active_elastic_job_adapter'
|
5
|
+
require 'active_elastic_job/rack/sqs_message_consumer'
|
6
|
+
require 'active_elastic_job/message_verifier'
|
7
|
+
|
8
|
+
module ActiveElasticJob
|
9
|
+
ACRONYM = 'AEJ'.freeze
|
10
|
+
end
|
11
|
+
|
12
|
+
require "active_elastic_job/railtie" if defined? Rails
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
module ActiveElasticJob
|
3
|
+
# This module provides methods that calculate the MD5 digest for Amazon
|
4
|
+
# SQS message bodies and message attributes.
|
5
|
+
# The digest can be used to verify that Amazon SQS received the message
|
6
|
+
# correctly.
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
#
|
10
|
+
# extend ActiveElasticJob::MD5MessageDigestCalculation
|
11
|
+
#
|
12
|
+
# resp = Aws::SQS::Client.new.send_message(
|
13
|
+
# queue_url: queue_url,
|
14
|
+
# message_body: body,
|
15
|
+
# message_attributes: attributes
|
16
|
+
# )
|
17
|
+
#
|
18
|
+
# if resp.md5_of_message_body != md5_of_message_body(body)
|
19
|
+
# raise "Returned digest of message body is invalid!"
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# if resp.md5_of_message_attributes != md5_of_message_attributes(attributes)
|
23
|
+
# raise "Returned digest of message attributes is invalid!"
|
24
|
+
# end
|
25
|
+
module MD5MessageDigestCalculation
|
26
|
+
TRANSPORT_TYPE_ENCODINGS = {
|
27
|
+
'String' => 1,
|
28
|
+
'Binary' => 2,
|
29
|
+
'Number' => 1
|
30
|
+
}
|
31
|
+
|
32
|
+
NORMALIZED_ENCODING = Encoding::UTF_8
|
33
|
+
|
34
|
+
def md5_of_message_body(message_body)
|
35
|
+
OpenSSL::Digest::MD5.hexdigest(message_body)
|
36
|
+
end
|
37
|
+
|
38
|
+
def md5_of_message_attributes(message_attributes)
|
39
|
+
encoded = { }
|
40
|
+
message_attributes.each do |name, attribute|
|
41
|
+
name = name.to_s
|
42
|
+
encoded[name] = String.new
|
43
|
+
encoded[name] << encode_length_and_bytes(name) <<
|
44
|
+
encode_length_and_bytes(attribute[:data_type]) <<
|
45
|
+
[TRANSPORT_TYPE_ENCODINGS[attribute[:data_type]]].pack('C'.freeze)
|
46
|
+
|
47
|
+
if attribute[:string_value] != nil
|
48
|
+
encoded[name] << encode_length_and_string(attribute[:string_value])
|
49
|
+
elsif attribute[:binary_value] != nil
|
50
|
+
encoded[name] << encode_length_and_bytes(attribute[:binary_value])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
buffer = encoded.keys.sort.reduce(String.new) do |string, name|
|
55
|
+
string << encoded[name]
|
56
|
+
end
|
57
|
+
OpenSSL::Digest::MD5.hexdigest(buffer)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def encode_length_and_string(string)
|
63
|
+
string = String.new(string)
|
64
|
+
string.encode!(NORMALIZED_ENCODING)
|
65
|
+
encode_length_and_bytes(string)
|
66
|
+
end
|
67
|
+
|
68
|
+
def encode_length_and_bytes(bytes)
|
69
|
+
[bytes.bytesize, bytes].pack('L>a*'.freeze)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'active_support/security_utils'
|
2
|
+
|
3
|
+
module ActiveElasticJob
|
4
|
+
class MessageVerifier #:nodoc:
|
5
|
+
|
6
|
+
# Raised when digest generated by
|
7
|
+
# <tt>ActiveJob::QueueAdapters::ActiveElasticJobAdapter</tt> could not
|
8
|
+
# be verified.
|
9
|
+
class InvalidDigest < StandardError
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(secret)
|
13
|
+
@secret = secret
|
14
|
+
end
|
15
|
+
|
16
|
+
def verify(message, digest)
|
17
|
+
if message.nil? || message.blank? || digest.nil? || digest.blank?
|
18
|
+
return false
|
19
|
+
end
|
20
|
+
|
21
|
+
return ActiveSupport::SecurityUtils.secure_compare(
|
22
|
+
digest, generate_digest(message))
|
23
|
+
end
|
24
|
+
|
25
|
+
def verify!(message, digest)
|
26
|
+
raise InvalidDigest unless verify(message, digest)
|
27
|
+
end
|
28
|
+
|
29
|
+
def generate_digest(message)
|
30
|
+
require 'openssl' unless defined?(OpenSSL)
|
31
|
+
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, @secret, message)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require "action_dispatch"
|
2
|
+
|
3
|
+
module ActiveElasticJob
|
4
|
+
module Rack
|
5
|
+
# This middleware intercepts requests which are sent by the SQS daemon
|
6
|
+
# running in {Amazon Elastic Beanstalk worker environments}[http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/using-features-managing-env-tiers.html].
|
7
|
+
# It does this by looking at the +User-Agent+ header.
|
8
|
+
# Requesets from the SQS daemon are handled in two alternative cases:
|
9
|
+
#
|
10
|
+
# (1) the processed SQS message was originally triggered by a periodic task
|
11
|
+
# supported by Elastic Beanstalk's Periodic Task feature
|
12
|
+
#
|
13
|
+
# (2) the processed SQS message was queued by this gem representing an active job.
|
14
|
+
# In this case it verifies the digest which is sent along with a legit SQS
|
15
|
+
# message, and passed as an HTTP header in the resulting request.
|
16
|
+
# The digest is based on Rails' +secrets.secret_key_base+.
|
17
|
+
# Therefore, the application running in the web environment, which generates
|
18
|
+
# the digest, and the application running in the worker
|
19
|
+
# environment, which verifies the digest, have to use the *same*
|
20
|
+
# +secrets.secret_key_base+ setting.
|
21
|
+
class SqsMessageConsumer
|
22
|
+
OK_RESPONSE = [ '200'.freeze, { 'Content-Type'.freeze => 'text/plain'.freeze }, [ 'OK'.freeze ] ]
|
23
|
+
FORBIDDEN_RESPONSE = [
|
24
|
+
'403'.freeze,
|
25
|
+
{ 'Content-Type'.freeze => 'text/plain'.freeze },
|
26
|
+
[ 'Request forbidden!'.freeze ]
|
27
|
+
]
|
28
|
+
DOCKER_HOST_IP = /172.17.0.\d+/.freeze
|
29
|
+
|
30
|
+
def initialize(app) #:nodoc:
|
31
|
+
@app = app
|
32
|
+
end
|
33
|
+
|
34
|
+
def call(env) #:nodoc:
|
35
|
+
request = ActionDispatch::Request.new env
|
36
|
+
if enabled? && aws_sqsd?(request)
|
37
|
+
unless request.local? || sent_from_docker_host?(request)
|
38
|
+
return FORBIDDEN_RESPONSE
|
39
|
+
end
|
40
|
+
|
41
|
+
if periodic_task?(request)
|
42
|
+
execute_periodic_task(request)
|
43
|
+
return OK_RESPONSE
|
44
|
+
elsif originates_from_gem?(request)
|
45
|
+
begin
|
46
|
+
execute_job(request)
|
47
|
+
rescue ActiveElasticJob::MessageVerifier::InvalidDigest => e
|
48
|
+
return FORBIDDEN_RESPONSE
|
49
|
+
end
|
50
|
+
return OK_RESPONSE
|
51
|
+
end
|
52
|
+
end
|
53
|
+
@app.call(env)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def enabled?
|
59
|
+
Rails.application.config.active_elastic_job.process_jobs == true
|
60
|
+
end
|
61
|
+
|
62
|
+
def verify!(request)
|
63
|
+
@verifier ||= ActiveElasticJob::MessageVerifier.new(secret_key_base)
|
64
|
+
digest = request.headers['HTTP_X_AWS_SQSD_ATTR_MESSAGE_DIGEST'.freeze]
|
65
|
+
message = request.body_stream.read
|
66
|
+
request.body_stream.rewind
|
67
|
+
@verifier.verify!(message, digest)
|
68
|
+
end
|
69
|
+
|
70
|
+
def secret_key_base
|
71
|
+
config.secret_key_base
|
72
|
+
end
|
73
|
+
|
74
|
+
def config
|
75
|
+
Rails.application.config.active_elastic_job
|
76
|
+
end
|
77
|
+
|
78
|
+
def aws_sqsd?(request)
|
79
|
+
# Does not match against a Regexp
|
80
|
+
# in order to avoid performance penalties.
|
81
|
+
# Instead performs a simple string comparison.
|
82
|
+
# Benchmark runs showed an performance increase of
|
83
|
+
# up to 40%
|
84
|
+
current_user_agent = request.headers['User-Agent'.freeze]
|
85
|
+
return (current_user_agent.present? &&
|
86
|
+
current_user_agent.size >= 'aws-sqsd'.freeze.size &&
|
87
|
+
current_user_agent[0..('aws-sqsd'.freeze.size - 1)] == 'aws-sqsd'.freeze)
|
88
|
+
end
|
89
|
+
|
90
|
+
def periodic_tasks_route
|
91
|
+
@periodic_tasks_route ||= config.periodic_tasks_route
|
92
|
+
end
|
93
|
+
|
94
|
+
def periodic_task?(request)
|
95
|
+
!request.fullpath.nil? && request.fullpath[0..(periodic_tasks_route.size - 1)] == periodic_tasks_route
|
96
|
+
end
|
97
|
+
|
98
|
+
def execute_job(request)
|
99
|
+
verify!(request)
|
100
|
+
job = JSON.load(request.body)
|
101
|
+
ActiveJob::Base.execute(job)
|
102
|
+
end
|
103
|
+
|
104
|
+
def execute_periodic_task(request)
|
105
|
+
job_name = request.headers['X-Aws-Sqsd-Taskname']
|
106
|
+
job = job_name.constantize.new
|
107
|
+
job.perform_now
|
108
|
+
end
|
109
|
+
|
110
|
+
def originates_from_gem?(request)
|
111
|
+
if request.headers['HTTP_X_AWS_SQSD_ATTR_ORIGIN'.freeze] == ActiveElasticJob::ACRONYM
|
112
|
+
return true
|
113
|
+
elsif request.headers['HTTP_X_AWS_SQSD_ATTR_MESSAGE_DIGEST'.freeze] != nil
|
114
|
+
return true
|
115
|
+
else
|
116
|
+
return false
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def sent_from_docker_host?(request)
|
121
|
+
app_runs_in_docker_container? && request.remote_ip =~ DOCKER_HOST_IP
|
122
|
+
end
|
123
|
+
|
124
|
+
def app_runs_in_docker_container?
|
125
|
+
@app_in_docker_container ||= `[ -f /proc/1/cgroup ] && cat /proc/1/cgroup` =~ /(ecs|docker)/
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module ActiveElasticJob
|
2
|
+
class Railtie < Rails::Railtie
|
3
|
+
config.active_elastic_job = ActiveSupport::OrderedOptions.new
|
4
|
+
process_active_elastic_jobs = ENV['PROCESS_ACTIVE_ELASTIC_JOBS']
|
5
|
+
config.active_elastic_job.process_jobs = !process_active_elastic_jobs.nil? && process_active_elastic_jobs.downcase == 'true'
|
6
|
+
config.active_elastic_job.aws_credentials = lambda { Aws::InstanceProfileCredentials.new }
|
7
|
+
config.active_elastic_job.aws_region = ENV['AWS_REGION']
|
8
|
+
config.active_elastic_job.periodic_tasks_route = '/periodic_tasks'.freeze
|
9
|
+
|
10
|
+
initializer "active_elastic_job.insert_middleware" do |app|
|
11
|
+
if app.config.active_elastic_job.secret_key_base.blank?
|
12
|
+
app.config.active_elastic_job.secret_key_base = app.secrets[:secret_key_base]
|
13
|
+
end
|
14
|
+
|
15
|
+
if app.config.active_elastic_job.process_jobs == true
|
16
|
+
app.config.active_elastic_job.aws_credentials ||= lambda { Aws::InstanceProfileCredentials.new }
|
17
|
+
if app.config.force_ssl
|
18
|
+
app.config.middleware.insert_before(ActionDispatch::SSL,ActiveElasticJob::Rack::SqsMessageConsumer)
|
19
|
+
else
|
20
|
+
app.config.middleware.use(ActiveElasticJob::Rack::SqsMessageConsumer)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,228 @@
|
|
1
|
+
module ActiveJob
|
2
|
+
module QueueAdapters
|
3
|
+
# == Active Elastic Job adapter for Active Job
|
4
|
+
#
|
5
|
+
# Active Elastic Job provides (1) an adapter (this class) for Rails'
|
6
|
+
# Active Job framework and (2) a Rack middleware to process job requests,
|
7
|
+
# which are sent by the SQS daemon running in {Amazon Elastic Beanstalk worker
|
8
|
+
# environments}[http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/using-features-managing-env-tiers.html].
|
9
|
+
#
|
10
|
+
# This adapter serializes job objects and sends them as a message to an
|
11
|
+
# Amazon SQS queue specified by the job's queue name,
|
12
|
+
# see <tt>ActiveJob::Base.queue_as</tt>
|
13
|
+
#
|
14
|
+
# To use Active Elastic Job, set the queue_adapter config
|
15
|
+
# to +:active_elastic_job+.
|
16
|
+
#
|
17
|
+
# Rails.application.config.active_job.queue_adapter = :active_elastic_job
|
18
|
+
class ActiveElasticJobAdapter
|
19
|
+
MAX_MESSAGE_SIZE = (256 * 1024)
|
20
|
+
MAX_DELAY_IN_MINUTES = 15
|
21
|
+
|
22
|
+
extend ActiveElasticJob::MD5MessageDigestCalculation
|
23
|
+
|
24
|
+
class Error < RuntimeError; end;
|
25
|
+
|
26
|
+
# Raised when job exceeds 256 KB in its serialized form. The limit is
|
27
|
+
# imposed by Amazon SQS.
|
28
|
+
class SerializedJobTooBig < Error
|
29
|
+
def initialize(serialized_job)
|
30
|
+
super(<<-MSG)
|
31
|
+
The job contains #{serialized_job.bytesize} bytes in its serialized form,
|
32
|
+
which exceeds the allowed maximum of #{MAX_MESSAGE_SIZE} bytes imposed by Amazon SQS.
|
33
|
+
MSG
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Raised when job queue does not exist. The job queue is determined by
|
38
|
+
# <tt>ActiveJob::Base.queue_as</tt>. You can either: (1) create a new
|
39
|
+
# Amazon SQS queue and attach a worker environment to it, or (2) select a
|
40
|
+
# different queue for your jobs.
|
41
|
+
#
|
42
|
+
# Example:
|
43
|
+
# * Open your AWS console and create an SQS queue named +high_priority+ in
|
44
|
+
# the same AWS region of your Elastic Beanstalk environments.
|
45
|
+
# * Queue your jobs accordingly:
|
46
|
+
#
|
47
|
+
# class MyJob < ActiveJob::Base
|
48
|
+
# queue_as :high_priority
|
49
|
+
# #..
|
50
|
+
# end
|
51
|
+
class NonExistentQueue < Error
|
52
|
+
def initialize(queue_name, aws_region)
|
53
|
+
|
54
|
+
super(<<-MSG)
|
55
|
+
The job is bound to queue at #{queue_name}.
|
56
|
+
Unfortunately a queue with this name does not exist in this
|
57
|
+
region. Either create an Amazon SQS queue named #{queue_name} -
|
58
|
+
you can do this in AWS console, make sure to select region
|
59
|
+
'#{aws_region}' - or you
|
60
|
+
select another queue for your jobs.
|
61
|
+
MSG
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Raised when calculated MD5 digest does not match the MD5 Digest
|
66
|
+
# of the response from Amazon SQS.
|
67
|
+
class MD5MismatchError < Error
|
68
|
+
def initialize(message_id, calculated, returned)
|
69
|
+
|
70
|
+
super(<<-MSG)
|
71
|
+
MD5 '#{returned}' returned by Amazon SQS does not match the
|
72
|
+
calculation on the original request which was '#{calculated}'.
|
73
|
+
The message with Message ID #{message_id} sent to SQS might be
|
74
|
+
corrupted.
|
75
|
+
MSG
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Raised when the delay is longer than the MAX_DELAY_IN_MINUTES
|
80
|
+
class DelayTooLong < RangeError
|
81
|
+
def initialize()
|
82
|
+
super(<<-MSG)
|
83
|
+
Jobs cannot be scheduled more than #{MAX_DELAY_IN_MINUTES} minutes
|
84
|
+
into the future.
|
85
|
+
See http://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_SendMessage.html
|
86
|
+
for further details!
|
87
|
+
MSG
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def enqueue(job) #:nodoc:
|
92
|
+
self.class.enqueue job
|
93
|
+
end
|
94
|
+
|
95
|
+
def enqueue_at(job, timestamp) #:nodoc:
|
96
|
+
self.class.enqueue_at(job, timestamp)
|
97
|
+
end
|
98
|
+
|
99
|
+
class << self
|
100
|
+
def enqueue(job) #:nodoc:
|
101
|
+
enqueue_at(job, Time.now)
|
102
|
+
end
|
103
|
+
|
104
|
+
def enqueue_at(job, timestamp) #:nodoc:
|
105
|
+
serialized_job = JSON.dump(job.serialize)
|
106
|
+
check_job_size!(serialized_job)
|
107
|
+
message = build_message(job.queue_name, serialized_job, timestamp)
|
108
|
+
resp = aws_sqs_client.send_message(message)
|
109
|
+
unless aws_client_verifies_md5_digests?
|
110
|
+
verify_md5_digests!(
|
111
|
+
resp,
|
112
|
+
message[:message_body],
|
113
|
+
message[:message_attributes])
|
114
|
+
end
|
115
|
+
job.provider_job_id = resp.message_id
|
116
|
+
rescue Aws::SQS::Errors::NonExistentQueue => e
|
117
|
+
unless @queue_urls[job.queue_name.to_s].nil?
|
118
|
+
@queue_urls[job.queue_name.to_s] = nil
|
119
|
+
retry
|
120
|
+
end
|
121
|
+
raise NonExistentQueue.new(job, aws_region)
|
122
|
+
rescue Aws::Errors::ServiceError => e
|
123
|
+
raise Error, "Could not enqueue job, #{e.message}"
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
def aws_client_verifies_md5_digests?
|
129
|
+
Gem::Version.new(Aws::CORE_GEM_VERSION) >= Gem::Version.new('2.2.19'.freeze)
|
130
|
+
end
|
131
|
+
|
132
|
+
def build_message(queue_name, serialized_job, timestamp)
|
133
|
+
{
|
134
|
+
queue_url: queue_url(queue_name),
|
135
|
+
message_body: serialized_job,
|
136
|
+
delay_seconds: calculate_delay(timestamp),
|
137
|
+
message_attributes: {
|
138
|
+
"message-digest".freeze => {
|
139
|
+
string_value: message_digest(serialized_job),
|
140
|
+
data_type: "String".freeze
|
141
|
+
},
|
142
|
+
origin: {
|
143
|
+
string_value: ActiveElasticJob::ACRONYM,
|
144
|
+
data_type: "String".freeze
|
145
|
+
}
|
146
|
+
}
|
147
|
+
}
|
148
|
+
end
|
149
|
+
|
150
|
+
def queue_url(queue_name)
|
151
|
+
cache_key = queue_name.to_s
|
152
|
+
@queue_urls ||= { }
|
153
|
+
return @queue_urls[cache_key] if @queue_urls[cache_key]
|
154
|
+
resp = aws_sqs_client.get_queue_url(queue_name: queue_name.to_s)
|
155
|
+
@queue_urls[cache_key] = resp.queue_url
|
156
|
+
rescue Aws::SQS::Errors::NonExistentQueue => e
|
157
|
+
raise NonExistentQueue.new(queue_name, aws_region)
|
158
|
+
end
|
159
|
+
|
160
|
+
def calculate_delay(timestamp)
|
161
|
+
delay = (timestamp - Time.current.to_f).to_i + 1
|
162
|
+
if delay > MAX_DELAY_IN_MINUTES.minutes
|
163
|
+
raise DelayTooLong.new
|
164
|
+
end
|
165
|
+
delay = 0 if delay < 0
|
166
|
+
delay
|
167
|
+
end
|
168
|
+
|
169
|
+
def check_job_size!(serialized_job)
|
170
|
+
if serialized_job.bytesize > MAX_MESSAGE_SIZE
|
171
|
+
raise SerializedJobTooBig, serialized_job
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def aws_sqs_client
|
176
|
+
options = {
|
177
|
+
credentials: aws_sqs_client_credentials,
|
178
|
+
region: aws_region
|
179
|
+
}
|
180
|
+
endpoint = Rails.application.config.active_elastic_job.endpoint
|
181
|
+
options[:endpoint] = endpoint if endpoint.present?
|
182
|
+
@aws_sqs_client ||= Aws::SQS::Client.new(options)
|
183
|
+
end
|
184
|
+
|
185
|
+
def aws_sqs_client_credentials
|
186
|
+
@aws_credentials ||= if config.aws_credentials.kind_of?(Proc)
|
187
|
+
config.aws_credentials.call
|
188
|
+
else
|
189
|
+
config.aws_credentials
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def aws_region
|
194
|
+
config.aws_region
|
195
|
+
end
|
196
|
+
|
197
|
+
def config
|
198
|
+
Rails.application.config.active_elastic_job
|
199
|
+
end
|
200
|
+
|
201
|
+
def message_digest(messsage_body)
|
202
|
+
@verifier ||= ActiveElasticJob::MessageVerifier.new(secret_key_base)
|
203
|
+
@verifier.generate_digest(messsage_body)
|
204
|
+
end
|
205
|
+
|
206
|
+
def verify_md5_digests!(response, messsage_body, message_attributes)
|
207
|
+
calculated = md5_of_message_body(messsage_body)
|
208
|
+
returned = response.md5_of_message_body
|
209
|
+
if calculated != returned
|
210
|
+
raise MD5MismatchError.new response.message_id, calculated, returned
|
211
|
+
end
|
212
|
+
|
213
|
+
if message_attributes
|
214
|
+
calculated = md5_of_message_attributes(message_attributes)
|
215
|
+
returned = response.md5_of_message_attributes
|
216
|
+
if calculated != returned
|
217
|
+
raise MD5MismatchError.new response.message_id, calculated, returned
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def secret_key_base
|
223
|
+
config.secret_key_base
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
metadata
ADDED
@@ -0,0 +1,208 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: active_elastic_job_gunner
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 3.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Cody Swann
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-02-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: aws-sdk-sqs
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '5.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '5.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sqlite3
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.4'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.4'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: dotenv
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: fuubar
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rdoc
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: byebug
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: benchmark-ips
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: climate_control
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
description: Run background jobs / tasks of Rails applications deployed in Amazon
|
168
|
+
Elastic Beanstalk environments. Active Elastic Job is an Active Job backend which
|
169
|
+
is easy to setup. No need for customised container commands or other workarounds.
|
170
|
+
email:
|
171
|
+
- cody@gunnertech.com
|
172
|
+
executables: []
|
173
|
+
extensions: []
|
174
|
+
extra_rdoc_files: []
|
175
|
+
files:
|
176
|
+
- active-elastic-job.gemspec
|
177
|
+
- lib/active_elastic_job.rb
|
178
|
+
- lib/active_elastic_job/md5_message_digest_calculation.rb
|
179
|
+
- lib/active_elastic_job/message_verifier.rb
|
180
|
+
- lib/active_elastic_job/rack/sqs_message_consumer.rb
|
181
|
+
- lib/active_elastic_job/railtie.rb
|
182
|
+
- lib/active_elastic_job/version.rb
|
183
|
+
- lib/active_job/queue_adapters/active_elastic_job_adapter.rb
|
184
|
+
homepage: https://github.com/gunnertech/active-elastic-job
|
185
|
+
licenses:
|
186
|
+
- MIT
|
187
|
+
metadata: {}
|
188
|
+
post_install_message:
|
189
|
+
rdoc_options: []
|
190
|
+
require_paths:
|
191
|
+
- lib
|
192
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
193
|
+
requirements:
|
194
|
+
- - ">="
|
195
|
+
- !ruby/object:Gem::Version
|
196
|
+
version: '2.5'
|
197
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - ">="
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: '0'
|
202
|
+
requirements: []
|
203
|
+
rubygems_version: 3.1.4
|
204
|
+
signing_key:
|
205
|
+
specification_version: 4
|
206
|
+
summary: Active Elastic Job is a simple to use Active Job backend for Rails applications
|
207
|
+
deployed on the Amazon Elastic Beanstalk platform.
|
208
|
+
test_files: []
|