switchman 2.1.4 → 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: d96c8878af2c919433712e502ab527fd77e811eeb88a652357cc004fb5a40ac7
4
- data.tar.gz: b8f9e1cb90e7dfbfb25ea02a8699a6c8e20d305f60a13f062a1d71d1da27816e
3
+ metadata.gz: 34560d2af725df11f4d5f35c512ee53f5e0ad4b614e7caf3d8558895c754c096
4
+ data.tar.gz: ab3bdc95958fe37f8210dd853e363394bf564c5600c4750cdb2cba408515a5dc
5
5
  SHA512:
6
- metadata.gz: bb9130947cbff85fef69fdb3f3ac54c29aa9cf7f95507faf92ac97673828addeebe58c9ff1fc478ff34daa834e2f42774a81a99b5bd42dde7e06e3fd63ff6dca
7
- data.tar.gz: 1e9fd256868e5b35a0f8d34c5726af34bc7381d76f77f9988d623da84b510959a4ff8563956c797c4921961a8540dd1f8fe74882df9027b4946b93e087309284
6
+ metadata.gz: 1b703818c43d4bdf7f39bb628e80b8d088aedb3b860e383339a99015da2d9780f78fa3eabd0b3e7f813681388f72155979e9de13eb1e2bdefcfeb9a5298d42a1
7
+ data.tar.gz: 5ea941cdbb24705af946fe34c583ee323ae5e8a9ac92808c67b8d6972c3ae52b04a3e4e724f824b2a9fef17ce2e1be9a6791498e3922ddea59a16933d9aa0fda
@@ -129,6 +129,11 @@ module Switchman
129
129
  cached_shards[id]
130
130
  end
131
131
 
132
+ def preload_cache
133
+ cached_shards.reverse_merge!(active_shards.values.index_by(&:id))
134
+ cached_shards.reverse_merge!(all.index_by(&:id))
135
+ end
136
+
132
137
  def clear_cache
133
138
  cached_shards.clear
134
139
  end
@@ -139,10 +144,9 @@ module Switchman
139
144
  # * +categories+ - an array of categories to activate
140
145
  # * +options+ -
141
146
  # :parallel - true/false to execute in parallel, or a integer of how many
142
- # sub-processes per database server. Note that parallel
143
- # invocation currently uses forking, so should be used sparingly
144
- # because errors are not raised, and you cannot get results back
145
- # :max_procs - only run this many parallel processes at a time
147
+ # sub-processes. Note that parallel invocation currently uses
148
+ # forking, so should be used sparingly because you cannot get
149
+ # results back
146
150
  # :exception - :ignore, :raise, :defer (wait until the end and raise the first
147
151
  # error), or a proc
148
152
  def with_each_shard(*args)
@@ -163,11 +167,14 @@ module Switchman
163
167
  scope, categories = args
164
168
  end
165
169
 
170
+ # back-compat
171
+ options[:parallel] = options.delete(:max_procs) if options.key?(:max_procs)
172
+
166
173
  parallel = case options[:parallel]
167
174
  when true
168
- 1
175
+ [Environment.cpu_count || 2, 2].min
169
176
  when false, nil
170
- 0
177
+ 1
171
178
  else
172
179
  options[:parallel]
173
180
  end
@@ -178,47 +185,19 @@ module Switchman
178
185
  scope = scope.order(::Arel.sql("database_server_id IS NOT NULL, database_server_id, id"))
179
186
  end
180
187
 
181
- if parallel > 0
182
- max_procs = determine_max_procs(options.delete(:max_procs), parallel)
188
+ if parallel > 1
183
189
  if ::ActiveRecord::Relation === scope
184
190
  # still need a post-uniq, cause the default database server could be NULL or Rails.env in the db
185
191
  database_servers = scope.reorder('database_server_id').select(:database_server_id).distinct.
186
192
  map(&:database_server).compact.uniq
187
193
  # nothing to do
188
194
  return if database_servers.count == 0
189
- parallel = [(max_procs.to_f / database_servers.count).ceil, parallel].min if max_procs
190
195
 
191
196
  scopes = Hash[database_servers.map do |server|
192
- server_scope = server.shards.merge(scope)
193
- if parallel == 1
194
- subscopes = [server_scope]
195
- else
196
- subscopes = []
197
- total = server_scope.count
198
- ranges = []
199
- server_scope.find_ids_in_ranges(:batch_size => (total.to_f / parallel).ceil) do |min, max|
200
- ranges << [min, max]
201
- end
202
- # create a half-open range on the last one
203
- ranges.last[1] = nil
204
- ranges.each do |min, max|
205
- subscope = server_scope.where("id>=?", min)
206
- subscope = subscope.where("id<=?", max) if max
207
- subscopes << subscope
208
- end
209
- end
210
- [server, subscopes]
197
+ [server, server.shards.merge(scope)]
211
198
  end]
212
199
  else
213
200
  scopes = scope.group_by(&:database_server)
214
- if parallel > 1
215
- parallel = [(max_procs.to_f / scopes.count).ceil, parallel].min if max_procs
216
- scopes = Hash[scopes.map do |(server, shards)|
217
- [server, shards.in_groups(parallel, false).compact]
218
- end]
219
- else
220
- scopes = Hash[scopes.map { |(server, shards)| [server, [shards]] }]
221
- end
222
201
  end
223
202
 
224
203
  exception_pipes = []
@@ -244,8 +223,8 @@ module Switchman
244
223
  end
245
224
 
246
225
  # only one process; don't bother forking
247
- if scopes.length == 1 && parallel == 1
248
- return with_each_shard(scopes.first.last.first, categories, options) { yield }
226
+ if scopes.length == 1
227
+ return with_each_shard(scopes.first.last, categories, options) { yield }
249
228
  end
250
229
 
251
230
  # clear connections prior to forking (no more queries will be executed in the parent,
@@ -253,84 +232,78 @@ module Switchman
253
232
  # silly like dealloc'ing prepared statements)
254
233
  ::ActiveRecord::Base.clear_all_connections!
255
234
 
256
- scopes.each do |server, subscopes|
257
- subscopes.each_with_index do |subscope, idx|
258
- if subscopes.length > 1
259
- name = "#{server.id} #{idx + 1}"
260
- else
261
- name = server.id
262
- end
235
+ scopes.each do |server, subscope|
236
+ name = server.id
237
+
238
+ exception_pipe = IO.pipe
239
+ exception_pipes << exception_pipe
240
+ pid, io_in, io_out, io_err = Open4.pfork4(lambda do
241
+ begin
242
+ Switchman.config[:on_fork_proc]&.call
243
+
244
+ # set a pretty name for the process title, up to 128 characters
245
+ # (we don't actually know the limit, depending on how the process
246
+ # was started)
247
+ # first, simplify the binary name by stripping directories,
248
+ # then truncate arguments as necessary
249
+ bin = File.basename($0) # Process.argv0 doesn't work on Ruby 2.5 (https://bugs.ruby-lang.org/issues/15887)
250
+ max_length = 128 - bin.length - name.length - 3
251
+ args = ARGV.join(" ")
252
+ if max_length >= 0
253
+ args = args[0..max_length]
254
+ end
255
+ new_title = [bin, args, name].join(" ")
256
+ Process.setproctitle(new_title)
263
257
 
264
- exception_pipe = IO.pipe
265
- exception_pipes << exception_pipe
266
- pid, io_in, io_out, io_err = Open4.pfork4(lambda do
258
+ with_each_shard(subscope, categories, options) { yield }
259
+ exception_pipe.last.close
260
+ rescue Exception => e
267
261
  begin
268
- Switchman.config[:on_fork_proc]&.call
269
-
270
- # set a pretty name for the process title, up to 128 characters
271
- # (we don't actually know the limit, depending on how the process
272
- # was started)
273
- # first, simplify the binary name by stripping directories,
274
- # then truncate arguments as necessary
275
- bin = File.basename($0) # Process.argv0 doesn't work on Ruby 2.5 (https://bugs.ruby-lang.org/issues/15887)
276
- max_length = 128 - bin.length - name.length - 3
277
- args = ARGV.join(" ")
278
- if max_length >= 0
279
- args = args[0..max_length]
280
- end
281
- new_title = [bin, args, name].join(" ")
282
- Process.setproctitle(new_title)
283
-
284
- with_each_shard(subscope, categories, options) { yield }
285
- exception_pipe.last.close
286
- rescue Exception => e
287
- begin
288
- dumped = Marshal.dump(e)
289
- dumped = nil if dumped.length > 64 * 1024
290
- rescue
291
- dumped = nil
292
- end
262
+ dumped = Marshal.dump(e)
263
+ dumped = nil if dumped.length > 64 * 1024
264
+ rescue
265
+ dumped = nil
266
+ end
293
267
 
294
- if dumped.nil?
295
- # couldn't dump the exception; create a copy with just
296
- # the message and the backtrace
297
- e2 = e.class.new(e.message)
298
- backtrace = e.backtrace
299
- # truncate excessively long backtraces
300
- if backtrace.length > 50
301
- backtrace = backtrace[0...25] + ['...'] + backtrace[-25..-1]
302
- end
303
- e2.set_backtrace(backtrace)
304
- e2.instance_variable_set(:@active_shards, e.instance_variable_get(:@active_shards))
305
- dumped = Marshal.dump(e2)
268
+ if dumped.nil?
269
+ # couldn't dump the exception; create a copy with just
270
+ # the message and the backtrace
271
+ e2 = e.class.new(e.message)
272
+ backtrace = e.backtrace
273
+ # truncate excessively long backtraces
274
+ if backtrace.length > 50
275
+ backtrace = backtrace[0...25] + ['...'] + backtrace[-25..-1]
306
276
  end
307
-
308
- exception_pipe.last.set_encoding(dumped.encoding)
309
- exception_pipe.last.write(dumped)
310
- exception_pipe.last.flush
311
- exception_pipe.last.close
312
- exit! 1
277
+ e2.set_backtrace(backtrace)
278
+ e2.instance_variable_set(:@active_shards, e.instance_variable_get(:@active_shards))
279
+ dumped = Marshal.dump(e2)
313
280
  end
314
- end)
315
- exception_pipe.last.close
316
- pids << pid
317
- io_in.close # don't care about writing to stdin
318
- out_fds << io_out
319
- err_fds << io_err
320
- pid_to_name_map[pid] = name
321
- fd_to_name_map[io_out] = name
322
- fd_to_name_map[io_err] = name
323
-
324
- while max_procs && pids.count >= max_procs
325
- while max_procs && out_fds.count >= max_procs
326
- # wait for output if we've hit the max_procs limit
327
- wait_for_output.call(out_fds, err_fds, fd_to_name_map)
328
- end
329
- # we've gotten all the output from one fd so wait for its child process to exit
330
- found_pid, status = Process.wait2
331
- pids.delete(found_pid)
332
- errors << pid_to_name_map[found_pid] if status.exitstatus != 0
281
+
282
+ exception_pipe.last.set_encoding(dumped.encoding)
283
+ exception_pipe.last.write(dumped)
284
+ exception_pipe.last.flush
285
+ exception_pipe.last.close
286
+ exit! 1
287
+ end
288
+ end)
289
+ exception_pipe.last.close
290
+ pids << pid
291
+ io_in.close # don't care about writing to stdin
292
+ out_fds << io_out
293
+ err_fds << io_err
294
+ pid_to_name_map[pid] = name
295
+ fd_to_name_map[io_out] = name
296
+ fd_to_name_map[io_err] = name
297
+
298
+ while pids.count >= parallel
299
+ while out_fds.count >= parallel
300
+ # wait for output if we've hit the parallel limit
301
+ wait_for_output.call(out_fds, err_fds, fd_to_name_map)
333
302
  end
303
+ # we've gotten all the output from one fd so wait for its child process to exit
304
+ found_pid, status = Process.wait2
305
+ pids.delete(found_pid)
306
+ errors << pid_to_name_map[found_pid] if status.exitstatus != 0
334
307
  end
335
308
  end
336
309
 
@@ -544,24 +517,6 @@ module Switchman
544
517
  shard || source_shard || Shard.current
545
518
  end
546
519
 
547
- # given the provided option, determines whether we need to (and whether
548
- # it's possible) to determine a reasonable default.
549
- def determine_max_procs(max_procs_input, parallel_input=2)
550
- max_procs = nil
551
- if max_procs_input
552
- max_procs = max_procs_input.to_i
553
- max_procs = nil if max_procs == 0
554
- else
555
- return 1 if parallel_input.nil? || parallel_input < 1
556
- cpus = Environment.cpu_count
557
- if cpus && cpus > 0
558
- max_procs = cpus * parallel_input
559
- end
560
- end
561
-
562
- return max_procs
563
- end
564
-
565
520
  private
