switchman 3.0.14 → 3.0.17

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: 148d79c6d4c79ef51204c2d32718b30463814b56849effaaf9cacaf86789261d
4
- data.tar.gz: 1db4039008bd8958e0dc918b0de81a85294f795f6aea23a1e68e592d6ddc691f
3
+ metadata.gz: 213adc61b124f8ba48e52259c47702cb601fdc9e0ef71c452c1eeee23dc94e46
4
+ data.tar.gz: f7982e2e7cb3efce13291382b7fa49f9cb0da87cfd5939581368ac5735ed3e00
5
5
  SHA512:
6
- metadata.gz: c2ff7f714441d360a38125d7eea85ac3aba981d76fa8a8fceca527f499b7c1a90eb4dd2c91fcf6e292569089e35b077b7ad248cb069a3a9a0579071b93f54a0d
7
- data.tar.gz: 7fc113c8b0a1ecdd6d5590401af399fc8f7dd220bc655c190e962870f5e64e2d1e922879e3515693713a9cc77b290913d38740102a29000c19071dab21c10844
6
+ metadata.gz: e3651c9824e8258c8854e0281ecbc081b42f3fa97524d90eb41ac701f9a5eb0bef53ea77a700c682ffa5913ea71aa7816044cfa408b051f20f4b96b86a4e5307
7
+ data.tar.gz: 8fac15d77c092790b6410bf15aaccef4c90243308970eddd29e2c9145281cde67e4d6572439b97966ecc2c5924557c7916aca8665e722530c2ead0d43dbcdab2
@@ -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
-
218
- 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
184
+ Switchman.config[:on_fork_proc]&.call
185
+ with_each_shard(subscope, classes, exception: exception, &block).map { |result| Parallel::ResultWrapper.new(result) }
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.map(&:result)
293
202
  end
294
203
 
295
204
  classes ||= []
@@ -498,6 +407,7 @@ module Switchman
498
407
 
499
408
  klass.connects_to shards: connects_to_hash
500
409
  end
410
+ DatabaseServer.all.each { |db| db.guard! if db.config[:prefer_secondary] } unless @sharding_initialized
501
411
 
502
412
  @sharding_initialized = true
503
413
  end
@@ -28,19 +28,11 @@ module Switchman
28
28
  if self != ::ActiveRecord::Base && current_scope
29
29
  current_scope.activate do
30
30
  db = Shard.current(connection_classes).database_server
31
- if ::GuardRail.environment == db.guard_rail_environment
32
- super
33
- else
34
- db.unguard { super }
35
- end
31
+ db.unguard { super }
36
32
  end
37
33
  else
38
34
  db = Shard.current(connection_classes).database_server
39
- if ::GuardRail.environment == db.guard_rail_environment
40
- super
41
- else
42
- db.unguard { super }
43
- end
35
+ db.unguard { super }
44
36
  end
45
37
  end
46
38
 
@@ -68,6 +60,28 @@ module Switchman
68
60
  end
69
61
  end
70
62
 
63
+ def current_role_overriden?
64
+ current_role != current_role(without_overrides: true)
65
+ end
66
+
67
+ # significant change: Allow per-shard roles
68
+ def current_role(without_overrides: false)
69
+ return super() if without_overrides
70
+
71
+ sharded_role = nil
72
+ connected_to_stack.reverse_each do |hash|
73
+ shard_role = hash.dig(:shard_roles, current_shard)
74
+ if shard_role && (hash[:klasses].include?(Base) || hash[:klasses].include?(connection_classes))
75
+ sharded_role = shard_role
76
+ break
77
+ end
78
+ end
79
+ # Allow a shard-specific role to be reverted to regular inheritance
80
+ return sharded_role if sharded_role && sharded_role != :_switchman_inherit
81
+
82
+ super()
83
+ end
84
+
71
85
  # significant change: _don't_ check if klasses.include?(Base)
72
86
  # i.e. other sharded models don't inherit the current shard of Base
73
87
  def current_shard
@@ -146,7 +160,8 @@ module Switchman
146
160
 
147
161
  def with_transaction_returning_status
148
162
  shard.activate(self.class.connection_classes) do
149
- super
163
+ db = Shard.current(self.class.connection_classes).database_server
164
+ db.unguard { super }
150
165
  end
151
166
  end
152
167
 
@@ -167,9 +182,7 @@ module Switchman
167
182
 
168
183
  def update_columns(*)
169
184
  db = shard.database_server
170
- return db.unguard { super } if ::GuardRail.environment != db.guard_rail_environment
171
-
172
- super
185
+ db.unguard { super }
173
186
  end
174
187
 
175
188
  def id_for_database
@@ -14,9 +14,7 @@ module Switchman
14
14
 
