active_elastic_job 2.0.0 → 3.1.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
- SHA1:
3
- metadata.gz: d8e455f174d4846927c18875dd2e1f645a87f870
4
- data.tar.gz: 3cd316cd0b53a07cbf66234ccbb2ec71f5fe6e61
2
+ SHA256:
3
+ metadata.gz: 7314269003451ee2d9b211948b70a351621f340395ffddcf47e7f8362a5e4d4e
4
+ data.tar.gz: 3b75e291c60225b28aaf879143e49ba16955bd3faed096a9b1c7dc6804aa40f8
5
5
  SHA512:
6
- metadata.gz: d7ef1844c86ce29e4a93e0184eca63e879b0d43bcc08d1e35ab7573a08975e6943f8eb489028d4f47960780ab60743cbefc760961dd25b3cf49ed49500a00d9b
7
- data.tar.gz: e0e5d7390144a3ebf53c756dc7a56436e14dabf4a6e2e37765fefff5625568a93669b7c8623b1f3aa1124c148938468e46d9059d59f604d2f85cb89b9d917798
6
+ metadata.gz: 0fe756ae355407bb1f923850efbb53e6587dcb136aee42b64467780ab73bc1935210494039a88e02ea7d525846bdcd90ac398136ca33c6389dae66be975d417b
7
+ data.tar.gz: c6cb04a7f3acee2cce0991d66b665ed544d42903f9b89a124705717af8b3787fd6b4e69de60d0d1c78ff03bde436377d803fc169f3accff0d6f61f50297e1e98
@@ -8,20 +8,31 @@ Gem::Specification.new do |spec|
8
8
  spec.platform = Gem::Platform::RUBY
9
9
  spec.name = 'active_elastic_job'
10
10
  spec.version = ActiveElasticJob.version
11
- spec.authors = ['Tawan Sierek']
12
- spec.email = ['tawan@sierek.com']
11
+ spec.authors = ['Tawan Sierek', 'Joey Paris']
12
+ spec.email = ['tawan@sierek.com', 'mail@joeyparis.me']
13
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
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
- spec.homepage = 'https://github.com/tawan/active-elastic-job'
16
+ spec.homepage = 'https://github.com/active-elastic-job/active-elastic-job'
17
17
 
18
18
  spec.files = Dir.glob('lib/**/*') + [ 'active-elastic-job.gemspec' ]
19
19
  spec.executables = []
20
20
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
21
  spec.require_paths = ['lib']
22
22
 
23
- spec.required_ruby_version = '>= 1.9.3'
23
+ spec.required_ruby_version = '>= 2.5'
24
24
 
25
- spec.add_dependency 'aws-sdk', '~> 2'
26
- spec.add_dependency 'rails', '>= 4.2'
25
+ spec.add_dependency 'aws-sdk-sqs', '~> 1'
26
+ spec.add_dependency 'rails', '>= 5.0', '<7'
27
+
28
+ spec.add_development_dependency 'benchmark-ips', '~> 2.8'
29
+ spec.add_development_dependency 'bundler', '~> 2.2'
30
+ spec.add_development_dependency 'byebug', '~> 11.1'
31
+ spec.add_development_dependency 'climate_control', '~> 0.2'
32
+ spec.add_development_dependency 'dotenv', '~> 2.7'
33
+ spec.add_development_dependency 'fuubar', '~> 2.5'
34
+ spec.add_development_dependency 'rdoc', '~> 6.3'
35
+ spec.add_development_dependency 'rspec', '~> 3.4'
36
+ spec.add_development_dependency 'sqlite3', '~> 1.4'
37
+ spec.add_development_dependency 'amazing_print', '~> 1.2'
27
38
  end
@@ -25,7 +25,10 @@ module ActiveElasticJob
25
25
  { 'Content-Type'.freeze => 'text/plain'.freeze },
26
26
  [ 'Request forbidden!'.freeze ]
27
27
  ]
28
- DOCKER_HOST_IP = '172.17.0.1'.freeze
28
+
29
+ # 172.17.0.x is the default for Docker
30
+ # 172.18.0.x is the default for the bridge network of Docker Compose
31
+ DOCKER_HOST_IP = /172.1(7|8).0.\d+/.freeze
29
32
 
