switchman-inst-jobs 1.3.7 → 1.5.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: 2bab0c63ffbb0a328830881dd3a7c4d87dcf4d257924540e3f817987cc0b02ca
4
- data.tar.gz: 4efc1a1f73cd1c4e3343ebabb4aee21ce8b974b7191052616d6e1266e1547875
3
+ metadata.gz: ce47b09551ccb65d58da3f01b597ae32423282c1f74ba28f57cdc46a47aeb9cf
4
+ data.tar.gz: 137d3b0de54521971d62d94d33cfb4feaa1751cbb1520781397827f0ca8228be
5
5
  SHA512:
6
- metadata.gz: 79109e3f82d473756cdb953c78b5e062c6f5bd4f262356c86c4502abe5aebff40a45679dea2ec530552d23b104fd70ab3f75f7686587d0da75c28ff97bc963fe
7
- data.tar.gz: 39dda681ada80591021c3ca33df1061ae62307d94870fd6e4f5193cc361a4353a4b74f7ac86e0a4ab8bfd1fa4f4e800ac7556bbc6d4572b3ea314b5f4b551bf9
6
+ metadata.gz: f6df832cc871c8cb2ca0fc1c40740fbda65a98b1e815b8228b1cac2ce9599846da95c0068f68eab800116258ddc4baa028f7290374a8b852a8a84a1d11298ad5
7
+ data.tar.gz: ce795c604d25ad649e55acfeb3762bdd60f5418cdf48fbe3544bdd01310d7fffc6246b8146e011f4e58e94c06e130c096452add76a29c1a4210981378719849e
@@ -0,0 +1,5 @@
1
+ class AddOnHoldToSwitchmanShards < ActiveRecord::Migration[5.2]
2
+ def change
3
+ add_column :switchman_shards, :jobs_held, :bool, default: false
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddBlockStrandedToSwitchmanShards < ActiveRecord::Migration[5.2]
2
+ def change
3
+ add_column :switchman_shards, :block_stranded, :bool, default: false
4
+ end
5
+ end
@@ -44,9 +44,11 @@ require 'switchman_inst_jobs/delayed/pool'
44
44
  require 'switchman_inst_jobs/delayed/worker'
45
45
  require 'switchman_inst_jobs/delayed/worker/health_check'
46
46
  require 'switchman_inst_jobs/engine'
47
+ require 'switchman_inst_jobs/jobs_migrator'
47
48
  require 'switchman_inst_jobs/new_relic'
48
49
  require 'switchman_inst_jobs/shackles'
49
50
  require 'switchman_inst_jobs/switchman/database_server'
50
51
  require 'switchman_inst_jobs/switchman/default_shard'
51
52
  require 'switchman_inst_jobs/switchman/shard'
53
+ require 'switchman_inst_jobs/timed_cache'
52
54
  require 'switchman_inst_jobs/yaml_extensions'
@@ -13,16 +13,32 @@ module SwitchmanInstJobs
13
13
  module Base
14
14
  module ClassMethods
15
15
  def enqueue(object, options = {})
16
+ ::Switchman::Shard.periodic_clear_shard_cache
17
+ current_shard = ::Switchman::Shard.current
16
18
  enqueue_options = options.merge(
17
- current_shard: ::Switchman::Shard.current
19
+ current_shard: current_shard
18
20
  )
21
+ enqueue_job = -> { ::Shackles.activate(:master) { super(object, enqueue_options) } }
19
22
 
20
- if ::ActiveRecord::Migration.open_migrations.positive?
21
- ::Switchman::Shard.current.delayed_jobs_shard.activate(:delayed_jobs) do
22
- super(object, enqueue_options)
23
- end
23
+ # Another dj shard must be currently manually activated, so just use that
24
+ # In general this will only happen in unusual circumstances like tests
25
+ # also if migrations are running, always use the current shard's job shard
26
+ if ::ActiveRecord::Migration.open_migrations.zero? &&
27
+ current_shard.delayed_jobs_shard != ::Switchman::Shard.current(:delayed_jobs)
28
+ enqueue_job.call
24
29
  else
