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 +4 -4
- data/app/models/switchman/shard.rb +23 -114
- data/lib/switchman/active_record/database_configurations.rb +14 -0
- data/lib/switchman/parallel.rb +43 -0
- data/lib/switchman/r_spec_helper.rb +3 -0
- data/lib/switchman/standard_error.rb +2 -1
- data/lib/switchman/version.rb +1 -1
- data/lib/switchman.rb +1 -1
- data/lib/tasks/switchman.rake +16 -9
- metadata +6 -6
- data/lib/switchman/open4.rb +0 -80
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d009eac65601ef2687150f472d14824f5d55976102257a381a2b7da4e9859131
|
4
|
+
data.tar.gz: 37f505b1911705b84053e6c2949f544e0975a1ee4ce99df1db33c53ce5c1df46
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
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
|
-
|
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
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
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
|
-
|
220
|
-
|
221
|
-
|
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
|
-
|
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
|
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
|
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
|
data/lib/switchman/version.rb
CHANGED
data/lib/switchman.rb
CHANGED
data/lib/tasks/switchman.rake
CHANGED
@@ -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
|
-
|
39
|
-
|
40
|
-
scope = scope.where(
|
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.
|
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
|
-
|
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.
|
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-
|
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:
|
50
|
+
name: parallel
|
51
51
|
requirement: !ruby/object:Gem::Requirement
|
52
52
|
requirements:
|
53
53
|
- - "~>"
|
54
54
|
- !ruby/object:Gem::Version
|
55
|
-
version: 1.
|
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.
|
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/
|
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
|
data/lib/switchman/open4.rb
DELETED
@@ -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
|