switchman-inst-jobs 2.0.0 → 3.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/db/migrate/20110610213249_optimize_delayed_jobs.rb +1 -1
- data/lib/switchman_inst_jobs/delayed/backend/base.rb +5 -5
- data/lib/switchman_inst_jobs/delayed/message_sending.rb +3 -2
- data/lib/switchman_inst_jobs/delayed/pool.rb +1 -1
- data/lib/switchman_inst_jobs/delayed/worker/health_check.rb +2 -4
- data/lib/switchman_inst_jobs/jobs_migrator.rb +57 -31
- data/lib/switchman_inst_jobs/switchman/database_server.rb +1 -1
- data/lib/switchman_inst_jobs/switchman/shard.rb +26 -31
- data/lib/switchman_inst_jobs/timed_cache.rb +1 -1
- data/lib/switchman_inst_jobs/version.rb +1 -1
- metadata +17 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f447f30d1f541b4fe85ad0d867276957b5ed583ddd01e70c509834c0441892bc
|
4
|
+
data.tar.gz: 677a35494a2b54c609017b335019a98510ca3db04e26e61778ad5a8abc0f9377
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4dead22acfe97a4a5c06ba7ae348916c39e22cef000754c98d7b4e3ecb9a3312fb2fdffaba1e889812bd77efa9ab2d0ecc9ef6bba3d8325fafb18fe99f70278b
|
7
|
+
data.tar.gz: 05513257e9b30f44e2cb10ee5eb497a66906c38b8e525fb2373326ec81d6cb84df413f02283d46faf3a85d8e996f7da66cbf4b59335e23a743329ad1c1ef6f7c
|
@@ -29,7 +29,7 @@ class OptimizeDelayedJobs < ActiveRecord::Migration[4.2]
|
|
29
29
|
add_index :delayed_jobs, %w[strand id], name: 'index_delayed_jobs_on_strand'
|
30
30
|
|
31
31
|
# move all failed jobs to the new failed table
|
32
|
-
Delayed::Backend::ActiveRecord::Job.where(
|
32
|
+
Delayed::Backend::ActiveRecord::Job.where.not(failed_at: nil).find_each do |job|
|
33
33
|
job.fail! unless job.on_hold?
|
34
34
|
end
|
35
35
|
end
|
@@ -12,13 +12,13 @@ module SwitchmanInstJobs
|
|
12
12
|
module Backend
|
13
13
|
module Base
|
14
14
|
module ClassMethods
|
15
|
-
def enqueue(object, options
|
15
|
+
def enqueue(object, **options)
|
16
16
|
::Switchman::Shard.periodic_clear_shard_cache
|
17
17
|
current_shard = ::Switchman::Shard.current
|
18
18
|
enqueue_options = options.merge(
|
19
19
|
current_shard: current_shard
|
20
20
|
)
|
21
|
-
enqueue_job = -> { ::GuardRail.activate(:master) { super(object, enqueue_options) } }
|
21
|
+
enqueue_job = -> { ::GuardRail.activate(:master) { super(object, **enqueue_options) } }
|
22
22
|
|
23
23
|
# Another dj shard must be currently manually activated, so just use that
|
24
24
|
# In general this will only happen in unusual circumstances like tests
|
@@ -43,8 +43,8 @@ module SwitchmanInstJobs
|
|
43
43
|
end
|
44
44
|
|
45
45
|
def configured_shard_ids
|
46
|
-
(::Delayed::Settings.worker_config.try(:[], 'workers') || [])
|
47
|
-
|
46
|
+
(::Delayed::Settings.worker_config.try(:[], 'workers') || []).
|
47
|
+
map { |w| w['shard'] }.compact.uniq
|
48
48
|
end
|
49
49
|
|
50
50
|
def processes_locked_locally
|
@@ -93,7 +93,7 @@ module SwitchmanInstJobs
|
|
93
93
|
# likely a missing shard with a stale cache
|
94
94
|
current_shard.send(:clear_cache)
|
95
95
|
::Switchman::Shard.clear_cache
|
96
|
-
raise ShardNotFoundError, shard_id unless ::Switchman::Shard.
|
96
|
+
raise ShardNotFoundError, shard_id unless ::Switchman::Shard.exists?(id: shard_id)
|
97
97
|
|
98
98
|
raise
|
99
99
|
end
|
@@ -1,8 +1,9 @@
|
|
1
1
|
module SwitchmanInstJobs
|
2
2
|
module Delayed
|
3
3
|
module MessageSending
|
4
|
-
def
|
5
|
-
|
4
|
+
def delay(sender: nil, synchronous: false, **enqueue_args)
|
5
|
+
sender ||= __calculate_sender_for_delay
|
6
|
+
synchronous ||= ::Switchman::DatabaseServer.creating_new_shard
|
6
7
|
|
7
8
|
super
|
8
9
|
end
|
@@ -17,7 +17,7 @@ module SwitchmanInstJobs
|
|
17
17
|
# We purposely don't .compact to remove nils here, since if any
|
18
18
|
# workers are on the default jobs shard we want to unlock against
|
19
19
|
# that shard too.
|
20
|
-
shard_ids = @config[:workers].
|
20
|
+
shard_ids = @config[:workers].pluck(:shard).uniq
|
21
21
|
shards = shard_ids.map { |shard_id| ::Delayed::Worker.shard(shard_id) }
|
22
22
|
end
|
23
23
|
::Switchman::Shard.with_each_shard(shards, [:delayed_jobs]) do
|
@@ -19,7 +19,7 @@ module SwitchmanInstJobs
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def reschedule_abandoned_jobs(call_super: false)
|
22
|
-
shards = ::Switchman::Shard.delayed_jobs_shards
|
22
|
+
shards = ::Switchman::Shard.delayed_jobs_shards.to_a
|
23
23
|
call_super = shards.first if shards.length == 1
|
24
24
|
unless call_super == false
|
25
25
|
call_super.activate(:delayed_jobs) do
|
@@ -32,9 +32,7 @@ module SwitchmanInstJobs
|
|
32
32
|
singleton = <<~SINGLETON
|
33
33
|
periodic: Delayed::Worker::HealthCheck.reschedule_abandoned_jobs:#{shard.id}
|
34
34
|
SINGLETON
|
35
|
-
|
36
|
-
:reschedule_abandoned_jobs, { singleton: singleton }, call_super: shard
|
37
|
-
)
|
35
|
+
delay(singleton: singleton).reschedule_abandoned_jobs(call_super: shard)
|
38
36
|
end
|
39
37
|
end
|
40
38
|
end
|
@@ -29,26 +29,41 @@ module SwitchmanInstJobs
|
|
29
29
|
|
30
30
|
def migrate_shards(shard_map)
|
31
31
|
source_shards = Set[]
|
32
|
+
target_shards = Hash.new([])
|
32
33
|
shard_map.each do |(shard, target_shard)|
|
33
34
|
shard = ::Switchman::Shard.find(shard) unless shard.is_a?(::Switchman::Shard)
|
34
35
|
source_shards << shard.delayed_jobs_shard.id
|
35
|
-
|
36
|
-
shard.
|
36
|
+
target_shard = target_shard.try(:id) || target_shard
|
37
|
+
target_shards[target_shard] += [shard.id]
|
37
38
|
end
|
38
39
|
|
40
|
+
# Do the updates in batches and then just clear redis instead of clearing them one at a time
|
41
|
+
target_shards.each do |target_shard, shards|
|
42
|
+
::Switchman::Shard.where(id: shards).update_all(delayed_jobs_shard_id: target_shard, block_stranded: true)
|
43
|
+
end
|
44
|
+
clear_shard_cache
|
45
|
+
|
39
46
|
# Wait a little over the 60 second in-process shard cache clearing
|
40
47
|
# threshold to ensure that all new stranded jobs are now being
|
41
48
|
# enqueued with next_in_strand: false
|
42
|
-
Rails.logger.debug(
|
49
|
+
Rails.logger.debug('Waiting for caches to clear')
|
43
50
|
sleep(65) unless @skip_cache_wait
|
44
51
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
52
|
+
::Switchman::Shard.clear_cache
|
53
|
+
# rubocop:disable Style/CombinableLoops
|
54
|
+
# We first migrate strands so that we can stop blocking strands before we migrate unstranded jobs
|
55
|
+
source_shards.each do |s|
|
56
|
+
::Switchman::Shard.lookup(s).activate(:delayed_jobs) { migrate_strands }
|
57
|
+
end
|
58
|
+
|
59
|
+
source_shards.each do |s|
|
60
|
+
::Switchman::Shard.lookup(s).activate(:delayed_jobs) { migrate_everything }
|
51
61
|
end
|
62
|
+
# rubocop:enable Style/CombinableLoops
|
63
|
+
end
|
64
|
+
|
65
|
+
def clear_shard_cache
|
66
|
+
::Switchman.cache.clear
|
52
67
|
end
|
53
68
|
|
54
69
|
# This method expects that all relevant shards already have block_stranded: true
|
@@ -71,7 +86,7 @@ module SwitchmanInstJobs
|
|
71
86
|
# those (= do nothing since it should already be true)
|
72
87
|
|
73
88
|
source_shard = ::Switchman::Shard.current(:delayed_jobs)
|
74
|
-
strand_scope = ::Delayed::Job.shard(source_shard).where(
|
89
|
+
strand_scope = ::Delayed::Job.shard(source_shard).where.not(strand: nil)
|
75
90
|
shard_map = build_shard_map(strand_scope, source_shard)
|
76
91
|
shard_map.each do |(target_shard, source_shard_ids)|
|
77
92
|
shard_scope = strand_scope.where(shard_id: source_shard_ids)
|
@@ -92,10 +107,10 @@ module SwitchmanInstJobs
|
|
92
107
|
# We lock it to ensure that the jobs worker can't delete it until we are done moving the strand
|
93
108
|
# Since we only unlock it on the new jobs queue *after* deleting from the original
|
94
109
|
# the lock ensures the blocker always gets unlocked
|
95
|
-
first = this_strand_scope.where(
|
110
|
+
first = this_strand_scope.where.not(locked_by: nil).next_in_strand_order.lock.first
|
96
111
|
if first
|
97
112
|
first_job = ::Delayed::Job.create!(strand: strand, next_in_strand: false)
|
98
|
-
first_job.payload_object = ::Delayed::PerformableMethod.new(Kernel, :sleep, [0])
|
113
|
+
first_job.payload_object = ::Delayed::PerformableMethod.new(Kernel, :sleep, args: [0])
|
99
114
|
first_job.queue = first.queue
|
100
115
|
first_job.tag = 'Kernel.sleep'
|
101
116
|
first_job.source = 'JobsMigrator::StrandBlocker'
|
@@ -106,8 +121,8 @@ module SwitchmanInstJobs
|
|
106
121
|
first_job.save!
|
107
122
|
# the rest of 3) is taken care of here
|
108
123
|
# make sure that all the jobs moved over are NOT next in strand
|
109
|
-
::Delayed::Job.where(next_in_strand: true, strand: strand, locked_by: nil)
|
110
|
-
|
124
|
+
::Delayed::Job.where(next_in_strand: true, strand: strand, locked_by: nil).
|
125
|
+
update_all(next_in_strand: false)
|
111
126
|
end
|
112
127
|
|
113
128
|
# 4) is taken care of here, by leaveing next_in_strand alone and
|
@@ -123,26 +138,37 @@ module SwitchmanInstJobs
|
|
123
138
|
end
|
124
139
|
end
|
125
140
|
|
126
|
-
::Switchman::Shard.
|
127
|
-
|
141
|
+
updated = ::Switchman::Shard.where(id: source_shard_ids, block_stranded: true).
|
142
|
+
update_all(block_stranded: false)
|
143
|
+
# If this is being manually re-run for some reason to clean something up, don't wait for nothing to happen
|
144
|
+
unless updated.zero?
|
145
|
+
clear_shard_cache
|
146
|
+
# Wait a little over the 60 second in-process shard cache clearing
|
147
|
+
# threshold to ensure that all new stranded jobs are now being
|
148
|
+
# enqueued with next_in_strand: false
|
149
|
+
Rails.logger.debug("Waiting for caches to clear (#{source_shard.id} -> #{target_shard.id})")
|
150
|
+
# for spec usage only
|
151
|
+
sleep(65) unless @skip_cache_wait
|
128
152
|
end
|
129
|
-
|
130
|
-
# threshold to ensure that all new stranded jobs are now being
|
131
|
-
# enqueued with next_in_strand: false
|
132
|
-
Rails.logger.debug("Waiting for caches to clear (#{source_shard.id} -> #{target_shard.id})")
|
133
|
-
# for spec usage only
|
134
|
-
sleep(65) unless @skip_cache_wait
|
153
|
+
::Switchman::Shard.clear_cache
|
135
154
|
# At this time, let's unblock all the strands on the target shard that aren't being held by a blocker
|
136
155
|
# but actually could have run and we just didn't know it because we didn't know if they had jobs
|
137
156
|
# on the source shard
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
157
|
+
target_shard.activate(:delayed_jobs) do
|
158
|
+
loop do
|
159
|
+
# We only want to unlock stranded jobs where they don't belong to a blocked shard (if they *do* belong)
|
160
|
+
# to a blocked shard, they must be part of a concurrent jobs migration from a different source shard to
|
161
|
+
# this target shard, so we shouldn't unlock them yet. We only ever unlock one job here to keep the
|
162
|
+
# logic cleaner; if the job is n-stranded, after the first one runs, the trigger will unlock larger
|
163
|
+
# batches
|
164
|
+
break if ::Delayed::Job.where(id: ::Delayed::Job.select('DISTINCT ON (strand) id').
|
165
|
+
where.not(strand: nil).
|
166
|
+
where.not(shard_id: ::Switchman::Shard.where(block_stranded: true).pluck(:id)).where(
|
167
|
+
::Delayed::Job.select(1).from("#{::Delayed::Job.quoted_table_name} dj2").
|
168
|
+
where("dj2.next_in_strand = true OR dj2.source = 'JobsMigrator::StrandBlocker'").
|
169
|
+
where('dj2.strand = delayed_jobs.strand').arel.exists.not
|
170
|
+
).order(:strand, :strand_order_override, :id)).limit(500).update_all(next_in_strand: true).zero?
|
171
|
+
end
|
146
172
|
end
|
147
173
|
end
|
148
174
|
end
|
@@ -242,7 +268,7 @@ module SwitchmanInstJobs
|
|
242
268
|
|
243
269
|
connection.execute "COPY #{::Delayed::Job.quoted_table_name} (#{quoted_keys}) FROM STDIN"
|
244
270
|
records.map do |record|
|
245
|
-
connection.raw_connection.put_copy_data(keys.map { |k| quote_text(record[k]) }.join("\t")
|
271
|
+
connection.raw_connection.put_copy_data("#{keys.map { |k| quote_text(record[k]) }.join("\t")}\n")
|
246
272
|
end
|
247
273
|
connection.clear_query_cache
|
248
274
|
connection.raw_connection.put_copy_end
|
@@ -11,7 +11,7 @@ module SwitchmanInstJobs
|
|
11
11
|
# shard's delayed_jobs_shard
|
12
12
|
if shard&.default?
|
13
13
|
# first look for any shard that behaves like a jobs shard
|
14
|
-
dj_shard ||= ::Switchman::Shard.delayed_jobs_shards.
|
14
|
+
dj_shard ||= ::Switchman::Shard.delayed_jobs_shards.find(&:database_server)
|
15
15
|
# we're really truly out of options, use the default shard itself
|
16
16
|
dj_shard ||= shard
|
17
17
|
end
|
@@ -28,9 +28,9 @@ module SwitchmanInstJobs
|
|
28
28
|
return unless wait
|
29
29
|
|
30
30
|
delayed_jobs_shard.activate(:delayed_jobs) do
|
31
|
-
while ::Delayed::Job.where(shard_id: id)
|
32
|
-
|
33
|
-
|
31
|
+
while ::Delayed::Job.where(shard_id: id).
|
32
|
+
where.not(locked_at: nil).
|
33
|
+
where.not(locked_by: ::Delayed::Backend::Base::ON_HOLD_LOCKED_BY).exists?
|
34
34
|
sleep 10
|
35
35
|
lock_jobs_for_hold
|
36
36
|
end
|
@@ -41,9 +41,9 @@ module SwitchmanInstJobs
|
|
41
41
|
self.jobs_held = false
|
42
42
|
save! if changed?
|
43
43
|
delayed_jobs_shard.activate(:delayed_jobs) do
|
44
|
-
::Delayed::Job.where(locked_by: ::Delayed::Backend::Base::ON_HOLD_LOCKED_BY, shard_id: id)
|
45
|
-
|
46
|
-
|
44
|
+
::Delayed::Job.where(locked_by: ::Delayed::Backend::Base::ON_HOLD_LOCKED_BY, shard_id: id).
|
45
|
+
in_batches(of: 10_000).
|
46
|
+
update_all(
|
47
47
|
locked_by: nil,
|
48
48
|
locked_at: nil,
|
49
49
|
attempts: 0,
|
@@ -111,32 +111,27 @@ module SwitchmanInstJobs
|
|
111
111
|
end
|
112
112
|
|
113
113
|
def delayed_jobs_shards
|
114
|
-
unless
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
db_dj_shards = ::Switchman::DatabaseServer.all.map do |db|
|
131
|
-
next db.shards.to_a if db.config[:delayed_jobs_shard] == 'self'
|
132
|
-
|
133
|
-
db.delayed_jobs_shard
|
134
|
-
end.compact.flatten.uniq # yes, all three
|
135
|
-
|
136
|
-
(db_dj_shards + shard_dj_shards).uniq.sort
|
137
|
-
end
|
114
|
+
return none unless ::Switchman::Shard.columns_hash.key?('delayed_jobs_shard_id')
|
115
|
+
|
116
|
+
scope = ::Switchman::Shard.unscoped.
|
117
|
+
where(id: ::Switchman::Shard.unscoped.distinct.where.not(delayed_jobs_shard_id: nil).
|
118
|
+
select(:delayed_jobs_shard_id))
|
119
|
+
db_jobs_shards = ::Switchman::DatabaseServer.all.map { |db| db.config[:delayed_jobs_shard] }.uniq
|
120
|
+
db_jobs_shards.delete(nil)
|
121
|
+
has_self = db_jobs_shards.delete('self')
|
122
|
+
scope = scope.or(::Switchman::Shard.unscoped.where(id: db_jobs_shards)) unless db_jobs_shards.empty?
|
123
|
+
|
124
|
+
if has_self
|
125
|
+
self_dbs = ::Switchman::DatabaseServer.all.
|
126
|
+
select { |db| db.config[:delayed_jobs_shard] == 'self' }.map(&:id)
|
127
|
+
scope = scope.or(::Switchman::Shard.unscoped.
|
128
|
+
where(id: ::Switchman::Shard.unscoped.where(delayed_jobs_shard_id: nil, database_server_id: self_dbs).
|
129
|
+
select(:id)))
|
138
130
|
end
|
139
|
-
@
|
131
|
+
@jobs_scope_empty = !scope.exists? unless instance_variable_defined?(:@jobs_scope_empty)
|
132
|
+
return [::Switchman::Shard.default] if @jobs_scope_empty
|
133
|
+
|
134
|
+
::Switchman::Shard.merge(scope)
|
140
135
|
end
|
141
136
|
end
|
142
137
|
end
|
metadata
CHANGED
@@ -1,35 +1,35 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: switchman-inst-jobs
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bryan Petty
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-11-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: inst-jobs
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '0
|
20
|
-
- - "
|
19
|
+
version: '1.0'
|
20
|
+
- - ">="
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version:
|
22
|
+
version: 1.0.3
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
26
26
|
requirements:
|
27
|
-
- - "
|
27
|
+
- - "~>"
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version: '0
|
30
|
-
- - "
|
29
|
+
version: '1.0'
|
30
|
+
- - ">="
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version:
|
32
|
+
version: 1.0.3
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
34
|
name: parallel
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -168,14 +168,14 @@ dependencies:
|
|
168
168
|
requirements:
|
169
169
|
- - "~>"
|
170
170
|
- !ruby/object:Gem::Version
|
171
|
-
version: '
|
171
|
+
version: '13'
|
172
172
|
type: :development
|
173
173
|
prerelease: false
|
174
174
|
version_requirements: !ruby/object:Gem::Requirement
|
175
175
|
requirements:
|
176
176
|
- - "~>"
|
177
177
|
- !ruby/object:Gem::Version
|
178
|
-
version: '
|
178
|
+
version: '13'
|
179
179
|
- !ruby/object:Gem::Dependency
|
180
180
|
name: rspec
|
181
181
|
requirement: !ruby/object:Gem::Requirement
|
@@ -210,28 +210,28 @@ dependencies:
|
|
210
210
|
requirements:
|
211
211
|
- - "~>"
|
212
212
|
- !ruby/object:Gem::Version
|
213
|
-
version:
|
213
|
+
version: 1.3.1
|
214
214
|
type: :development
|
215
215
|
prerelease: false
|
216
216
|
version_requirements: !ruby/object:Gem::Requirement
|
217
217
|
requirements:
|
218
218
|
- - "~>"
|
219
219
|
- !ruby/object:Gem::Version
|
220
|
-
version:
|
220
|
+
version: 1.3.1
|
221
221
|
- !ruby/object:Gem::Dependency
|
222
222
|
name: rubocop-rails
|
223
223
|
requirement: !ruby/object:Gem::Requirement
|
224
224
|
requirements:
|
225
225
|
- - "~>"
|
226
226
|
- !ruby/object:Gem::Version
|
227
|
-
version: 2.
|
227
|
+
version: 2.8.1
|
228
228
|
type: :development
|
229
229
|
prerelease: false
|
230
230
|
version_requirements: !ruby/object:Gem::Requirement
|
231
231
|
requirements:
|
232
232
|
- - "~>"
|
233
233
|
- !ruby/object:Gem::Version
|
234
|
-
version: 2.
|
234
|
+
version: 2.8.1
|
235
235
|
- !ruby/object:Gem::Dependency
|
236
236
|
name: simplecov
|
237
237
|
requirement: !ruby/object:Gem::Requirement
|
@@ -334,7 +334,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
334
334
|
- !ruby/object:Gem::Version
|
335
335
|
version: '0'
|
336
336
|
requirements: []
|
337
|
-
rubygems_version: 3.1.
|
337
|
+
rubygems_version: 3.1.4
|
338
338
|
signing_key:
|
339
339
|
specification_version: 4
|
340
340
|
summary: Switchman and Instructure Jobs compatibility gem.
|