25
- ::Shackles.activate(:master) { super(object, enqueue_options) }
30
+ ::Switchman::Shard.default.activate do
31
+ current_shard = ::Switchman::Shard.lookup(current_shard.id)
32
+ current_job_shard = current_shard.delayed_jobs_shard
33
+
34
+ if (options[:singleton] || options[:strand]) && current_shard.block_stranded
35
+ enqueue_options[:next_in_strand] = false
36
+ end
37
+
38
+ current_job_shard.activate(:delayed_jobs) do
39
+ enqueue_job.call
40
+ end
41
+ end
26
42
  end
27
43
  end
28
44
 
@@ -54,8 +70,15 @@ module SwitchmanInstJobs
54
70
  end
55
71
 
56
72
  def current_shard=(shard)
73
+ @current_shard = nil
57
74
  self.shard_id = shard.id
58
75
  self.shard_id = nil if shard.is_a?(::Switchman::DefaultShard)
76
+ # If jobs are held for a shard, enqueue new ones as held as well
77
+ return unless shard.jobs_held
78
+
79
+ self.locked_by = ::Delayed::Backend::Base::ON_HOLD_LOCKED_BY
80
+ self.locked_at = ::Delayed::Job.db_time_now
81
+ self.attempts = ::Delayed::Backend::Base::ON_HOLD_COUNT
59
82
  end
60
83
 
61
84
  def invoke_job
@@ -64,12 +87,14 @@ module SwitchmanInstJobs
64
87
 
65
88
  def deserialize(source)
66
89
  raise ShardNotFoundError, shard_id unless current_shard
90
+
67
91
  current_shard.activate { super }
68
92
  rescue ::Switchman::ConnectionError, PG::ConnectionBad, PG::UndefinedTable
69
93
  # likely a missing shard with a stale cache
70
94
  current_shard.send(:clear_cache)
71
95
  ::Switchman::Shard.clear_cache
72
96
  raise ShardNotFoundError, shard_id unless ::Switchman::Shard.where(id: shard_id).exists?
97
+
73
98
  raise
74
99
  end
75
100
  end
@@ -3,6 +3,7 @@ module SwitchmanInstJobs
3
3
  module MessageSending
4
4
  def send_later_enqueue_args(method, _enqueue_args = {}, *args)
5
5
  return send(method, *args) if ::Switchman::DatabaseServer.creating_new_shard
6
+
6
7
  super
7
8
  end
8
9
  end
@@ -20,15 +20,20 @@ module SwitchmanInstJobs
20
20
 
21
21
  def reschedule_abandoned_jobs(call_super: false)
22
22
  shards = ::Switchman::Shard.delayed_jobs_shards
23
- call_super = true if shards.length == 1
24
- return munge_service_name(::Switchman::Shard.current(:delayed_jobs)) { super() } if call_super
23
+ call_super = shards.first if shards.length == 1
24
+ unless call_super == false
25
+ call_super.activate(:delayed_jobs) do
26
+ return munge_service_name(call_super) { super() }
27
+ end
28
+ end
25
29
 
26
30
  ::Switchman::Shard.with_each_shard(shards, [:delayed_jobs], exception: :ignore) do
31
+ shard = ::Switchman::Shard.current(:delayed_jobs)
27
32
  singleton = <<~SINGLETON
28
- periodic: Delayed::Worker::HealthCheck.reschedule_abandoned_jobs:#{::Switchman::Shard.current(:delayed_jobs).id}
33
+ periodic: Delayed::Worker::HealthCheck.reschedule_abandoned_jobs:#{shard.id}
29
34
  SINGLETON
30
35
  send_later_enqueue_args(
31
- :reschedule_abandoned_jobs, { singleton: singleton }, call_super: true
36
+ :reschedule_abandoned_jobs, { singleton: singleton }, call_super: shard
32
37
  )
33
38
  end
34
39
  end
@@ -8,6 +8,30 @@ module SwitchmanInstJobs
8
8
 
9
9
  initializer 'sharding.delayed' do
10
10
  SwitchmanInstJobs.initialize_inst_jobs
