active_elastic_job 1.2.1 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/active-elastic-job.gemspec +3 -3
- data/lib/active_elastic_job.rb +1 -0
- data/lib/active_elastic_job/md5_message_digest_calculation.rb +79 -0
- data/lib/active_elastic_job/message_verifier.rb +1 -1
- data/lib/active_elastic_job/rack/sqs_message_consumer.rb +8 -4
- data/lib/active_elastic_job/version.rb +16 -1
- data/lib/active_job/queue_adapters/active_elastic_job_adapter.rb +61 -15
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: be0c3eef5f0c1b0aa66524d644f7f855f8c3a99d
|
4
|
+
data.tar.gz: af9a0f1e16545725b593d01a42743769f3f9d621
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 82c4cda1a5c365a803ee5e3d0948a22d9a372d77f354671bfd2e50197db807ee424f27bb5fb1474c6a6130c5a1b50279dd1e7d60fc5b8c47ee101f326ca933ed
|
7
|
+
data.tar.gz: 39aee6ee9c19d0fac3b263045211fb420643c93fb7bb65f249113c5d53d89fc1611ea171d717a9b7e288346fa9a1a8efa6f8c8fcc9d40fa1edb7437e3ee013d5
|
data/active-elastic-job.gemspec
CHANGED
@@ -7,11 +7,11 @@ require 'active_elastic_job/version'
|
|
7
7
|
Gem::Specification.new do |spec|
|
8
8
|
spec.platform = Gem::Platform::RUBY
|
9
9
|
spec.name = 'active_elastic_job'
|
10
|
-
spec.version = ActiveElasticJob
|
10
|
+
spec.version = ActiveElasticJob.version
|
11
11
|
spec.authors = ['Tawan Sierek']
|
12
12
|
spec.email = ['tawan@sierek.com']
|
13
|
-
spec.
|
14
|
-
spec.
|
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
15
|
spec.license = 'MIT'
|
16
16
|
spec.homepage = 'https://github.com/tawan/active-elastic-job'
|
17
17
|
|
data/lib/active_elastic_job.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'aws-sdk-core'
|
2
2
|
require 'active_elastic_job/version'
|
3
|
+
require 'active_elastic_job/md5_message_digest_calculation'
|
3
4
|
require 'active_job/queue_adapters/active_elastic_job_adapter'
|
4
5
|
require 'active_elastic_job/rack/sqs_message_consumer'
|
5
6
|
require 'active_elastic_job/message_verifier'
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'digest'
|
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
|
+
CHARSET_ENCODING = Encoding::UTF_8
|
33
|
+
|
34
|
+
# Returns MD5 digest of +message_body+.
|
35
|
+
def md5_of_message_body(message_body)
|
36
|
+
OpenSSL::Digest::MD5.hexdigest(message_body)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns MD5 digest of +message_attributes+.
|
40
|
+
#
|
41
|
+
# The calculation follows the official algorithm which
|
42
|
+
# is specified by Amazon.
|
43
|
+
def md5_of_message_attributes(message_attributes)
|
44
|
+
encoded = message_attributes.each.with_object({ }) do |(name, v), hash|
|
45
|
+
hash[name.to_s] = ""
|
46
|
+
data_type = v['data_type'] || v[:data_type]
|
47
|
+
|
48
|
+
hash[name.to_s] << encode_length_and_bytes(name.to_s) <<
|
49
|
+
encode_length_and_bytes(data_type) <<
|
50
|
+
[ TRANSPORT_TYPE_ENCODINGS[data_type] ].pack('C')
|
51
|
+
|
52
|
+
if string_value = v['string_value'] || v[:string_value]
|
53
|
+
hash[name.to_s] << encode_length_and_string(string_value)
|
54
|
+
elsif binary_value = v['binary_value'] || v[:binary_value]
|
55
|
+
hash[name.to_s] << encode_length_and_bytes(binary_value)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
buffer = encoded.keys.sort.reduce("") do |b, name|
|
60
|
+
b << encoded[name]
|
61
|
+
end
|
62
|
+
OpenSSL::Digest::MD5.hexdigest(buffer)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def encode_length_and_string(string)
|
68
|
+
return '' if string.nil?
|
69
|
+
string = String.new(string)
|
70
|
+
string.encode!(CHARSET_ENCODING)
|
71
|
+
encode_length_and_bytes(string)
|
72
|
+
end
|
73
|
+
|
74
|
+
def encode_length_and_bytes(bytes)
|
75
|
+
return '' if bytes.nil?
|
76
|
+
[ bytes.bytesize, bytes ].pack("L>a#{bytes.bytesize}")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -21,9 +21,9 @@ module ActiveElasticJob
|
|
21
21
|
|
22
22
|
def call(env) #:nodoc:
|
23
23
|
request = ActionDispatch::Request.new env
|
24
|
-
if aws_sqsd?(request)
|
24
|
+
if aws_sqsd?(request) && originates_from_gem?(request)
|
25
25
|
begin
|
26
|
-
verify(request)
|
26
|
+
verify!(request)
|
27
27
|
job = JSON.load(request.body)
|
28
28
|
ActiveJob::Base.execute(job)
|
29
29
|
rescue ActiveElasticJob::MessageVerifier::InvalidDigest => e
|
@@ -38,11 +38,11 @@ module ActiveElasticJob
|
|
38
38
|
|
39
39
|
private
|
40
40
|
|
41
|
-
def verify(request)
|
41
|
+
def verify!(request)
|
42
42
|
secret_key_base = Rails.application.secrets[:secret_key_base]
|
43
43
|
@verifier ||= ActiveElasticJob::MessageVerifier.new(secret_key_base)
|
44
44
|
digest = request.headers['HTTP_X_AWS_SQSD_ATTR_MESSAGE_DIGEST']
|
45
|
-
@verifier.verify(request.body.string, digest)
|
45
|
+
@verifier.verify!(request.body.string, digest)
|
46
46
|
end
|
47
47
|
|
48
48
|
def aws_sqsd?(request)
|
@@ -56,6 +56,10 @@ module ActiveElasticJob
|
|
56
56
|
current_user_agent.size >= USER_AGENT_PREFIX.size &&
|
57
57
|
current_user_agent[0..(USER_AGENT_PREFIX.size - 1)] == USER_AGENT_PREFIX)
|
58
58
|
end
|
59
|
+
|
60
|
+
def originates_from_gem?(request)
|
61
|
+
request.headers['HTTP_X_AWS_SQSD_ATTR_ORIGIN'] == 'AEJ'
|
62
|
+
end
|
59
63
|
end
|
60
64
|
end
|
61
65
|
end
|
@@ -1,3 +1,18 @@
|
|
1
1
|
module ActiveElasticJob
|
2
|
-
VERSION
|
2
|
+
module VERSION
|
3
|
+
MAJOR = 1
|
4
|
+
MINOR = 3
|
5
|
+
TINY = 0
|
6
|
+
PRE = nil
|
7
|
+
|
8
|
+
STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
|
9
|
+
|
10
|
+
def self.to_s
|
11
|
+
STRING
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.version
|
16
|
+
VERSION::STRING
|
17
|
+
end
|
3
18
|
end
|
@@ -16,7 +16,9 @@ module ActiveJob
|
|
16
16
|
class ActiveElasticJobAdapter
|
17
17
|
MAX_MESSAGE_SIZE = (256 * 1024)
|
18
18
|
|
19
|
-
|
19
|
+
extend ActiveElasticJob::MD5MessageDigestCalculation
|
20
|
+
|
21
|
+
class Error < RuntimeError; end;
|
20
22
|
|
21
23
|
# Raised when job exceeds 256 KB in its serialized form. The limit is
|
22
24
|
# imposed by Amazon SQS.
|
@@ -45,48 +47,79 @@ which exceeds the allowed maximum of #{MAX_MESSAGE_SIZE} bytes imposed by Amazon
|
|
45
47
|
# #..
|
46
48
|
# end
|
47
49
|
class NonExistentQueue < Error
|
48
|
-
def initialize(
|
50
|
+
def initialize(queue_name)
|
49
51
|
msg = <<-MSG
|
50
|
-
The job is bound to queue at #{
|
52
|
+
The job is bound to queue at #{queue_name}. Unfortunately a queue
|
51
53
|
with this name does not exist in this region. Either create an Amazon SQS queue
|
52
|
-
named #{
|
53
|
-
region '#{ENV['AWS_REGION']}' - or you select another queue for
|
54
|
-
#{job.class.name} jobs.
|
54
|
+
named #{queue_name} - you can do this in AWS console, make sure to select
|
55
|
+
region '#{ENV['AWS_REGION']}' - or you select another queue for your jobs.
|
55
56
|
MSG
|
56
57
|
super msg
|
57
58
|
end
|
58
59
|
end
|
59
60
|
|
61
|
+
# Raised when calculated MD5 digest does not match the MD5 Digest
|
62
|
+
# of the response from Amazon SQS.
|
63
|
+
class MD5MismatchError < Error
|
64
|
+
def initialize( message_id)
|
65
|
+
msg = <<-MSG
|
66
|
+
MD5 returned by Amazon SQS does not match the calculation on the original request.
|
67
|
+
The message with Message ID #{message_id} sent to SQS might be corrupted.
|
68
|
+
MSG
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
60
72
|
class << self
|
61
73
|
def enqueue(job) #:nodoc:
|
62
74
|
enqueue_at(job, Time.now)
|
63
75
|
end
|
64
76
|
|
65
77
|
def enqueue_at(job, timestamp) #:nodoc:
|
66
|
-
queue_url = nil
|
67
|
-
begin
|
68
|
-
resp = aws_sqs_client.get_queue_url(queue_name: job.queue_name.to_s)
|
69
|
-
queue_url = resp.queue_url
|
70
|
-
rescue Aws::SQS::Errors::NonExistentQueue => e
|
71
|
-
raise NonExistentQueue, job
|
72
|
-
end
|
73
78
|
serialized_job = JSON.dump(job.serialize)
|
74
79
|
check_job_size!(serialized_job)
|
75
|
-
|
76
|
-
queue_url: queue_url,
|
80
|
+
message = {
|
81
|
+
queue_url: queue_url(job.queue_name),
|
77
82
|
message_body: serialized_job,
|
78
83
|
delay_seconds: calculate_delay(timestamp),
|
79
84
|
message_attributes: {
|
80
85
|
"message_digest" => {
|
81
86
|
string_value: message_digest(serialized_job),
|
82
87
|
data_type: "String"
|
88
|
+
},
|
89
|
+
"origin" => {
|
90
|
+
string_value: "AEJ",
|
91
|
+
data_type: "String"
|
83
92
|
}
|
84
93
|
}
|
94
|
+
}
|
95
|
+
resp = aws_sqs_client.send_message(message)
|
96
|
+
verify_md5_digests!(
|
97
|
+
resp,
|
98
|
+
message[:message_body],
|
99
|
+
message[:message_attributes]
|
85
100
|
)
|
101
|
+
rescue Aws::SQS::Errors::NonExistentQueue => e
|
102
|
+
unless @queue_urls[job.queue_name.to_s].nil?
|
103
|
+
@queue_urls[job.queue_name.to_s] = nil
|
104
|
+
retry
|
105
|
+
end
|
106
|
+
raise NonExistentQueue, job
|
107
|
+
rescue Aws::Errors::ServiceError => e
|
108
|
+
raise Error, "Could not enqueue job, #{e.message}"
|
86
109
|
end
|
87
110
|
|
88
111
|
private
|
89
112
|
|
113
|
+
def queue_url(queue_name)
|
114
|
+
cache_key = queue_name.to_s
|
115
|
+
@queue_urls ||= { }
|
116
|
+
return @queue_urls[cache_key] if @queue_urls[cache_key]
|
117
|
+
resp = aws_sqs_client.get_queue_url(queue_name: queue_name.to_s)
|
118
|
+
@queue_urls[cache_key] = resp.queue_url
|
119
|
+
rescue Aws::SQS::Errors::NonExistentQueue => e
|
120
|
+
raise NonExistentQueue, queue_name
|
121
|
+
end
|
122
|
+
|
90
123
|
def calculate_delay(timestamp)
|
91
124
|
delay = (timestamp - Time.current.to_f).to_i + 1
|
92
125
|
if delay > 15.minutes
|
@@ -119,6 +152,19 @@ for further details!
|
|
119
152
|
verifier = ActiveElasticJob::MessageVerifier.new(secret_key_base)
|
120
153
|
verifier.generate_digest(messsage_body)
|
121
154
|
end
|
155
|
+
|
156
|
+
def verify_md5_digests!(response, messsage_body, message_attributes = nil)
|
157
|
+
if md5_of_message_body(messsage_body) != response.md5_of_message_body
|
158
|
+
raise MD5MismatchError, response.message_id
|
159
|
+
end
|
160
|
+
|
161
|
+
if message_attributes
|
162
|
+
if md5_of_message_attributes(message_attributes) !=
|
163
|
+
response.md5_of_message_attributes
|
164
|
+
raise MD5MismatchError, response.message_id
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
122
168
|
end
|
123
169
|
end
|
124
170
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_elastic_job
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tawan Sierek
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-02-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sdk
|
@@ -38,8 +38,9 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '4.2'
|
41
|
-
description:
|
42
|
-
|
41
|
+
description: Run background jobs / tasks of Rails applications deployed in Amazon
|
42
|
+
Elastic Beanstalk environments. Active Elastic Job is an Active Job backend which
|
43
|
+
is easy to setup. No need for customised container commands or other workarounds.
|
43
44
|
email:
|
44
45
|
- tawan@sierek.com
|
45
46
|
executables: []
|
@@ -48,6 +49,7 @@ extra_rdoc_files: []
|
|
48
49
|
files:
|
49
50
|
- active-elastic-job.gemspec
|
50
51
|
- lib/active_elastic_job.rb
|
52
|
+
- lib/active_elastic_job/md5_message_digest_calculation.rb
|
51
53
|
- lib/active_elastic_job/message_verifier.rb
|
52
54
|
- lib/active_elastic_job/rack/sqs_message_consumer.rb
|
53
55
|
- lib/active_elastic_job/railtie.rb
|