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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9f1525c9c7e0f08abefab65094c1479bed9c042585b627022ef5f3d97f05a798
4
- data.tar.gz: d47f434d7869d9898e04222b232230c5b54b6c9c11dc390b387438d169dcec67
3
+ metadata.gz: f447f30d1f541b4fe85ad0d867276957b5ed583ddd01e70c509834c0441892bc
4
+ data.tar.gz: 677a35494a2b54c609017b335019a98510ca3db04e26e61778ad5a8abc0f9377
5
5
  SHA512:
6
- metadata.gz: 13ea49df7bfd9c7430c6d6261d11510697ce2c91e62e175cb94d12bc03eb0f6174b5153e35ddf1fe8357a5d20e389b13046aae42d9658016f3ecf7174691c45c
7
- data.tar.gz: 7c4ab126b6527b164aa5f278dda90755f15a39d9bc0c2d94cbbabac3d7c3f7c493fcef5009f7d35f4d836c756538d563e00ed3cc96657a7e356ee860c726c7f3
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('failed_at IS NOT NULL').find_each do |job|
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
- .map { |w| w['shard'] }.compact.uniq
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.where(id: shard_id).exists?
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 send_later_enqueue_args(method, _enqueue_args = {}, *args)
5
- return send(method, *args) if ::Switchman::DatabaseServer.creating_new_shard
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].map { |c| c[:shard] }.uniq
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
- send_later_enqueue_args(
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
- # 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)
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("Waiting for caches to clear (#{source_shard.id} -> #{target_shard.id})")
49
+ Rails.logger.debug('Waiting for caches to clear')
43
50
  sleep(65) unless @skip_cache_wait
44
51
 
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 }
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('strand IS NOT NULL')
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('locked_by IS NOT NULL').next_in_strand_order.lock.first
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
- .update_all(next_in_strand: false)
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.find(source_shard_ids).each do |shard|
127
- shard.update(block_stranded: false)
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
- # 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
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
- # 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)
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") + "\n")
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.first
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
- .where.not(locked_at: nil)
33
- .where.not(locked_by: ::Delayed::Backend::Base::ON_HOLD_LOCKED_BY).exists?
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
- .in_batches(of: 10_000)
46
- .update_all(
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 instance_variable_defined?(:@delayed_jobs_shards)
115
- # re-entrancy protection
116
- @delayed_jobs_shards = begin
117
- shard_dj_shards = [] unless ::Switchman::Shard.columns_hash.key?('delayed_jobs_shard_id')
118
- shard_dj_shards ||= begin
119
- ::Switchman::Shard
120
- .where.not(delayed_jobs_shard_id: nil)
121
- .distinct
122
- .pluck(:delayed_jobs_shard_id)
123
- .map { |id| ::Switchman::Shard.lookup(id) }
124
- .compact
125
- end
126
- # set it temporarily, to avoid the default shard falling back to itself
127
- # if other shards are usable
128
- @delayed_jobs_shards = shard_dj_shards.uniq.sort
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
- @delayed_jobs_shards
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
@@ -6,7 +6,7 @@ module SwitchmanInstJobs
6
6
  @cached_at = Time.zone.now
7
7
  end
8
8
 
9
- def clear(force = false)
9
+ def clear(force: false)
10
10
  if force || @cached_at < @timeout.call
11
11
  @block.call
12
12
  @cached_at = Time.zone.now
@@ -1,3 +1,3 @@
1
1
  module SwitchmanInstJobs
2
- VERSION = '2.0.0'.freeze
2
+ VERSION = '3.0.4'.freeze
3
3
  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: 2.0.0
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-10-06 00:00:00.000000000 Z
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.16'
20
- - - "<"
19
+ version: '1.0'
20
+ - - ">="
21
21
  - !ruby/object:Gem::Version
22
- version: '0.17'
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.16'
30
- - - "<"
29
+ version: '1.0'
30
+ - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: '0.17'
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: '12'
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: '12'
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: 0.79.0
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: 0.79.0
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.4.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.4.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.2
337
+ rubygems_version: 3.1.4
338
338
  signing_key:
339
339
  specification_version: 4
340
340
  summary: Switchman and Instructure Jobs compatibility gem.