inst-jobs 2.1.0 → 2.2.1

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
  SHA256:
3
- metadata.gz: b789a390da84dd32ede51b6d8655853e3f20efa55144fe19cf0605d9e5083e62
4
- data.tar.gz: bd263366443698e93572f83a6f0e2b3d747c9e72acd851fbc0aa3f920c413e02
3
+ metadata.gz: 9550c353f31dc1d34a15993bdfbf279d87979ebb625ae776fb6727d7dc9e897e
4
+ data.tar.gz: 379f8faac5d3369551b4012a00f522d7871a4001776120c4e02535b70bdef8a3
5
5
  SHA512:
6
- metadata.gz: b902bbdb3d4d676b984a22c2df5d4382960219de7a9a3d639df4fd6f958816ac77a3e2e00e881d7afbc240fd6043eef21f5362885301ccf1c9fb1ae790a21e2f
7
- data.tar.gz: 3dbb3fe818c20be76f6f07f61893014bc881a2f82a7c44139e62642230ff49a2acd0d3e2ba746b4bcdf1834fed0b5cb1ebcbb6ea3431b9fe46ffc29d6956200b
6
+ metadata.gz: 4dbb7a9366256f16a0fd7fbf22938d94c7754cdd3d9189d3d7d94e0d8deda59859ec4822f1e3c1d05cc7a2a67c8501a79c83d7b0b69f91fd106d9b24b4f35b06
7
+ data.tar.gz: 637e09a985feeef6144e5b172a04c8ceee5980eed3b1b49f2597e33bbf26522c43244bc2778905ba400cf0de6485166258103e09aa7a765e261f8971b89b2c06
@@ -87,6 +87,10 @@ module Delayed
87
87
  batches[batch_enqueue_args] << kwargs
88
88
  return true
89
89
  else
90
+ if kwargs[:on_conflict].present?
91
+ Delayed::Logging.logger.warn("[DELAYED_JOB] WARNING: providing 'on_conflict' as an option to a non-singleton job will have no effect. Discarding.")
92
+ kwargs.delete(:on_conflict)
93
+ end
90
94
  job = self.create(**kwargs)
91
95
  end
92
96
 
@@ -56,10 +56,7 @@ class Periodic
56
56
  inferred_args = {
57
57
  max_attempts: 1,
58
58
  run_at: @cron.next_time(Delayed::Periodic.now).utc.to_time,
59
- singleton: (@job_args[:singleton] == false ? nil : tag),
60
- # yes, checking for whether it is actually the boolean literal false,
61
- # which means the consuming code really does not want this job to be
62
- # a singleton at all.
59
+ singleton: tag,
63
60
  on_conflict: :patient
64
61
  }
65
62
  @job_args.merge(inferred_args)
data/lib/delayed/pool.rb CHANGED
@@ -39,6 +39,7 @@ class Pool
39
39
  Process.wait unlock_pid
40
40
 
41
41
  spawn_periodic_auditor
42
+ spawn_abandoned_job_cleanup
42
43
  spawn_all_workers
43
44
  say "Workers spawned"
44
45
  join
@@ -111,6 +112,34 @@ class Pool
111
112
  end
112
113
  end
113
114
 
115
+ def spawn_abandoned_job_cleanup
116
+ return if Settings.disable_abandoned_job_cleanup
117
+ cleanup_interval_in_minutes = 60
118
+ @abandoned_cleanup_thread = Thread.new do
119
+ # every hour (staggered by process)
120
+ # check for dead jobs and cull them.
121
+ # Will actually be more often based on the
122
+ # number of worker nodes in the pool. This will actually
123
+ # be a max of N times per hour where N is the number of workers,
124
+ # but they won't overrun each other because the health check
125
+ # takes an advisory lock internally
126
+ sleep(rand(cleanup_interval_in_minutes * 60))
127
+ loop do
128
+ schedule_abandoned_job_cleanup
129
+ sleep(cleanup_interval_in_minutes * 60)
130
+ end
131
+ end
132
+ end
133
+
134
+ def schedule_abandoned_job_cleanup
135
+ pid = fork_with_reconnects do
136
+ # we want to avoid db connections in the main pool process
137
+ $0 = "delayed_abandoned_job_cleanup"
138
+ Delayed::Worker::HealthCheck.reschedule_abandoned_jobs
139
+ end
140
+ workers[pid] = :abandoned_job_cleanup
141
+ end
142
+
114
143
  def spawn_periodic_auditor
