switchman 3.0.14 → 3.0.17

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