30
33
  def initialize(app) #:nodoc:
31
34
  @app = app
@@ -33,7 +36,7 @@ module ActiveElasticJob
33
36
 
34
37
  def call(env) #:nodoc:
35
38
  request = ActionDispatch::Request.new env
36
- if enabled? && aws_sqsd?(request)
39
+ if enabled? && (aws_sqsd?(request) || sqsd?(request))
37
40
  unless request.local? || sent_from_docker_host?(request)
38
41
  return FORBIDDEN_RESPONSE
39
42
  end
@@ -47,7 +50,7 @@ module ActiveElasticJob
47
50
  rescue ActiveElasticJob::MessageVerifier::InvalidDigest => e
48
51
  return FORBIDDEN_RESPONSE
49
52
  end
50
- return OK_RESPONSE
53
+ return OK_RESPONSE
51
54
  end
52
55
  end
53
56
  @app.call(env)
@@ -87,6 +90,18 @@ module ActiveElasticJob
87
90
  current_user_agent[0..('aws-sqsd'.freeze.size - 1)] == 'aws-sqsd'.freeze)
88
91
  end
89
92
 
93
+ def sqsd?(request)
94
+ # Does not match against a Regexp
95
+ # in order to avoid performance penalties.
96
+ # Instead performs a simple string comparison.
97
+ # Benchmark runs showed an performance increase of
98
+ # up to 40%
99
+ current_user_agent = request.headers['User-Agent'.freeze]
100
+ return (current_user_agent.present? &&
101
+ current_user_agent.size >= 'sqsd'.freeze.size &&
102
+ current_user_agent[0..('sqsd'.freeze.size - 1)] == 'sqsd'.freeze)
103
+ end
104
+
90
105
  def periodic_tasks_route
91
106
  @periodic_tasks_route ||= config.periodic_tasks_route
92
107
  end
@@ -118,11 +133,15 @@ module ActiveElasticJob
118
133
  end
119
134
 
120
135
  def sent_from_docker_host?(request)
121
- app_runs_in_docker_container? && request.remote_ip == DOCKER_HOST_IP
136
+ app_runs_in_docker_container? && ip_originates_from_docker?(request)
137
+ end
138
+
139
+ def ip_originates_from_docker?(request)
140
+ (request.remote_ip =~ DOCKER_HOST_IP).present? or (request.remote_addr =~ DOCKER_HOST_IP).present?
122
141
  end
123
142
 
124
143
  def app_runs_in_docker_container?
125
- @app_in_docker_container ||= `[ -f /proc/1/cgroup ] && cat /proc/1/cgroup` =~ /docker/
144
+ (`[ -f /proc/1/cgroup ] && cat /proc/1/cgroup` =~ /(ecs|docker)/).present?
126
145
  end
127
146
  end
128
147
  end
@@ -1,8 +1,10 @@
1
1
  module ActiveElasticJob
2
2
  class Railtie < Rails::Railtie
3
3
  config.active_elastic_job = ActiveSupport::OrderedOptions.new
4
- config.active_elastic_job.process_jobs = ENV['PROCESS_ACTIVE_ELASTIC_JOBS'] == 'true' || false
5
- config.active_elastic_job.aws_credentials = Aws::InstanceProfileCredentials.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']
6
8
  config.active_elastic_job.periodic_tasks_route = '/periodic_tasks'.freeze
7
9
 
8
10
  initializer "active_elastic_job.insert_middleware" do |app|
@@ -11,6 +13,7 @@ module ActiveElasticJob
11
13
  end
12
14
 
13
15
  if app.config.active_elastic_job.process_jobs == true
16
+ app.config.active_elastic_job.aws_credentials ||= lambda { Aws::InstanceProfileCredentials.new }
14
17
  if app.config.force_ssl
15
18
  app.config.middleware.insert_before(ActionDispatch::SSL,ActiveElasticJob::Rack::SqsMessageConsumer)
16
19
  else
@@ -1,7 +1,7 @@
1
1
  module ActiveElasticJob