11
+
12
+ ::Delayed::Worker.lifecycle.around(:work_queue_pop) do |worker, config, &block|
13
+ if config[:shard]
14
+ ::Switchman::Shard.lookup(config[:shard]).activate(:delayed_jobs) { block.call(worker, config) }
15
+ else
16
+ block.call(worker, config)
17
+ end
18
+ end
19
+
20
+ # Ensure jobs get unblocked on the new shard if they exist
21
+ ::Delayed::Worker.lifecycle.after(:perform) do |_worker, job|
22
+ if job.strand
23
+ ::Switchman::Shard.clear_cache
24
+ ::Switchman::Shard.default.activate do
25
+ current_job_shard = ::Switchman::Shard.lookup(job.shard_id).delayed_jobs_shard
26
+ if current_job_shard != ::Switchman::Shard.current(:delayed_jobs)
27
+ current_job_shard.activate(:delayed_jobs) do
28
+ j = ::Delayed::Job.where(strand: job.strand).next_in_strand_order.first
29
+ j.update_column(:next_in_strand, true) if j && !j.next_in_strand
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
11
35
  end
12
36
 
13
37
  initializer 'sharding.shackles', after: 'switchman.extend_shackles' do
@@ -0,0 +1,271 @@
1
+ # Just disabling all the rubocop metrics for this file for now,
2
+ # as it is a direct port-in of existing code
3
+
4
+ # rubocop:disable Metrics/BlockLength, Metrics/MethodLength, Metrics/AbcSize, Metrics/ClassLength
5
+ require 'set'
6
+ require 'parallel'
7
+
8
+ module SwitchmanInstJobs
9
+ class JobsMigrator
10
+ class << self
11
+ def add_before_move_callback(proc)
12
+ @before_move_callbacks ||= []
13
+ @before_move_callbacks << proc
14
+ end
15
+
16
+ def transaction_on(shards, &block)
17
+ return yield if shards.empty?
18
+
19
+ shard = shards.pop
20
+ current_shard = ::Switchman::Shard.current(:delayed_jobs)
21
+ shard.activate(:delayed_jobs) do
22
+ ::Delayed::Job.transaction do
23
+ current_shard.activate(:delayed_jobs) do
24
+ transaction_on(shards, &block)
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ def migrate_shards(shard_map)
31
+ source_shards = Set[]
32
+ shard_map.each do |(shard, target_shard)|
33
+ shard = ::Switchman::Shard.find(shard) unless shard.is_a?(::Switchman::Shard)
34
+ source_shards << shard.delayed_jobs_shard.id
35
+ # If target_shard is an int, it won't have an id, but we can just use it as is
36
+ shard.update(delayed_jobs_shard_id: target_shard.try(:id) || target_shard, block_stranded: true)
37
+ end
38
+
39
+ # Wait a little over the 60 second in-process shard cache clearing
40
+ # threshold to ensure that all new stranded jobs are now being
41
+ # enqueued with next_in_strand: false
42
+ Rails.logger.debug("Waiting for caches to clear (#{source_shard.id} -> #{target_shard.id})")
43
+ sleep(65) unless @skip_cache_wait
44
+
45
+ # TODO: 4 has been picked completely out of a hat. We should make it configurable or something
46
+ Parallel.each(source_shards, in_processes: 4) do |s|
47
+ # Ensure the child processes don't share connections with the parent
48
+ Delayed::Pool.on_fork.call
49
+ ActiveRecord::Base.clear_all_connections!
50
+ s.activate(:delayed_jobs) { run }
51
+ end
52
+ end
53
+
54
+ # This method expects that all relevant shards already have block_stranded: true
55
+ # but otherwise jobs can be running normally
56
+ def run
57
+ # Ensure this is never run with a dirty in-memory shard cache
58
+ ::Switchman::Shard.clear_cache
59
+ migrate_strands
60
+ migrate_everything
61
+ end
62
+
63
+ def migrate_strands
64
+ # there are 4 scenarios to deal with here
65
+ # 1) no running job, no jobs moved: do nothing
66
+ # 2) running job, no jobs moved; create blocker with next_in_strand=false
67
+ # to prevent new jobs from immediately executing
68
+ # 3) running job, jobs moved; set next_in_strand=false on the first of
69
+ # those (= do nothing since it should already be false)
70
+ # 4) no running job, jobs moved: set next_in_strand=true on the first of
71
+ # those (= do nothing since it should already be true)
72
+
73
+ source_shard = ::Switchman::Shard.current(:delayed_jobs)
74
+ strand_scope = ::Delayed::Job.shard(source_shard).where('strand IS NOT NULL')
75
+ shard_map = build_shard_map(strand_scope, source_shard)
76
+ shard_map.each do |(target_shard, source_shard_ids)|
77
+ shard_scope = strand_scope.where(shard_id: source_shard_ids)
78
+
79
+ # 1) is taken care of because it should not show up here in strands
80
+ strands = shard_scope.distinct.order(:strand).pluck(:strand)
81
+
82
+ target_shard.activate(:delayed_jobs) do
83
+ strands.each do |strand|
84
+ transaction_on([source_shard, target_shard]) do
85
+ this_strand_scope = shard_scope.where(strand: strand)
86
+ # we want to copy all the jobs except the one that is still running.
87
+ jobs_scope = this_strand_scope.where(locked_by: nil)
88
+
89
+ # 2) and part of 3) are taken care of here by creating a blocker
90
+ # job with next_in_strand = false. as soon as the current
91
+ # running job is finished it should set next_in_strand
92
+ # We lock it to ensure that the jobs worker can't delete it until we are done moving the strand
93
+ # Since we only unlock it on the new jobs queue *after* deleting from the original
94
+ # the lock ensures the blocker always gets unlocked
95
+ first = this_strand_scope.where('locked_by IS NOT NULL').next_in_strand_order.lock.first
96
+ if first
97
+ first_job = ::Delayed::Job.create!(strand: strand, next_in_strand: false)
98
+ first_job.payload_object = ::Delayed::PerformableMethod.new(Kernel, :sleep, [0])
99
+ first_job.queue = first.queue
100
+ first_job.tag = 'Kernel.sleep'
101
+ first_job.source = 'JobsMigrator::StrandBlocker'
102
+ first_job.max_attempts = 1
103
+ # If we ever have jobs left over from 9999 jobs moves of a single shard,
104
+ # something has gone terribly wrong
105
+ first_job.strand_order_override = -9999
106
+ first_job.save!
107
+ # the rest of 3) is taken care of here
108
+ # 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
+ .update_all(next_in_strand: false)
111
+ end
112
+
113
+ # 4) is taken care of here, by leaveing next_in_strand alone and
114
+ # it should execute on the new shard
115
+ batch_move_jobs(
116
+ target_shard: target_shard,
117
+ source_shard: source_shard,
118
+ scope: jobs_scope
119
+ ) do |job, new_job|
120
+ # This ensures jobs enqueued on the old jobs shard run before jobs on the new jobs queue
121
+ new_job.strand_order_override = job.strand_order_override - 1
122
+ end
123
+ end
124
+ end
125
+
126
+ ::Switchman::Shard.find(source_shard_ids).each do |shard|
127
+ shard.update(block_stranded: false)
128
+ end
129
+ # Wait a little over the 60 second in-process shard cache clearing
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
135
+ # At this time, let's unblock all the strands on the target shard that aren't being held by a blocker
136
+ # but actually could have run and we just didn't know it because we didn't know if they had jobs
137
+ # on the source shard
138
+ # rubocop:disable Layout/LineLength
139
+ strands_to_unblock = shard_scope.where.not(source: 'JobsMigrator::StrandBlocker')
140
+ .distinct
141
+ .where("NOT EXISTS (SELECT 1 FROM #{::Delayed::Job.quoted_table_name} dj2 WHERE delayed_jobs.strand=dj2.strand AND next_in_strand)")
142
+ .pluck(:strand)
143
+ # rubocop:enable Layout/LineLength
144
+ strands_to_unblock.each do |strand|
145
+ Delayed::Job.where(strand: strand).next_in_strand_order.first.update_attribute(:next_in_strand, true)
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ def migrate_everything
152
+ source_shard = ::Switchman::Shard.current(:delayed_jobs)
153
+ scope = ::Delayed::Job.shard(source_shard).where('strand IS NULL')
154
+
155
+ shard_map = build_shard_map(scope, source_shard)
156
+ shard_map.each do |(target_shard, source_shard_ids)|
157
+ batch_move_jobs(
158
+ target_shard: target_shard,
159
+ source_shard: source_shard,
160
+ scope: scope.where(shard_id: source_shard_ids).where(locked_by: nil)
161
+ )
162
+ end
163
+ end
164
+
165
+ private
166
+
167
+ def build_shard_map(scope, source_shard)
168
+ shard_ids = scope.distinct.pluck(:shard_id)
169
+
170
+ shard_map = {}
171
+ ::Switchman::Shard.find(shard_ids).each do |shard|
172
+ next if shard.delayed_jobs_shard == source_shard
173
+
174
+ shard_map[shard.delayed_jobs_shard] ||= []
175
+ shard_map[shard.delayed_jobs_shard] << shard.id
176
+ end
177
+
178
+ shard_map
179
+ end
180
+
181
+ def batch_move_jobs(target_shard:, source_shard:, scope:)
182
+ while scope.exists?
183
+ # Adapted from get_and_lock_next_available in delayed/backend/active_record.rb
184
+ target_jobs = scope.limit(1000).lock('FOR UPDATE SKIP LOCKED')
185
+
186
+ query = "WITH limited_jobs AS (#{target_jobs.to_sql}) " \
187
+ "UPDATE #{::Delayed::Job.quoted_table_name} " \
188
+ "SET locked_by = #{::Delayed::Job.connection.quote(::Delayed::Backend::Base::ON_HOLD_LOCKED_BY)}, " \
189
+ "locked_at = #{::Delayed::Job.connection.quote(::Delayed::Job.db_time_now)} "\
190
+ "FROM limited_jobs WHERE limited_jobs.id=#{::Delayed::Job.quoted_table_name}.id " \
191
+ "RETURNING #{::Delayed::Job.quoted_table_name}.*"
192
+
193
+ jobs = source_shard.activate(:delayed_jobs) { ::Delayed::Job.find_by_sql(query) }
194
+ new_jobs = jobs.map do |job|
195
+ new_job = job.dup
196
+ new_job.shard = target_shard
197
+ new_job.created_at = job.created_at
198
+ new_job.updated_at = job.updated_at
199
+ new_job.locked_at = nil
200
+ new_job.locked_by = nil
201
+ yield(job, new_job) if block_given?
202
+ @before_move_callbacks&.each do |proc|
203
+ proc.call(
204
+ old_job: job,
205
+ new_job: new_job
206
+ )
207
+ end
208
+ new_job
209
+ end
210
+ transaction_on([source_shard, target_shard]) do
211
+ target_shard.activate(:delayed_jobs) do
212
+ bulk_insert_jobs(new_jobs)
213
+ end
214
+ source_shard.activate(:delayed_jobs) do
215
+ ::Delayed::Job.delete(jobs)
216
+ end
217
+ end
218
+ end
219
+ end
220
+
221
+ # This is adapted from the postgreql adapter in canvas-lms
222
+ # Once we stop supporting rails 5.2 we can just use insert_all from activerecord
223
+ def bulk_insert_jobs(objects)
224
+ records = objects.map do |object|
225
+ object.attributes.map do |(name, value)|
226
+ next if name == ::Delayed::Job.primary_key
227
+
228
+ if (type = ::Delayed::Job.attribute_types[name]).is_a?(::ActiveRecord::Type::Serialized)
229
+ value = type.serialize(value)
230
+ end
231
+ [name, value]
232
+ end.compact.to_h
233
+ end
234
+ return if records.length.zero?
235
+
236
+ keys = records.first.keys
237
+
238
+ connection = ::Delayed::Job.connection
239
+ quoted_keys = keys.map { |k| connection.quote_column_name(k) }.join(', ')
240
+
241
+ connection.execute "COPY #{::Delayed::Job.quoted_table_name} (#{quoted_keys}) FROM STDIN"
242
+ records.map do |record|
243
+ connection.raw_connection.put_copy_data(keys.map { |k| quote_text(record[k]) }.join("\t") + "\n")
244
+ end
245
+ connection.clear_query_cache
246
+ connection.raw_connection.put_copy_end
247
+ result = connection.raw_connection.get_result
248
+ begin
249
+ result.check
250
+ rescue StandardError => e
251
+ raise connection.send(:translate_exception, e, 'COPY FROM STDIN')
252
+ end
253
+ result.cmd_tuples
254
+ end
255
+
256
+ # See above comment...
257
+ def quote_text(value)
258
+ if value.nil?
259
+ '\\N'
260
+ elsif value.is_a?(::ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array::Data)
261
+ quote_text(encode_array(value))
262
+ else
263
+ hash = { "\n" => '\\n', "\r" => '\\r', "\t" => '\\t', '\\' => '\\\\' }
264
+ value.to_s.gsub(/[\n\r\t\\]/) { |c| hash[c] }
265
+ end
266
+ end
267
+ end
268
+ end
269
+ end
270
+
271
+ # rubocop:enable Metrics/BlockLength, Metrics/MethodLength, Metrics/AbcSize, Metrics/ClassLength
@@ -3,6 +3,7 @@ module SwitchmanInstJobs
3
3
  module DatabaseServer