566
521
  # in-process caching
567
522
  def cached_shards
@@ -158,7 +158,7 @@ module Switchman
158
158
  end
159
159
 
160
160
  def update_columns(*)
161
- db = Shard.current(self.class.shard_category).database_server
161
+ db = shard.database_server
162
162
  if ::GuardRail.environment != db.guard_rail_environment
163
163
  return db.unguard { super }
164
164
  else
@@ -30,10 +30,13 @@ module Switchman
30
30
  end
31
31
 
32
32
  module Migrator
33
- # significant change: hash shard id, not database name
33
+ # significant change: just return MIGRATOR_SALT directly
34
+ # especially if you're going through pgbouncer, the database
35
+ # name you're accessing may not be consistent. it is NOT allowed
36
+ # to run migrations against multiple shards in the same database
37
+ # concurrently
34
38
  def generate_migrator_advisory_lock_id
35
- shard_name_hash = Zlib.crc32(Shard.current.name)
36
- ::ActiveRecord::Migrator::MIGRATOR_SALT * shard_name_hash
39
+ ::ActiveRecord::Migrator::MIGRATOR_SALT
37
40
  end
38
41
 
39
42
  if ::Rails.version >= '6.0'
@@ -13,6 +13,15 @@ module Switchman
13
13
  shard.activate(self.class.shard_category) { super }
14
14
  end
15
15
  end
16
+
17
+ def delete
18
+ db = shard.database_server
19
+ if ::GuardRail.environment != db.guard_rail_environment
20
+ return db.unguard { super }
21
+ else
22
+ super
23
+ end
24
+ end
16
25
  end
17
26
  end
18
- end
27
+ end
@@ -102,7 +102,7 @@ module Switchman
102
102
  WHERE i.relkind = 'i'
103
103
  AND d.indisprimary = 'f'
104
104
  AND t.relname = '#{table_name}'
105
- AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = '#{shard.name}' )
105
+ AND t.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = '#{shard.name}' )
106
106
  ORDER BY i.relname
107
107
  SQL
108
108
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Switchman
4
- VERSION = "2.1.4"
4
+ VERSION = "2.2.1"
5
5
  end
@@ -46,7 +46,8 @@ module Switchman
46
46
  end
47
47
 
48
48
  def self.options
49
- { parallel: ENV['PARALLEL'].to_i, max_procs: ENV['MAX_PARALLEL_PROCS'] }
49
+ # we still pass through both of these options for back-compat purposes
50
+ { parallel: ENV['PARALLEL']&.to_i, max_procs: ENV['MAX_PARALLEL_PROCS']&.to_i }
50
51
  end
51
52
 
52
53
  # categories - an array or proc, to activate as the current shard during the
@@ -89,7 +90,7 @@ module Switchman
89
90
  nil
90
91
  end
91
92
  rescue => e
92
- puts "Exception from #{e.current_shard.id}: #{e.current_shard.description}" if options[:parallel] != 0
93
+ puts "Exception from #{e.current_shard.id}: #{e.current_shard.description}" if options[:parallel].to_i != 0
93
94
  raise
94
95
  end
95
96
  end
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: switchman
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.4
4
+ version: 2.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer
8
8
  - James Williams
9
9
  - Jacob Fugal
10
- autorequire:
10
+ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2021-07-29 00:00:00.000000000 Z
13
+ date: 2021-08-25 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: railties
@@ -257,7 +257,7 @@ homepage: http://www.instructure.com/
257
257
  licenses:
258
258
  - MIT
259
259
  metadata: {}
260
- post_install_message:
260
+ post_install_message:
261
261
  rdoc_options: []
262
262
  require_paths:
263
263
  - lib
@@ -272,8 +272,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
272
272
  - !ruby/object:Gem::Version
273
273
  version: '0'
274
274
  requirements: []
275
- rubygems_version: 3.0.3
276
- signing_key:
275
+ rubygems_version: 3.2.24
276
+ signing_key:
277
277
  specification_version: 4
278
278
  summary: Rails sharding magic
279
279
  test_files: []