2
2
  module VERSION
3
- MAJOR = 2
4
- MINOR = 0
3
+ MAJOR = 3
4
+ MINOR = 1
5
5
  TINY = 0
6
6
  PRE = nil
7
7
 
@@ -1,4 +1,4 @@
1
- require 'aws-sdk-core'
1
+ require 'aws-sdk-sqs'
2
2
  require 'active_elastic_job/version'
3
3
  require 'active_elastic_job/md5_message_digest_calculation'
4
4
  require 'active_job/queue_adapters/active_elastic_job_adapter'
@@ -27,9 +27,8 @@ module ActiveJob
27
27
  # imposed by Amazon SQS.
28
28
  class SerializedJobTooBig < Error
29
29
  def initialize(serialized_job)
30
- msg = <<-MSG
31
30
  super(<<-MSG)
32
- The job contains #{serialized_job.bytesize} bytes in its serialzed form,
31
+ The job contains #{serialized_job.bytesize} bytes in its serialized form,
33
32
  which exceeds the allowed maximum of #{MAX_MESSAGE_SIZE} bytes imposed by Amazon SQS.
34
33
  MSG
35
34
  end
@@ -77,6 +76,18 @@ module ActiveJob
77
76
  end
78
77
  end
79
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
+
80
91
  def enqueue(job) #:nodoc:
81
92
  self.class.enqueue job
82
93
  end
@@ -101,6 +112,7 @@ module ActiveJob
101
112
  message[:message_body],
102
113
  message[:message_attributes])
103
114
  end
115
+ job.provider_job_id = resp.message_id
104
116
  rescue Aws::SQS::Errors::NonExistentQueue => e
105
117
  unless @queue_urls[job.queue_name.to_s].nil?
106
118
  @queue_urls[job.queue_name.to_s] = nil
@@ -114,27 +126,46 @@ module ActiveJob
114
126
  private
115
127
 
116
128
  def aws_client_verifies_md5_digests?
117
- Gem::Version.new(Aws::VERSION) >= Gem::Version.new('2.2.19'.freeze)
129
+ Gem::Version.new(Aws::CORE_GEM_VERSION) >= Gem::Version.new('2.2.19'.freeze)
118
130
  end
119
131
 
120
132
  def build_message(queue_name, serialized_job, timestamp)