4
4
  def delayed_jobs_shard(shard = nil)
5
5
  return shard if config[:delayed_jobs_shard] == 'self'
6
+
6
7
  dj_shard =
7
8
  config[:delayed_jobs_shard] &&
8
9
  ::Switchman::Shard.lookup(config[:delayed_jobs_shard])
@@ -4,6 +4,14 @@ module SwitchmanInstJobs
4
4
  def delayed_jobs_shard
5
5
  self
6
6
  end
7
+
8
+ def jobs_held
9
+ false
10
+ end
11
+
12
+ def block_stranded
13
+ false
14
+ end
7
15
  end
8
16
  end
9
17
  end
@@ -17,6 +17,51 @@ module SwitchmanInstJobs
17
17
  database_server&.delayed_jobs_shard(self)
18
18
  end
19
19
 
20
+ # Adapted from hold/unhold methods in base delayed jobs base
21
+ # Wait is required to be able to safely move jobs
22
+ def hold_jobs!(wait: false)
23
+ self.jobs_held = true
24
+ save! if changed?
25
+ delayed_jobs_shard.activate(:delayed_jobs) do
26
+ lock_jobs_for_hold
27
+ end
28
+ return unless wait
29
+
30
+ delayed_jobs_shard.activate(:delayed_jobs) do
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
+ sleep 10
35
+ lock_jobs_for_hold
36
+ end
37
+ end
38
+ end
39
+
40
+ def unhold_jobs!
41
+ self.jobs_held = false
42
+ save! if changed?
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
+ .in_batches(of: 10_000)
46
+ .update_all(
47
+ locked_by: nil,
48
+ locked_at: nil,
49
+ attempts: 0,
50
+ failed_at: nil
51
+ )
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def lock_jobs_for_hold
58
+ ::Delayed::Job.where(locked_at: nil, shard_id: id).in_batches(of: 10_000).update_all(
59
+ locked_by: ::Delayed::Backend::Base::ON_HOLD_LOCKED_BY,
60
+ locked_at: ::Delayed::Job.db_time_now,
61
+ attempts: ::Delayed::Backend::Base::ON_HOLD_COUNT
62
+ )
63
+ end
64
+
20
65
  module ClassMethods
