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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a15dee97e68451ab5d4e68fdc849c8898c40ebd6
4
- data.tar.gz: 76a7503a7a8d3385b71675587a7094224025ade3
3
+ metadata.gz: be0c3eef5f0c1b0aa66524d644f7f855f8c3a99d
4
+ data.tar.gz: af9a0f1e16545725b593d01a42743769f3f9d621
5
5
  SHA512:
6
- metadata.gz: fa4289336749473efac64d3f6bb09751ddad6d7eee12fe977ee0d9bb84c888caed6711ba0cc01e5af8040943bcabaa32ef6b4b2a39c383df90a93f3a2e5003dd
7
- data.tar.gz: 397a332e277690241346c84c3fda6fc13d3010d902c6b7406b16b2841139f99e727c5baeb42d54808dde0506bc9ea5e930ddffa02ea6bb2002eac0ea70ffeab3
6
+ metadata.gz: 82c4cda1a5c365a803ee5e3d0948a22d9a372d77f354671bfd2e50197db807ee424f27bb5fb1474c6a6130c5a1b50279dd1e7d60fc5b8c47ee101f326ca933ed
7
+ data.tar.gz: 39aee6ee9c19d0fac3b263045211fb420643c93fb7bb65f249113c5d53d89fc1611ea171d717a9b7e288346fa9a1a8efa6f8c8fcc9d40fa1edb7437e3ee013d5
@@ -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::VERSION
10
+ spec.version = ActiveElasticJob.version
11
11
  spec.authors = ['Tawan Sierek']
12
12
  spec.email = ['tawan@sierek.com']
13
- spec.description = 'Active Elastic Job is a simple to use Active Job backend for Rails applications deployed on the Amazon Elastic Beanstalk platform.'
14
- spec.summary = spec.description
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
 
@@ -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
@@ -13,7 +13,7 @@ module ActiveElasticJob
13
13
  @secret = secret
14
14
  end
15
15
 
16
- def verify(message, digest)
16
+ def verify!(message, digest)
17
17
  if message.nil? || message.blank? || digest.nil? || digest.blank?
18
18
  raise InvalidDigest
19
19
  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 = '1.2.1'
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
- class Error < StandardError; end;
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(job)
50
+ def initialize(queue_name)
49
51
  msg = <<-MSG
50
- The job is bound to queue at #{job.queue_name}. Unfortunately a queue
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 #{job.queue_name} - you can do this in AWS console, make sure to select
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
- aws_sqs_client.send_message(
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.2.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-01-20 00:00:00.000000000 Z
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: Active Elastic Job is a simple to use Active Job backend for Rails applications
42
- deployed on the Amazon Elastic Beanstalk platform.
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