15
15
  def delete
16
16
  db = shard.database_server
17
- return db.unguard { super } unless ::GuardRail.environment == db.guard_rail_environment
18
-
19
- super
17
+ db.unguard { super }
20
18
  end
21
19
  end
22
20
  end
@@ -129,27 +129,26 @@ module Switchman
129
129
  end
130
130
  end
131
131
 
132
- def guard_rail_environment
133
- @guard_rail_environment || ::GuardRail.environment
134
- end
135
-
136
132
  # locks this db to a specific environment, except for
137
133
  # when doing writes (then it falls back to the current
138
134
  # value of GuardRail.environment)
139
135
  def guard!(environment = :secondary)
140
- @guard_rail_environment = environment
136
+ ::ActiveRecord::Base.connected_to_stack << { shard_roles: { id.to_sym => environment }, klasses: [::ActiveRecord::Base] }
141
137
  end
142
138
 
143
139
  def unguard!
144
- @guard_rail_environment = nil
140
+ ::ActiveRecord::Base.connected_to_stack << { shard_roles: { id.to_sym => :_switchman_inherit }, klasses: [::ActiveRecord::Base] }
145
141
  end
146
142
 
147
143
  def unguard
148
- old_env = @guard_rail_environment
149
- unguard!
150
- yield
151
- ensure
152
- guard!(old_env)
144
+ return yield unless ::ActiveRecord::Base.current_role_overriden?
145
+
146
+ begin
147
+ unguard!
148
+ yield
149
+ ensure
150
+ ::ActiveRecord::Base.connected_to_stack.pop
151
+ end
153
152
  end
154
153
 
155
154
  def shards
@@ -6,20 +6,17 @@ module Switchman
6
6
  def exec_queries(*args)
7
7
  if lock_value
8
8
  db = Shard.current(connection_classes).database_server
9
- return db.unguard { super } if ::GuardRail.environment != db.guard_rail_environment
9
+ db.unguard { super }
10
+ else
11
+ super
10
12
  end
11
- super
12
13
  end
13
14
 
14
15
  %w[update_all delete_all].each do |method|
15
16
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
16
17
  def #{method}(*args)
17
18
  db = Shard.current(connection_classes).database_server
18
- if ::GuardRail.environment != db.guard_rail_environment
19
- db.unguard { super }
20
- else
21
- super
22
- end
19
+ db.unguard { super }
23
20
  end
24
21
  RUBY
25
22
  end
@@ -3,6 +3,11 @@
3
3
  module Switchman
4
4
  module GuardRail
5
5
  module ClassMethods
6
+ def environment
7
+ # no overrides so we get the global role, not the role for the default shard
8
+ ::ActiveRecord::Base.current_role(without_overrides: true)
9
+ end
10
+
6
11
  def activate(role)
7
12
  DatabaseServer.send(:reference_role, role)
8
13
  super
@@ -0,0 +1,68 @@
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 UndumpableResult
29
+ attr_reader :name
30
+
31
+ def initialize(result)
32
+ @name = result.inspect
33
+ end
34
+
35
+ def inspect
36
+ "#<UndumpableResult:#{name}>"
37
+ end
38
+ end
39
+
40
+ class ResultWrapper
41
+ attr_reader :result
42
+
43
+ def initialize(result)
44
+ @result =
45
+ begin
46
+ Marshal.dump(result) && result
47
+ rescue
48
+ UndumpableResult.new(result)
49
+ end
50
+ end
51
+ end
52
+
53
+ class PrefixingIO
54
+ delegate_missing_to :@original_io
55
+
56
+ def initialize(prefix, original_io)
57
+ @prefix = prefix
58
+ @original_io = original_io
59
+ end
60
+
61
+ def puts(*args)
62
+ args.flatten.each { |arg| @original_io.puts "#{@prefix}: #{arg}" }
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ ::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'
@@ -16,7 +16,7 @@ module Switchman
16
16
  payload[:shard] = {
17
17
  database_server_id: shard.database_server.id,
18
18
  id: shard.id,
19
- env: shard.database_server.guard_rail_environment
19
+ env: @shard_host.pool.connection_klass&.current_role
20
20
  }
21
21
  end
22
22
  super name, payload
@@ -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.14'
4
+ VERSION = '3.0.17'
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
@@ -86,10 +86,8 @@ module Switchman
86
86
  nil
87
87
  end
88
88
  rescue => e
89
- 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
90
90
  raise
91
-
92
- # ::ActiveRecord::Base.configurations = old_configurations
93
91
  end
94
92
  end
95
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.14
4
+ version: 3.0.17
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-23 00:00:00.000000000 Z
13
+ date: 2022-04-05 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