121
- {
133
+ args = {
122
134
  queue_url: queue_url(queue_name),
123
135
  message_body: serialized_job,
124
136
  delay_seconds: calculate_delay(timestamp),
125
- message_attributes: {
126
- "message-digest".freeze => {
127
- string_value: message_digest(serialized_job),
128
- data_type: "String".freeze
129
- },
130
- origin: {
131
- string_value: ActiveElasticJob::ACRONYM,
132
- data_type: "String".freeze
133
- }
137
+ message_attributes: build_message_attributes(serialized_job)
138
+ }
139
+
140
+ if queue_name.split('.').last == 'fifo'
141
+ args.merge!(fifo_required_params(serialized_job))
142
+ end
143
+
144
+ return args
145
+ end
146
+
147
+ def build_message_attributes(serialized_job)
148
+ {
149
+ "message-digest".freeze => {
150
+ string_value: message_digest(serialized_job),
151
+ data_type: "String".freeze
152
+ },
153
+ origin: {
154
+ string_value: ActiveElasticJob::ACRONYM,
155
+ data_type: "String".freeze
134
156
  }
135
157
  }
136
158
  end
137
159
 
160
+ def fifo_required_params(serialized_job)
161
+ parsed_job = JSON.parse(serialized_job)
162
+
163
+ {
164
+ message_group_id: parsed_job['job_class'],
165
+ message_deduplication_id: parsed_job['job_id']
166
+ }
167
+ end
168
+
138
169
  def queue_url(queue_name)
139
170
  cache_key = queue_name.to_s
140
171
  @queue_urls ||= { }
@@ -142,18 +173,13 @@ module ActiveJob
142
173
  resp = aws_sqs_client.get_queue_url(queue_name: queue_name.to_s)
143
174
  @queue_urls[cache_key] = resp.queue_url
144
175
  rescue Aws::SQS::Errors::NonExistentQueue => e
145
- raise NonExistentQueue, queue_name
176
+ raise NonExistentQueue.new(queue_name, aws_region)
146
177
  end
147
178
 
148
179
  def calculate_delay(timestamp)
149
180
  delay = (timestamp - Time.current.to_f).to_i + 1
150
181
  if delay > MAX_DELAY_IN_MINUTES.minutes
151
- msg = "Jobs cannot be scheduled more than " <<
152
- "#{MAX_DELAY_IN_MINUTES} minutes into the future. " <<
153
- "See http://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_SendMessage.html" <<
154
- " for further details!"
155
-
156
- raise RangeError, msg
182
+ raise DelayTooLong.new
157
183
  end
158
184
  delay = 0 if delay < 0
159
185
  delay
@@ -166,11 +192,21 @@ module ActiveJob
166
192
  end
167
193
 
168
194
  def aws_sqs_client
169
- @aws_sqs_client ||= Aws::SQS::Client.new(credentials: aws_sqs_client_credentials )
195
+ options = {
196
+ credentials: aws_sqs_client_credentials,
197
+ region: aws_region
198
+ }
199
+ endpoint = Rails.application.config.active_elastic_job.endpoint
200
+ options[:endpoint] = endpoint if endpoint.present?
201
+ @aws_sqs_client ||= Aws::SQS::Client.new(options)
170
202
  end
171
203
 
172
204
  def aws_sqs_client_credentials
173
- config.aws_credentials
205
+ @aws_credentials ||= if config.aws_credentials.kind_of?(Proc)
206
+ config.aws_credentials.call
207
+ else
208
+ config.aws_credentials
209
+ end
174
210
  end
175
211
 
176
212
  def aws_region
@@ -181,13 +217,13 @@ module ActiveJob
181
217
  Rails.application.config.active_elastic_job
182
218
  end
183
219
 
184
- def message_digest(messsage_body)
220
+ def message_digest(message_body)
185
221
  @verifier ||= ActiveElasticJob::MessageVerifier.new(secret_key_base)
186
- @verifier.generate_digest(messsage_body)
222
+ @verifier.generate_digest(message_body)
187
223
  end
188
224
 
189
- def verify_md5_digests!(response, messsage_body, message_attributes)
190
- calculated = md5_of_message_body(messsage_body)
225
+ def verify_md5_digests!(response, message_body, message_attributes)
226
+ calculated = md5_of_message_body(message_body)
191
227
  returned = response.md5_of_message_body
192
228
  if calculated != returned
193
229
  raise MD5MismatchError.new response.message_id, calculated, returned
@@ -196,7 +232,7 @@ module ActiveJob
196
232
  if message_attributes
197
233
  calculated = md5_of_message_attributes(message_attributes)
198
234
  returned = response.md5_of_message_attributes
199
- if calculated != returned
235
+ if calculated != returned
200
236
  raise MD5MismatchError.new response.message_id, calculated, returned
201
237
  end
202
238
  end
metadata CHANGED
@@ -1,48 +1,196 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_elastic_job
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tawan Sierek
8
+ - Joey Paris
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2016-11-27 00:00:00.000000000 Z
12
+ date: 2021-10-25 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
- name: aws-sdk
15
+ name: aws-sdk-sqs
15
16
  requirement: !ruby/object:Gem::Requirement
16
17
  requirements:
17
18
  - - "~>"
18
19
  - !ruby/object:Gem::Version
19
- version: '2'
20
+ version: '1'
20
21
  type: :runtime
21
22
  prerelease: false
22
23
  version_requirements: !ruby/object:Gem::Requirement
23
24
  requirements:
24
25
  - - "~>"
25
26
  - !ruby/object:Gem::Version
26
- version: '2'
27
+ version: '1'
27
28
  - !ruby/object:Gem::Dependency
28
29
  name: rails
29
30
  requirement: !ruby/object:Gem::Requirement
30
31
  requirements:
31
32
  - - ">="
32
33
  - !ruby/object:Gem::Version
33
- version: '4.2'
34
+ version: '5.0'
35
+ - - "<"
36
+ - !ruby/object:Gem::Version
37
+ version: '7'
34
38
  type: :runtime
35
39
  prerelease: false
36
40
  version_requirements: !ruby/object:Gem::Requirement
37
41
  requirements:
38
42
  - - ">="
39
43
  - !ruby/object:Gem::Version
40
- version: '4.2'
44
+ version: '5.0'
45
+ - - "<"
46
+ - !ruby/object:Gem::Version
47
+ version: '7'
48
+ - !ruby/object:Gem::Dependency
49
+ name: benchmark-ips
50
+ requirement: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.8'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.8'
62
+ - !ruby/object:Gem::Dependency
63
+ name: bundler
64
+ requirement: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.2'
69
+ type: :development
70
+ prerelease: false
71
+ version_requirements: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.2'
76
+ - !ruby/object:Gem::Dependency
77
+ name: byebug
78
+ requirement: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '11.1'
83
+ type: :development
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '11.1'
90
+ - !ruby/object:Gem::Dependency
91
+ name: climate_control
92
+ requirement: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.2'
97
+ type: :development
98
+ prerelease: false
99
+ version_requirements: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.2'
104
+ - !ruby/object:Gem::Dependency
105
+ name: dotenv
106
+ requirement: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2.7'
111
+ type: :development
112
+ prerelease: false
113
+ version_requirements: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '2.7'
118
+ - !ruby/object:Gem::Dependency
119
+ name: fuubar
120
+ requirement: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '2.5'
125
+ type: :development
126
+ prerelease: false
127
+ version_requirements: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '2.5'
132
+ - !ruby/object:Gem::Dependency
133
+ name: rdoc
134
+ requirement: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '6.3'
139
+ type: :development
140
+ prerelease: false
141
+ version_requirements: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '6.3'
146
+ - !ruby/object:Gem::Dependency
147
+ name: rspec
148
+ requirement: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '3.4'
153
+ type: :development
154
+ prerelease: false
155
+ version_requirements: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '3.4'
160
+ - !ruby/object:Gem::Dependency
161
+ name: sqlite3
162
+ requirement: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '1.4'
167
+ type: :development
168
+ prerelease: false
169
+ version_requirements: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '1.4'
174
+ - !ruby/object:Gem::Dependency
175
+ name: amazing_print
176
+ requirement: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '1.2'
181
+ type: :development
182
+ prerelease: false
183
+ version_requirements: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: '1.2'
41
188
  description: Run background jobs / tasks of Rails applications deployed in Amazon
42
189
  Elastic Beanstalk environments. Active Elastic Job is an Active Job backend which
43
190
  is easy to setup. No need for customised container commands or other workarounds.
44
191
  email:
45
192
  - tawan@sierek.com
193
+ - mail@joeyparis.me
46
194
  executables: []
47
195
  extensions: []
48
196
  extra_rdoc_files: []
@@ -55,7 +203,7 @@ files:
55
203
  - lib/active_elastic_job/railtie.rb
56
204
  - lib/active_elastic_job/version.rb
57
205
  - lib/active_job/queue_adapters/active_elastic_job_adapter.rb
58
- homepage: https://github.com/tawan/active-elastic-job
206
+ homepage: https://github.com/active-elastic-job/active-elastic-job
59
207
  licenses:
60
208
  - MIT
61
209
  metadata: {}
@@ -67,15 +215,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
67
215
  requirements:
68
216
  - - ">="
69
217
  - !ruby/object:Gem::Version
70
- version: 1.9.3
218
+ version: '2.5'
71
219
  required_rubygems_version: !ruby/object:Gem::Requirement
72
220
  requirements:
73
221
  - - ">="
74
222
  - !ruby/object:Gem::Version
75
223
  version: '0'
76
224
  requirements: []
77
- rubyforge_project:
78
- rubygems_version: 2.4.5.1
225
+ rubygems_version: 3.0.3
79
226
  signing_key:
80
227
  specification_version: 4
81
228
  summary: Active Elastic Job is a simple to use Active Job backend for Rails applications