switchman 3.0.12 → 3.0.15

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: 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