21
66
  def clear_cache
22
67
  super
@@ -57,6 +102,14 @@ module SwitchmanInstJobs
57
102
  db.create_new_shard
58
103
  end
59
104
 
105
+ def periodic_clear_shard_cache
106
+ # TODO: make this configurable
107
+ @timed_cache ||= TimedCache.new(-> { 60.to_i.seconds.ago }) do
108
+ ::Switchman::Shard.clear_cache
109
+ end
110
+ @timed_cache.clear
111
+ end
112
+
60
113
  def delayed_jobs_shards
61
114
  unless instance_variable_defined?(:@delayed_jobs_shards)
62
115
  # re-entrancy protection
@@ -76,6 +129,7 @@ module SwitchmanInstJobs
76
129
 
77
130
  db_dj_shards = ::Switchman::DatabaseServer.all.map do |db|
78
131
  next db.shards.to_a if db.config[:delayed_jobs_shard] == 'self'
132
+
79
133
  db.delayed_jobs_shard
80
134
  end.compact.flatten.uniq # yes, all three
81
135
 
@@ -0,0 +1,19 @@
1
+ module SwitchmanInstJobs
2
+ class TimedCache
3
+ def initialize(timeout, &block)
4
+ @timeout = timeout
5
+ @block = block
6
+ @cached_at = Time.zone.now
7
+ end
8
+
9
+ def clear(force = false)
10
+ if force || @cached_at < @timeout.call
11
+ @block.call
12
+ @cached_at = Time.zone.now
13
+ true
14
+ else
15
+ false
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,3 +1,3 @@
1
1
  module SwitchmanInstJobs