115
144
  return if Settings.disable_periodic_jobs
116
145
 
@@ -217,6 +246,8 @@ class Pool
217
246
  case worker
218
247
  when :periodic_audit
219
248
  say "ran auditor: #{worker}"
249
+ when :abandoned_job_cleanup
250
+ say "ran cleanup: #{worker}"
220
251
  when :work_queue
221
252
  say "work queue exited, restarting", :info
222
253
  spawn_work_queue
@@ -8,6 +8,7 @@ module Delayed
8
8
  module Settings
9
9
  SETTINGS = [
10
10
  :default_job_options,
11
+ :disable_abandoned_job_cleanup,
11
12
  :disable_periodic_jobs,
12
13
  :disable_automatic_orphan_unlocking,
13
14
  :fetch_batch_size,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Delayed
4
- VERSION = "2.1.0"
4
+ VERSION = "2.2.1"
5
5
  end
@@ -105,7 +105,11 @@ class Worker
105
105
  end
106
106
 
107
107
  def exit?
108
- @exit
108
+ !!@exit || parent_exited?
109
+ end
110
+
111
+ def parent_exited?
112
+ @parent_pid && @parent_pid != Process.ppid
109
113
  end
110
114
 
111
115
  def wake_up
@@ -209,38 +213,38 @@ class Worker
209
213
  end
210
214
 
211
215
  def perform(job)
212
- count = 1
213
- raise Delayed::Backend::JobExpired, "job expired at #{job.expires_at}" if job.expired?
214
- self.class.lifecycle.run_callbacks(:perform, self, job) do
215
- set_process_name("run:#{Settings.worker_procname_prefix}#{job.id}:#{job.name}")
216
- logger.info("Processing #{log_job(job, :long)}")
217
- runtime = Benchmark.realtime do
218
- if job.batch?
219
- # each job in the batch will have perform called on it, so we don't
220
- # need a timeout around this
221
- count = perform_batch(job)
222
- else
223
- job.invoke_job
216
+ begin
217
+ count = 1
218
+ raise Delayed::Backend::JobExpired, "job expired at #{job.expires_at}" if job.expired?
219
+ self.class.lifecycle.run_callbacks(:perform, self, job) do
220
+ set_process_name("run:#{Settings.worker_procname_prefix}#{job.id}:#{job.name}")
221
+ logger.info("Processing #{log_job(job, :long)}")
222
+ runtime = Benchmark.realtime do
223
+ if job.batch?
224
+ # each job in the batch will have perform called on it, so we don't
225
+ # need a timeout around this
226
+ count = perform_batch(job)
227
+ else
228
+ job.invoke_job
229
+ end
230
+ job.destroy
224
231
  end
225
- job.destroy
232
+ logger.info("Completed #{log_job(job)} #{"%.0fms" % (runtime * 1000)}")
233
+ end
234
+ rescue ::Delayed::RetriableError => re
235
+ can_retry = job.attempts + 1 < job.inferred_max_attempts
236
+ callback_type = can_retry ? :retry : :error
237
+ self.class.lifecycle.run_callbacks(callback_type, self, job, re) do
238
+ handle_failed_job(job, re)
239
+ end
240
+ rescue SystemExit => se
241
+ # There wasn't really a failure here so no callbacks and whatnot needed,
242
+ # still reschedule the job though.
243
+ job.reschedule(se)
244
+ rescue Exception => e
245
+ self.class.lifecycle.run_callbacks(:error, self, job, e) do
246
+ handle_failed_job(job, e)
226
247
  end
227
- logger.info("Completed #{log_job(job)} #{"%.0fms" % (runtime * 1000)}")
228
- end
229
- count
230
- rescue ::Delayed::RetriableError => re
231
- can_retry = job.attempts + 1 < job.inferred_max_attempts
232
- callback_type = can_retry ? :retry : :error
233
- self.class.lifecycle.run_callbacks(callback_type, self, job, re) do
234
- handle_failed_job(job, re)
235
- end
236
- rescue SystemExit => se
237
- # There wasn't really a failure here so no callbacks and whatnot needed,
238
- # still reschedule the job though.
239
- job.reschedule(se)
240
- count
241
- rescue Exception => e
242
- self.class.lifecycle.run_callbacks(:error, self, job, e) do
243
- handle_failed_job(job, e)
244
248
  end
245
249
  count
246
250
  end
@@ -9,52 +9,49 @@ module Delayed
9
9
  class ConsulHealthCheck < HealthCheck
10
10
  self.type_name = :consul
11
11
 
12
- CONSUL_CONFIG_KEYS = %w{url host port ssl token connect_timeout receive_timeout send_timeout}.map(&:freeze).freeze
12
+ CONSUL_CONFIG_KEYS = %w{url acl_token}.map(&:freeze).freeze
13
13
  DEFAULT_SERVICE_NAME = 'inst-jobs_worker'.freeze
14
- attr_reader :agent_client, :catalog_client
14
+ attr_reader :service_client, :health_client
15
15
 
16
16
  def initialize(*, **)
17
17
  super
18
18
  # Because we don't want the consul client to be a hard dependency we're
19
19
  # only requiring it once it's absolutely needed
20
- require 'imperium'
20
+ require 'diplomat'
21
21
 
22
22
  if config.keys.any? { |k| CONSUL_CONFIG_KEYS.include?(k) }
23
- consul_config = Imperium::Configuration.new.tap do |conf|
23
+ consul_config = Diplomat::Configuration.new.tap do |conf|
24
24
  CONSUL_CONFIG_KEYS.each do |key|
25
25
  conf.send("#{key}=", config[key]) if config[key]
26
26
  end
27
27
  end
28
- @agent_client = Imperium::Agent.new(consul_config)
29
- @catalog_client = Imperium::Catalog.new(consul_config)
28
+ @service_client = Diplomat::Service.new(configuration: consul_config)
29
+ @health_client = Diplomat::Health.new(configuration: consul_config)
30
30
  else
31
- @agent_client = Imperium::Agent.default_client
32
- @catalog_client = Imperium::Catalog.default_client
31
+ @service_client = Diplomat::Service.new
32
+ @health_client = Diplomat::Health.new
33
33
  end
34
34
  end
35
35
 
36
36
  def start
37
- service = Imperium::Service.new({
37
+ @service_client.register({
38
38
  id: worker_name,
39
39
  name: service_name,
40
+ check: check_attributes
40
41
  })
41
- service.add_check(check_attributes)
42
- response = @agent_client.register_service(service)
43
- response.ok?
44
42
  end
45
43
 
46
44
  def stop
47
- response = @agent_client.deregister_service(worker_name)
48
- response.ok? || response.not_found?
45
+ @service_client.deregister(worker_name)
49
46
  end
50
47
 
51
48
  def live_workers
52
- live_nodes = @catalog_client.list_nodes_for_service(service_name)
53
- if live_nodes.ok?
54
- live_nodes.map(&:service_id)
55
- else
56
- raise "Unable to read from Consul catalog: #{live_nodes.content}"
57
- end
49
+ # Filter out critical workers (probably nodes failing their serf health check)
50
+ live_nodes = @health_client.service(service_name, {
51
+ filter: 'not Checks.Status == critical'
52
+ })
53
+
54
+ live_nodes.map { |n| n.Service['ID']}
58
55
  end
59
56
 
60
57
  private
@@ -23,12 +23,13 @@ module Delayed
23
23
  def reschedule_abandoned_jobs
24
24
  return if Settings.worker_health_check_type == :none
25
25
  Delayed::Job.transaction do
26
- # this job is a special case, and is not a singleton
26
+ # this action is a special case, and SHOULD NOT be a periodic job
27
27
  # because if it gets wiped out suddenly during execution
28
28
  # it can't go clean up it's abandoned self. Therefore,
29
- # we try to get an advisory lock when it runs. If we succeed,
30
- # no other job is trying to do this right now (and if we abandon the
31
- # job, the transaction will end, releasing the advisory lock).
29
+ # we expect it to get run from it's own process forked from the job pool
30
+ # and we try to get an advisory lock when it runs. If we succeed,
31
+ # no other worker is trying to do this right now (and if we abandon the
32
+ # operation, the transaction will end, releasing the advisory lock).
32
33
  result = attempt_advisory_lock
33
34
  return unless result
34
35
  checker = Worker::HealthCheck.build(
@@ -59,8 +60,8 @@ module Delayed
59
60
 
60
61
  def attempt_advisory_lock
61
62
  lock_name = "Delayed::Worker::HealthCheck#reschedule_abandoned_jobs"
62
- output = ActiveRecord::Base.connection.execute("SELECT pg_try_advisory_xact_lock(half_md5_as_bigint('#{lock_name}'));")
63
- output.getvalue(0, 0)
63
+ conn = ActiveRecord::Base.connection
64
+ conn.select_value("SELECT pg_try_advisory_xact_lock(#{conn.quote_table_name('half_md5_as_bigint')}('#{lock_name}'));")
64
65
  end
65
66
  end
66
67
 
@@ -14,6 +14,7 @@ RSpec.describe Delayed::Periodic do
14
14
  ensure
15
15
  Delayed::Periodic.scheduled = prev_sched
16
16
  Delayed::Periodic.overrides = prev_ovr
17
+ Delayed::Job.delete_all
17
18
  end
18
19
 
19
20
  describe ".cron" do
@@ -26,14 +27,5 @@ RSpec.describe Delayed::Periodic do
26
27
  expect(instance).to_not be_nil
27
28
  expect(instance.enqueue_args[:singleton]).to eq("periodic: just a test")
28
29
  end
29
-
30
- it "uses no singleton if told to skip" do
31
- Delayed::Periodic.cron job_name, '*/10 * * * *', {singleton: false} do
32
- # no-op
33
- end
34
- instance = Delayed::Periodic.scheduled[job_name]
35
- expect(instance).to_not be_nil
36
- expect(instance.enqueue_args[:singleton]).to be_nil
37
- end
38
30
  end
39
31
  end
@@ -1,76 +1,63 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'spec_helper'
4
- require 'imperium'
5
4
 
6
5
  RSpec.describe Delayed::Worker::ConsulHealthCheck do
7
6
  let(:health_check) { Delayed::Worker::ConsulHealthCheck.new(worker_name: 'foobar') }
8
7
 
9
- # can't use a verifying double for the response because the methods we're
10
- # tryig to stub are actually on HTTP::Message
11
- let(:response) { double('Imperium::Response') }
12
- let(:agent_client) { instance_double(Imperium::Agent) }
13
-
14
- before do
15
- allow(Imperium::Agent).to receive(:default_client).and_return(agent_client)
16
- end
17
-
18
8
  describe '#initialize' do
19
- it 'must use the default agent client when the config is mostly empty' do
9
+ it 'must use a default service client when the config is mostly empty' do
20
10
  check = Delayed::Worker::ConsulHealthCheck.new(worker_name: 'foobar')
21
- expect(check.agent_client).to eq Imperium::Agent.default_client
11
+ expect(check.service_client.configuration.url.to_s).to eq 'http://localhost:8500'
22
12
  end
23
13
 
24
- it 'must create a new agent API client when the config has relevant keys set' do
14
+ it 'must create a new service API client when the config has relevant keys set' do
25
15
  check = Delayed::Worker::ConsulHealthCheck.new(worker_name: 'foobar', config: {url: 'http://consul.example.com:8500'})
26
- agent_client = check.agent_client
27
- expect(agent_client).to_not eq Imperium::Agent.default_client
28
- expect(agent_client.config.url.to_s).to eq 'http://consul.example.com:8500'
16
+ service_client = check.service_client
17
+ expect(service_client.configuration.url.to_s).to eq 'http://consul.example.com:8500'
29
18
  end
30
19
  end
31
20
 
32
21
  describe '#start' do
33
22
  it 'must register this process as a service with consul' do
34
- expect(response).to receive(:ok?).and_return(true)
35
- expect(agent_client).to receive(:register_service)
36
- .with(an_instance_of(Imperium::Service))
37
- .and_return(response)
23
+ stub = stub_request(:put, "localhost:8500/v1/agent/service/register")
24
+ .with(body: hash_including({id: 'foobar' }))
25
+
38
26
  health_check.start
27
+
28
+ expect(stub).to have_been_requested
39
29
  end
40
30
 
41
31
 
42
32
  it 'must supply a args style check' do
43
- allow(response).to receive(:ok?).and_return(true)
44
- allow(agent_client).to receive(:register_service) { |service|
45
- check = service.checks.first
46
- expect(check.args).to_not be_nil
47
- response
48
- }
33
+ stub = stub_request(:put, "localhost:8500/v1/agent/service/register")
34
+ .with(body: hash_including({check: WebMock::API.hash_including({args: anything})}))
35
+
49
36
  health_check.start
37
+
38
+ expect(stub).to have_been_requested
50
39
  end
51
40
 
52
41
  it 'must include the docker container id when the docker option is set to true' do
42
+ stub = stub_request(:put, "localhost:8500/v1/agent/service/register")
43
+ .with(body: hash_including({check: WebMock::API.hash_including({docker_container_id: anything})}))
44
+
53
45
  local_health_check = Delayed::Worker::ConsulHealthCheck.new(
54
46
  worker_name: 'foobar',
55
47
  config: {docker: true}
56
48
  )
57
- allow(response).to receive(:ok?).and_return(true)
58
- allow(agent_client).to receive(:register_service) { |service|
59
- check = service.checks.first
60
- expect(check.docker_container_id).to_not be_nil
61
- response
62
- }
63
49
  local_health_check.start
50
+
51
+ expect(stub).to have_been_requested
64
52
  end
65
53
  end
66
54
 
67
55
  describe '#stop' do
68
56
  it 'must deregister the service from consul' do
69
- allow(response).to receive(:ok?).and_return(true)
70
- expect(agent_client).to receive(:deregister_service)
71
- .with(health_check.worker_name)
72
- .and_return(response)
57
+ stub = stub_request(:put, "localhost:8500/v1/agent/service/deregister/foobar")
58
+
73
59
  health_check.stop
60
+ expect(stub).to have_been_requested
74
61
  end
75
62
  end
76
63
  end
@@ -6,6 +6,11 @@ describe Delayed::Worker do
6
6
  let(:worker_config) { {
7
7
  queue: "test", min_priority: 1, max_priority: 2, stuff: "stuff",
8
8
  }.freeze }
9
+ let(:job_attrs) { {
10
+ id: 42, name: "testjob", full_name: "testfullname", :last_error= => nil,
11
+ attempts: 1, reschedule: nil, :expired? => false,
12
+ payload_object: {}, priority: 25
13
+ }.freeze }
9
14
  subject { described_class.new(worker_config.dup) }
10
15
 
11
16
  after { Delayed::Worker.lifecycle.reset! }
@@ -14,9 +19,10 @@ describe Delayed::Worker do
14
19
  it "fires off an error callback when a job raises an exception" do
15
20
  fired = false
16
21
  Delayed::Worker.lifecycle.before(:error) {|worker, exception| fired = true}
17
- job = double(:last_error= => nil, attempts: 1, reschedule: nil)
18
- subject.perform(job)
22
+ job = double(job_attrs)
23
+ output_count = subject.perform(job)
19
24
  expect(fired).to be_truthy
25
+ expect(output_count).to eq(1)
20
26
  end
21
27
 
22
28
  it "uses the retry callback for a retriable exception" do
@@ -27,9 +33,10 @@ describe Delayed::Worker do
27
33
  expect(job).to receive(:invoke_job) do
28
34
  raise Delayed::RetriableError, "that's all this job does"
29
35
  end
30
- subject.perform(job)
36
+ output_count = subject.perform(job)
31
37
  expect(error_fired).to be_falsey
32
38
  expect(retry_fired).to be_truthy
39
+ expect(output_count).to eq(1)
33
40
  end
34
41
 
35
42
  it "reloads" do
@@ -48,7 +55,7 @@ describe Delayed::Worker do
48
55
  expect(ActionDispatch::Reloader).to receive(:prepare!).once
49
56
  expect(ActionDispatch::Reloader).to receive(:cleanup!).once
50
57
  end
51
- job = double(:last_error= => nil, attempts: 0, reschedule: nil, expired?: false)
58
+ job = double(job_attrs)
52
59
  subject.perform(job)
53
60
  end
54
61
  end
data/spec/spec_helper.rb CHANGED
@@ -7,6 +7,8 @@ require 'database_cleaner'
7
7
  require 'rack/test'
8
8
  require 'test_after_commit' if ::Rails.version < '5'
9
9
  require 'timecop'
10
+ require 'webmock/rspec'
11
+
10
12
  require 'pry'
11
13
  require 'byebug'
12
14
 
@@ -19,6 +21,7 @@ RSpec.configure do |config|
19
21
  config.before(:suite) do
20
22
  DatabaseCleaner.strategy = :transaction
21
23
  DatabaseCleaner.clean_with(:truncation)
24
+ WebMock.disable_net_connect!
22
25
  end
23
26
 
24
27
  config.before(:each) do |example|
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inst-jobs
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tobias Luetke
8
8
  - Brian Palmer
9
- autorequire:
9
+ autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2020-12-07 00:00:00.000000000 Z
12
+ date: 2021-02-19 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -172,33 +172,33 @@ dependencies:
172
172
  - !ruby/object:Gem::Version
173
173
  version: 1.6.1
174
174
  - !ruby/object:Gem::Dependency
175
- name: imperium
175
+ name: diplomat
176
176
  requirement: !ruby/object:Gem::Requirement
177
177
  requirements:
178
- - - ">="
178
+ - - "~>"
179
179
  - !ruby/object:Gem::Version
180
- version: 0.5.2
180
+ version: 2.5.1
181
181
  type: :development
182
182
  prerelease: false
183
183
  version_requirements: !ruby/object:Gem::Requirement
184
184
  requirements:
185
- - - ">="
185
+ - - "~>"
186
186
  - !ruby/object:Gem::Version
187
- version: 0.5.2
187
+ version: 2.5.1
188
188
  - !ruby/object:Gem::Dependency
189
189
  name: pg
190
190
  requirement: !ruby/object:Gem::Requirement
191
191
  requirements:
192
- - - "<"
192
+ - - ">="
193
193
  - !ruby/object:Gem::Version
194
- version: '1.0'
194
+ version: '0'
195
195
  type: :development
196
196
  prerelease: false
197
197
  version_requirements: !ruby/object:Gem::Requirement
198
198
  requirements:
199
- - - "<"
199
+ - - ">="
200
200
  - !ruby/object:Gem::Version
201
- version: '1.0'
201
+ version: '0'
202
202
  - !ruby/object:Gem::Dependency
203
203
  name: pry
204
204
  requirement: !ruby/object:Gem::Requirement
@@ -311,6 +311,20 @@ dependencies:
311
311
  - - '='
312
312
  - !ruby/object:Gem::Version
313
313
  version: 0.7.1
314
+ - !ruby/object:Gem::Dependency
315
+ name: webmock
316
+ requirement: !ruby/object:Gem::Requirement
317
+ requirements:
318
+ - - ">="
319
+ - !ruby/object:Gem::Version
320
+ version: '0'
321
+ type: :development
322
+ prerelease: false
323
+ version_requirements: !ruby/object:Gem::Requirement
324
+ requirements:
325
+ - - ">="
326
+ - !ruby/object:Gem::Version
327
+ version: '0'
314
328
  - !ruby/object:Gem::Dependency
315
329
  name: wwtd
316
330
  requirement: !ruby/object:Gem::Requirement
@@ -325,7 +339,7 @@ dependencies:
325
339
  - - "~>"
326
340
  - !ruby/object:Gem::Version
327
341
  version: 1.4.0
328
- description:
342
+ description:
329
343
  email:
330
344
  - brianp@instructure.com
331
345
  executables:
@@ -441,7 +455,7 @@ files:
441
455
  homepage: https://github.com/instructure/inst-jobs
442
456
  licenses: []
443
457
  metadata: {}
444
- post_install_message:
458
+ post_install_message:
445
459
  rdoc_options: []
446
460
  require_paths:
447
461
  - lib
@@ -456,8 +470,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
456
470
  - !ruby/object:Gem::Version
457
471
  version: '0'
458
472
  requirements: []
459
- rubygems_version: 3.0.3
460
- signing_key:
473
+ rubygems_version: 3.1.4
474
+ signing_key:
461
475
  specification_version: 4
462
476
  summary: Instructure-maintained fork of delayed_job
463
477
  test_files: