switchman 3.0.12 → 3.0.15

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: a1e19102ef71895fd381f6da856f98b71a44102fbbc08b9738f510c18fe198ed
4
- data.tar.gz: acf67d407d42a2d32c617696f66000d358f4fbfbc7b12077e32945c07553853f
3
+ metadata.gz: d009eac65601ef2687150f472d14824f5d55976102257a381a2b7da4e9859131
4
+ data.tar.gz: 37f505b1911705b84053e6c2949f544e0975a1ee4ce99df1db33c53ce5c1df46
5
5
  SHA512:
6
- metadata.gz: 9fad637185836ac6f213b6bd61870fd3390a0f10f66d74fcb0c913977e6c5d394cfee57ceab430800dc0b96a43508f131864b1aad4fb037bea861166fed30dd5
7
- data.tar.gz: 6f3592f8ed5a920a9e556fd0a888ac59ce17ca144f85a3d78077f6a3ab036ec385f7e7070b08fd0db9de93fd54f408315837fc97e118229259e43147b017aae7
6
+ metadata.gz: 05c8ac0cec905fd3922c2271fa2e41dd0ba64890b433cdee1a15ad1255a74827d96fa79bd7f569c51cdd01e92965adc2889e534e63eaa72fae200ece912970d8
7
+ data.tar.gz: f363145f950678a22893653bc6bfd34230c2e650e68151ebdf65ce7174cc14c79fc1dcfb16a1da4f1f4d264f7eae3a16f8766a27696e4659b58ca924d0ce9513
@@ -125,8 +125,7 @@ module Switchman
125
125
  # * +classes+ - an array of classes to activate
126
126
  # parallel: - true/false to execute in parallel, or an integer of how many
127
127
  # sub-processes. Note that parallel invocation currently uses
128
- # forking, so should be used sparingly because you cannot get
129
- # results back
128
+ # forking.
130
129
  # exception: - :ignore, :raise, :defer (wait until the end and raise the first
131
130
  # error), or a proc
132
131
  def with_each_shard(*args, parallel: false, exception: :raise, &block)
@@ -159,137 +158,47 @@ module Switchman
159
158
  return if database_servers.count.zero?
160
159
 
161
160
  scopes = database_servers.to_h do |server|
162
- [server, server.shards.merge(scope)]
161
+ [server, scope.merge(server.shards)]
163
162
  end
164
163
  else
165
164
  scopes = scope.group_by(&:database_server)
166
165
  end
167
166
 
168
- exception_pipes = []
169
- pids = []
170
- out_fds = []
171
- err_fds = []
172
- pid_to_name_map = {}
173
- fd_to_name_map = {}
174
- errors = []
175
-
176
- wait_for_output = lambda do
177
- ready, = IO.select(out_fds + err_fds)
178
- ready.each do |fd|
179
- if fd.eof?
180
- fd.close
181
- out_fds.delete(fd)
182
- err_fds.delete(fd)
183
- next
184
- end
185
- line = fd.readline
186
- puts "#{fd_to_name_map[fd]}: #{line}"
187
- end
188
- end
189
-
190
- # only one process; don't bother forking
191
- return with_each_shard(scopes.first.last, classes, exception: exception, &block) if scopes.length == 1
192
-
193
167
  # clear connections prior to forking (no more queries will be executed in the parent,
194
168
  # and we want them gone so that we don't accidentally use them post-fork doing something
195
169
  # silly like dealloc'ing prepared statements)
196
170
  ::ActiveRecord::Base.clear_all_connections!
197
171
 