2
- VERSION = '1.3.7'.freeze
2
+ VERSION = '1.5.1'.freeze
3
3
  end
@@ -5,7 +5,7 @@ module SwitchmanInstJobs
5
5
  if object.tag == '!ruby/ActiveRecord:Switchman::Shard'
6
6
  ::Switchman::Shard.lookup(object.value) ||
7
7
  raise(Delayed::Backend::RecordNotFound,
8
- "Couldn't find Switchman::Shard with id #{object.value.inspect}")
8
+ "Couldn't find Switchman::Shard with id #{object.value.inspect}")
9
9
  else
10
10
  super
11
11
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: switchman-inst-jobs
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.7
4
+ version: 1.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bryan Petty
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-11-21 00:00:00.000000000 Z
11
+ date: 2020-08-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: inst-jobs
@@ -16,27 +16,41 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.12.1
19
+ version: '0.16'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '0.16'
22
+ version: '0.17'
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.12.1
29
+ version: '0.16'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '0.16'
32
+ version: '0.17'
33
+ - !ruby/object:Gem::Dependency
34
+ name: parallel
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '1.19'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '1.19'
33
47
  - !ruby/object:Gem::Dependency
34
48
  name: railties
35
49
  requirement: !ruby/object:Gem::Requirement
36
50
  requirements:
37
51
  - - ">="
38
52
  - !ruby/object:Gem::Version
39
- version: '4.2'
53
+ version: '5.2'
40
54
  - - "<"
41
55
  - !ruby/object:Gem::Version
42
56
  version: '6.1'
@@ -46,7 +60,7 @@ dependencies:
46
60
  requirements:
47
61
  - - ">="
48
62
  - !ruby/object:Gem::Version
49
- version: '4.2'
63
+ version: '5.2'
50
64
  - - "<"
51
65
  - !ruby/object:Gem::Version
52
66
  version: '6.1'
@@ -56,20 +70,20 @@ dependencies:
56
70
  requirements:
57
71
  - - ">="
58
72
  - !ruby/object:Gem::Version
59
- version: 1.9.7
73
+ version: '1.14'
60
74
  - - "<"
61
75
  - !ruby/object:Gem::Version
62
- version: '1.15'
76
+ version: '1.16'
63
77
  type: :runtime
64
78
  prerelease: false
65
79
  version_requirements: !ruby/object:Gem::Requirement
66
80
  requirements:
67
81
  - - ">="
68
82
  - !ruby/object:Gem::Version
69
- version: 1.9.7
83
+ version: '1.14'
70
84
  - - "<"
71
85
  - !ruby/object:Gem::Version
72
- version: '1.15'
86
+ version: '1.16'
73
87
  - !ruby/object:Gem::Dependency
74
88
  name: bundler
75
89
  requirement: !ruby/object:Gem::Requirement
@@ -132,14 +146,14 @@ dependencies:
132
146
  requirements:
133
147
  - - "~>"
134
148
  - !ruby/object:Gem::Version
135
- version: '0'
149
+ version: '1.0'
136
150
  type: :development
137
151
  prerelease: false
138
152
  version_requirements: !ruby/object:Gem::Requirement
139
153
  requirements:
140
154
  - - "~>"
141
155
  - !ruby/object:Gem::Version
142
- version: '0'
156
+ version: '1.0'
143
157
  - !ruby/object:Gem::Dependency
144
158
  name: pry
145
159
  requirement: !ruby/object:Gem::Requirement
@@ -160,14 +174,14 @@ dependencies:
160
174
  requirements:
161
175
  - - "~>"
162
176
  - !ruby/object:Gem::Version
163
- version: '12.0'
177
+ version: '12'
164
178
  type: :development
165
179
  prerelease: false
166
180
  version_requirements: !ruby/object:Gem::Requirement
167
181
  requirements:
168
182
  - - "~>"
169
183
  - !ruby/object:Gem::Version
170
- version: '12.0'
184
+ version: '12'
171
185
  - !ruby/object:Gem::Dependency
172
186
  name: rspec
173
187
  requirement: !ruby/object:Gem::Requirement
@@ -202,42 +216,56 @@ dependencies:
202
216
  requirements:
203
217
  - - "~>"
204
218
  - !ruby/object:Gem::Version
205
- version: 0.57.2
219
+ version: 0.79.0
220
+ type: :development
221
+ prerelease: false
222
+ version_requirements: !ruby/object:Gem::Requirement
223
+ requirements:
224
+ - - "~>"
225
+ - !ruby/object:Gem::Version
226
+ version: 0.79.0
227
+ - !ruby/object:Gem::Dependency
228
+ name: rubocop-rails
229
+ requirement: !ruby/object:Gem::Requirement
230
+ requirements:
231
+ - - "~>"
232
+ - !ruby/object:Gem::Version
233
+ version: 2.4.2
206
234
  type: :development
207
235
  prerelease: false
208
236
  version_requirements: !ruby/object:Gem::Requirement
209
237
  requirements:
210
238
  - - "~>"
211
239
  - !ruby/object:Gem::Version
212
- version: 0.57.2
240
+ version: 2.4.2
213
241
  - !ruby/object:Gem::Dependency
214
242
  name: simplecov
215
243
  requirement: !ruby/object:Gem::Requirement
216
244
  requirements:
217
245
  - - "~>"
218
246
  - !ruby/object:Gem::Version
219
- version: '0.14'
247
+ version: '0.18'
220
248
  type: :development
221
249
  prerelease: false
222
250
  version_requirements: !ruby/object:Gem::Requirement
223
251
  requirements:
224
252
  - - "~>"
225
253
  - !ruby/object:Gem::Version
226
- version: '0.14'
254
+ version: '0.18'
227
255
  - !ruby/object:Gem::Dependency
228
256
  name: wwtd
229
257
  requirement: !ruby/object:Gem::Requirement
230
258
  requirements:
231
259
  - - "~>"
232
260
  - !ruby/object:Gem::Version
233
- version: 1.3.0
261
+ version: '1.4'
234
262
  type: :development
235
263
  prerelease: false
236
264
  version_requirements: !ruby/object:Gem::Requirement
237
265
  requirements:
238
266
  - - "~>"
239
267
  - !ruby/object:Gem::Version
240
- version: 1.3.0
268
+ version: '1.4'
241
269
  description:
242
270
  email:
243
271
  - bpetty@instructure.com
@@ -247,6 +275,8 @@ extra_rdoc_files: []
247
275
  files:
248
276
  - db/migrate/20170308045400_add_shard_id_to_delayed_jobs.rb
249
277
  - db/migrate/20180628153808_set_search_paths_on_functions.rb
278
+ - db/migrate/20200818130101_add_on_hold_to_switchman_shards.rb
279
+ - db/migrate/20200822014259_add_block_stranded_to_switchman_shards.rb
250
280
  - lib/switchman-inst-jobs.rb
251
281
  - lib/switchman_inst_jobs.rb
252
282
  - lib/switchman_inst_jobs/active_record/connection_adapters/postgresql_adapter.rb
@@ -257,11 +287,13 @@ files:
257
287
  - lib/switchman_inst_jobs/delayed/worker.rb
258
288
  - lib/switchman_inst_jobs/delayed/worker/health_check.rb
259
289
  - lib/switchman_inst_jobs/engine.rb
290
+ - lib/switchman_inst_jobs/jobs_migrator.rb
260
291
  - lib/switchman_inst_jobs/new_relic.rb
261
292
  - lib/switchman_inst_jobs/shackles.rb
262
293
  - lib/switchman_inst_jobs/switchman/database_server.rb
263
294
  - lib/switchman_inst_jobs/switchman/default_shard.rb
264
295
  - lib/switchman_inst_jobs/switchman/shard.rb
296
+ - lib/switchman_inst_jobs/timed_cache.rb
265
297
  - lib/switchman_inst_jobs/version.rb
266
298
  - lib/switchman_inst_jobs/yaml_extensions.rb
267
299
  homepage: https://github.com/instructure/switchman-inst-jobs
@@ -276,7 +308,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
276
308
  requirements:
277
309
  - - ">="
278
310
  - !ruby/object:Gem::Version
279
- version: '2.3'
311
+ version: '2.4'
280
312
  required_rubygems_version: !ruby/object:Gem::Requirement
281
313
  requirements:
282
314
  - - ">="