198
- scopes.each do |server, subscope|
172
+ parent_process_name = `ps -ocommand= -p#{Process.pid}`.slice(/#{$0}.*/)
173
+ ret = ::Parallel.map(scopes, in_processes: scopes.length > 1 ? parallel : 0) do |server, subscope|
199
174
  name = server.id
200
-
201
- exception_pipe = IO.pipe
202
- exception_pipes << exception_pipe
203
- pid, io_in, io_out, io_err = Open4.pfork4(lambda do
204
- Switchman.config[:on_fork_proc]&.call
205
-
206
- # set a pretty name for the process title, up to 128 characters
207
- # (we don't actually know the limit, depending on how the process
208
- # was started)
209
- # first, simplify the binary name by stripping directories,
210
- # then truncate arguments as necessary
211
- bin = File.basename($0) # Process.argv0 doesn't work on Ruby 2.5 (https://bugs.ruby-lang.org/issues/15887)
212
- max_length = 128 - bin.length - name.length - 3
213
- args = ARGV.join(' ')
214
- args = args[0..max_length] if max_length >= 0
215
- new_title = [bin, args, name].join(' ')
175
+ # rubocop:disable Style/GlobalStdStream
176
+ $stdout = Parallel::PrefixingIO.new(name, STDOUT)
177
+ $stderr = Parallel::PrefixingIO.new(name, STDERR)
178
+ # rubocop:enable Style/GlobalStdStream
179
+ begin
180
+ max_length = 128 - name.length - 3
181
+ short_parent_name = parent_process_name[0..max_length] if max_length >= 0
182
+ new_title = [short_parent_name, name].join(' ')
216
183
  Process.setproctitle(new_title)
217
-
184
+ Switchman.config[:on_fork_proc]&.call
218
185
  with_each_shard(subscope, classes, exception: exception, &block)
219
- exception_pipe.last.close
220
- rescue Exception => e # rubocop:disable Lint/RescueException
221
- begin
222
- dumped = Marshal.dump(e)
223
- dumped = nil if dumped.length > 64 * 1024
224
- rescue
225
- dumped = nil
226
- end
227
-
228
- if dumped.nil?
229
- # couldn't dump the exception; create a copy with just
230
- # the message and the backtrace
231
- e2 = e.class.new(e.message)
232
- backtrace = e.backtrace
233
- # truncate excessively long backtraces
234
- backtrace = backtrace[0...25] + ['...'] + backtrace[-25..] if backtrace.length > 50
235
- e2.set_backtrace(backtrace)
236
- e2.instance_variable_set(:@active_shards, e.instance_variable_get(:@active_shards))
237
- dumped = Marshal.dump(e2)
238
- end
239
- exception_pipe.last.set_encoding(dumped.encoding)
240
- exception_pipe.last.write(dumped)
241
- exception_pipe.last.flush
242
- exception_pipe.last.close
243
- exit! 1
244
- end)
245
- exception_pipe.last.close
246
- pids << pid
247
- io_in.close # don't care about writing to stdin
248
- out_fds << io_out
249
- err_fds << io_err
250
- pid_to_name_map[pid] = name
251
- fd_to_name_map[io_out] = name
252
- fd_to_name_map[io_err] = name
253
-
254
- while pids.count >= parallel
255
- while out_fds.count >= parallel
256
- # wait for output if we've hit the parallel limit
257
- wait_for_output.call
258
- end
259
- # we've gotten all the output from one fd so wait for its child process to exit
260
- found_pid, status = Process.wait2
261
- pids.delete(found_pid)
262
- errors << pid_to_name_map[found_pid] if status.exitstatus != 0
186
+ rescue => e
187
+ logger.error e.full_message
188
+ Parallel::QuietExceptionWrapper.new(name, ::Parallel::ExceptionWrapper.new(e))
263
189
  end
264
- # we've gotten all the output from one fd so wait for its child process to exit
265
- found_pid, status = Process.wait2
266
- pids.delete(found_pid)
267
- errors << pid_to_name_map[found_pid] if status.exitstatus != 0
268
- end
269
-
270
- wait_for_output.call while out_fds.any? || err_fds.any?
271
- pids.each do |pid|
272
- _, status = Process.waitpid2(pid)
273
- errors << pid_to_name_map[pid] if status.exitstatus != 0
274
- end
275
-
276
- # check for an exception; we only re-raise the first one
277
- exception_pipes.each do |exception_pipe|
278
- serialized_exception = exception_pipe.first.read
279
- next if serialized_exception.empty?
280
-
281
- ex = Marshal.load(serialized_exception) # rubocop:disable Security/MarshalLoad
282
- raise ex
283
- ensure
284
- exception_pipe.first.close
285
- end
190
+ end.flatten
286
191
 
192
+ errors = ret.select { |val| val.is_a?(Parallel::QuietExceptionWrapper) }
287
193
  unless errors.empty?
194
+ raise errors.first.exception if errors.length == 1
195
+
288
196
  raise ParallelShardExecError,
289
- "The following subprocesses did not exit cleanly: #{errors.sort.join(', ')}"
197
+ "The following database server(s) did not finish processing cleanly: #{errors.map(&:name).sort.join(', ')}",
198
+ cause: errors.first.exception
290
199
  end
291
200
 
292
- return
201
+ return ret
293
202
  end
294
203
 
295
204
  classes ||= []
@@ -3,6 +3,20 @@
3
3
  module Switchman
4
4
  module ActiveRecord
5
5
  module DatabaseConfigurations
6
+ # key difference: For each env name, ensure only one writable config is returned
7
+ # since all should point to the same data, even if multiple are writable
8
+ # (Picks 'primary' since it is guaranteed to exist and switchman handles activating
9
+ # deploy through other means)
10
+ def configs_for(include_replicas: false, name: nil, **)
11
+ res = super
12
+ if name && !include_replicas
13
+ return nil unless name.end_with?('primary')
14
+ elsif !include_replicas
15
+ return res.select { |config| config.name.end_with?('primary') }
16
+ end
17
+ res
18
+ end
19
+
6
20
  private
7
21
 
8
22
  # key difference: assumes a hybrid two-tier structure; each third tier
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parallel'
4
+
5
+ module Switchman
6
+ module Parallel
7
+ module UndumpableException
8
+ def initialize(original)
9
+ super
10
+ @active_shards = original.instance_variable_get(:@active_shards)
11
+ current_shard
12
+ end
13
+ end
14
+
15
+ class QuietExceptionWrapper
16
+ attr_accessor :name
17
+
18
+ def initialize(name, wrapper)
19
+ @name = name
20
+ @wrapper = wrapper
21
+ end
22
+
23
+ def exception
24
+ @wrapper.exception
25
+ end
26
+ end
27
+
28
+ class PrefixingIO
29
+ delegate_missing_to :@original_io
30
+
31
+ def initialize(prefix, original_io)
32
+ @prefix = prefix
33
+ @original_io = original_io
34
+ end
35
+
36
+ def puts(*args)
37
+ args.flatten.each { |arg| @original_io.puts "#{@prefix}: #{arg}" }
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ ::Parallel::UndumpableException.prepend(::Switchman::Parallel::UndumpableException)
@@ -70,7 +70,10 @@ module Switchman
70
70
  Shard.default(reload: true)
71
71
  puts 'Done!'
72
72
 
73
+ main_pid = Process.pid
73
74
  at_exit do
75
+ next unless main_pid == Process.pid
76
+
74
77
  # preserve rspec's exit status
75
78
  status = $!.is_a?(::SystemExit) ? $!.status : nil
76
79
  puts 'Tearing down sharding for all specs'
@@ -6,13 +6,14 @@ module Switchman
6
6
  super
7
7
  # These seem to get themselves into a bad state if we try to lookup shards while processing
8
8
  return if is_a?(IO::EAGAINWaitReadable)
9
+
9
10
  return if Thread.current[:switchman_error_handler]
10
11
 
11
12
  begin
12
13
  Thread.current[:switchman_error_handler] = true
13
14
 
14
15
  begin
15
- @active_shards = Shard.active_shards if defined?(Shard)
16
+ @active_shards ||= Shard.active_shards if defined?(Shard)
16
17
  rescue
17
18
  # If we hit an error really early in boot, activerecord may not be initialized yet
18
19
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Switchman
4
- VERSION = '3.0.12'
4
+ VERSION = '3.0.15'
5
5
  end
data/lib/switchman.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'guard_rail'
4
- require 'switchman/open4'
4
+ require 'switchman/parallel'
5
5
  require 'switchman/engine'
6
6
 
7
7
  module Switchman
@@ -1,5 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # In rails 7.0+ if you have only 1 db in the env it doesn't try to do explicit activation
4
+ # (and for rails purposes we only have one db per env because each database server is a separate env)
5
+ if Rails.version < '7.0'
6
+ task_prefix = ::Rake::Task.task_defined?('app:db:migrate') ? 'app:db' : 'db'
7
+ ::Rake::Task["#{task_prefix}:migrate"].clear_actions.enhance do
8
+ ::ActiveRecord::Tasks::DatabaseTasks.migrate
9
+ # Ensure this doesn't blow up when running inside the dummy app
10
+ Rake::Task["#{task_prefix}:_dump"].invoke
11
+ end
12
+ end
13
+
3
14
  module Switchman
4
15
  module Rake
5
16
  def self.filter_database_servers(&block)
@@ -35,9 +46,9 @@ module Switchman
35
46
 
36
47
  scope = base_scope.order(::Arel.sql('database_server_id IS NOT NULL, database_server_id, id'))
37
48
  if servers != DatabaseServer.all
38
- conditions = ['database_server_id IN (?)', servers.map(&:id)]
39
- conditions.first << ' OR database_server_id IS NULL' if servers.include?(Shard.default.database_server)
40
- scope = scope.where(conditions)
49
+ database_server_ids = servers.map(&:id)
50
+ database_server_ids << nil if servers.include?(Shard.default.database_server)
51
+ scope = scope.where(database_server_id: database_server_ids)
41
52
  end
42
53
 
43
54
  scope = shard_scope(scope, shard) if shard
@@ -50,9 +61,7 @@ module Switchman
50
61
  end
51
62
 
52
63
  # classes - an array or proc, to activate as the current shard during the
53
- # task. tasks which modify the schema may want to pass all categories in
54
- # so that schema updates for non-default tables happen against all shards.
55
- # this is handled automatically for the default migration tasks, below.
64
+ # task.
56
65
  def self.shardify_task(task_name, classes: [::ActiveRecord::Base])
57
66
  old_task = ::Rake::Task[task_name]
58
67
  old_actions = old_task.actions.dup
@@ -77,10 +86,8 @@ module Switchman
77
86
  nil
78
87
  end
79
88
  rescue => e
80
- puts "Exception from #{e.current_shard.id}: #{e.current_shard.description}" if options[:parallel] != 0
89
+ warn "Exception from #{e.current_shard.id}: #{e.current_shard.description}:\n#{e.full_message}" if options[:parallel] != 0
81
90
  raise
82
-
83
- # ::ActiveRecord::Base.configurations = old_configurations
84
91
  end
85
92
  end
86
93
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: switchman
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.12
4
+ version: 3.0.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2022-03-22 00:00:00.000000000 Z
13
+ date: 2022-03-24 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -47,19 +47,19 @@ dependencies:
47
47
  - !ruby/object:Gem::Version
48
48
  version: 3.0.0
49
49
  - !ruby/object:Gem::Dependency
50
- name: open4
50
+ name: parallel
51
51
  requirement: !ruby/object:Gem::Requirement
52
52
  requirements:
53
53
  - - "~>"
54
54
  - !ruby/object:Gem::Version
55
- version: 1.3.0
55
+ version: '1.22'
56
56
  type: :runtime
57
57
  prerelease: false
58
58
  version_requirements: !ruby/object:Gem::Requirement
59
59
  requirements:
60
60
  - - "~>"
61
61
  - !ruby/object:Gem::Version
62
- version: 1.3.0
62
+ version: '1.22'
63
63
  - !ruby/object:Gem::Dependency
64
64
  name: railties
65
65
  requirement: !ruby/object:Gem::Requirement
@@ -287,7 +287,7 @@ files:
287
287
  - lib/switchman/errors.rb
288
288
  - lib/switchman/guard_rail.rb
289
289
  - lib/switchman/guard_rail/relation.rb
290
- - lib/switchman/open4.rb
290
+ - lib/switchman/parallel.rb
291
291
  - lib/switchman/r_spec_helper.rb
292
292
  - lib/switchman/rails.rb
293
293
  - lib/switchman/sharded_instrumenter.rb
@@ -1,80 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'open4'
4
-
5
- # This fixes a bug with exception handling,
6
- # see https://github.com/ahoward/open4/pull/30
7
- module Open4
8
- def self.do_popen(b = nil, exception_propagation_at = nil, closefds=false, &cmd)
9
- pw, pr, pe, ps = IO.pipe, IO.pipe, IO.pipe, IO.pipe
10
-
11
- verbose = $VERBOSE
12
- begin
13
- $VERBOSE = nil
14
-
15
- cid = fork {
16
- if closefds
17
- exlist = [0, 1, 2] | [pw,pr,pe,ps].map{|p| [p.first.fileno, p.last.fileno] }.flatten
18
- ObjectSpace.each_object(IO){|io|
19
- io.close if (not io.closed?) and (not exlist.include? io.fileno) rescue nil
20
- }
21
- end
22
-
23
- pw.last.close
24
- STDIN.reopen pw.first
25
- pw.first.close
26
-
27
- pr.first.close
28
- STDOUT.reopen pr.last
29
- pr.last.close
30
-
31
- pe.first.close
32
- STDERR.reopen pe.last
33
- pe.last.close
34
-
35
- STDOUT.sync = STDERR.sync = true
36
-
37
- begin
38
- cmd.call(ps)
39
- rescue Exception => e
40
- begin
41
- Marshal.dump(e, ps.last)
42
- ps.last.flush
43
- rescue Errno::EPIPE
44
- raise e
45
- end
46
- ensure
47
- ps.last.close unless ps.last.closed?
48
- end
49
-
50
- exit!
51
- }
52
- ensure
53
- $VERBOSE = verbose
54
- end
55
-
56
- [ pw.first, pr.last, pe.last, ps.last ].each { |fd| fd.close }
57
-
58
- Open4.propagate_exception cid, ps.first if exception_propagation_at == :init
59
-
60
- pw.last.sync = true
61
-
62
- pi = [ pw.last, pr.first, pe.first ]
63
-
64
- begin
65
- return [cid, *pi] unless b
66
-
67
- begin
68
- b.call(cid, *pi)
69
- ensure
70
- pi.each { |fd| fd.close unless fd.closed? }
71
- end
72
-
73
- Open4.propagate_exception cid, ps.first if exception_propagation_at == :block
74
-
75
- Process.waitpid2(cid).last
76
- ensure
77
- ps.first.close unless ps.first.closed?
78
- end
79
- end
80
- end