switchman 3.0.5 → 3.0.6

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: c3f9894955745b77bd418c8cede4882f5f37bf8cd3c0e2a3eecd53baa8658e1a
4
- data.tar.gz: d160939ff0c15d4c57b0836abe0850e9c8a499fc05eb410932dc038d2835c60f
3
+ metadata.gz: 81aeec21e056026642c6aa15bda349e97c8194212f8b79222b4581d5cb2a9909
4
+ data.tar.gz: 77bfd14eaf245a399ec6a5b48a9f3106daac6df393d8c55d7dad5eae56966f15
5
5
  SHA512:
6
- metadata.gz: 89dc3d801b5f97d2147f10bcfb691279395590b45e9631103c73f66e487345e8608e185c07f5e54429bf43d29bd3f35c017bd93cd9354a88e29d0456d1d5040f
7
- data.tar.gz: 9893296f2801ded4b1e227a4672b0e474735d2c6ba4ff7f5e95d0591b753659a97066f422ca0615d8abbd4c1e5f9bd4c8250ff9c3c697fcb18640b3ff47dd219
6
+ metadata.gz: bfc71a265b6aea2a48350bcce77dcdcb24cea206c2dbcd4b4e3e113f0615917b314233da2038197d394d2ae961dbc8be18f12d48ccc240d57ca96eb39d4db9e6
7
+ data.tar.gz: d392e78986ef050800ad61e064c702d3cb3dc98b97e48cbf42c9a973ae52226672a33ade5944119be7365f9d7f2f17e05097e0e1f2a46b1adab27dfe5169bf87
@@ -39,12 +39,17 @@ module Switchman
39
39
  # the first time we need a dummy dummy for re-entrancy to avoid looping on ourselves
40
40
  @default ||= default
41
41
 
42
- # Now find the actual record, if it exists; rescue the fake default if the table doesn't exist
42
+ # Now find the actual record, if it exists
43
43
  @default = begin
44
44
  find_cached('default_shard') { Shard.where(default: true).take } || default
45
+ # If we are *super* early in boot, the connection pool won't exist; we don't want to fill in the default shard yet
46
+ rescue ::ActiveRecord::ConnectionNotEstablished
47
+ nil
48
+ # rescue the fake default if the table doesn't exist
45
49
  rescue
46
50
  default
47
51
  end
52
+ return default unless @default
48
53
 
49
54
  # make sure this is not erroneously cached
50
55
  @default.database_server.remove_instance_variable(:@primary_shard) if @default.database_server.instance_variable_defined?(:@primary_shard)
@@ -59,34 +64,38 @@ module Switchman
59
64
 
60
65
  def current(klass = ::ActiveRecord::Base)
61
66
  klass ||= ::ActiveRecord::Base
62
- klass.connection_pool.shard
67
+ klass.current_switchman_shard
63
68
  end
64
69
 
65
70
  def activate(shards)
66
71
  activated_classes = activate!(shards)
67
72
  yield
68
73
  ensure
69
- activated_classes.each do |klass|
70
- klass.connection_pool.shard_stack.pop
74
+ activated_classes&.each do |klass|
71
75
  klass.connected_to_stack.pop
72
76
  end
73
77
  end
74
78
 
75
79
  def activate!(shards)
76
- activated_classes = []
80
+ activated_classes = nil
77
81
  shards.each do |klass, shard|
78
82
  next if klass == UnshardedRecord
79
83
 
80
84
  next unless klass.current_shard != shard.database_server.id.to_sym ||
81
- klass.connection_pool.shard != shard
85
+ klass.current_switchman_shard != shard
82
86
 
83
- activated_classes << klass
84
- klass.connected_to_stack << { shard: shard.database_server.id.to_sym, klasses: [klass] }
85
- klass.connection_pool.shard_stack << shard
87
+ (activated_classes ||= []) << klass
88
+ klass.connected_to_stack << { shard: shard.database_server.id.to_sym, klasses: [klass], switchman_shard: shard }
86
89
  end
87
90
  activated_classes
88
91
  end
89
92
 
93
+ def active_shards
94
+ sharded_models.map do |klass|
95
+ [klass, current(klass)]
96
+ end.compact.to_h
97
+ end
98
+
90
99
  def lookup(id)
91
100
  id_i = id.to_i
92
101
  return current if id_i == current.id || id == 'self'
@@ -103,6 +112,11 @@ module Switchman
103
112
  cached_shards[id]
104
113
  end
105
114
 
115
+ def preload_cache
116
+ cached_shards.reverse_merge!(active_shards.values.index_by(&:id))
117
+ cached_shards.reverse_merge!(all.index_by(&:id))
118
+ end
119
+
106
120
  def clear_cache
107
121
  cached_shards.clear
108
122
  end
@@ -111,14 +125,13 @@ module Switchman
111
125
  #
112
126
  # * +shards+ - an array or relation of Shards to iterate over
113
127
  # * +classes+ - an array of classes to activate
114
- # parallel: - true/false to execute in parallel, or a integer of how many
115
- # sub-processes per database server. Note that parallel
116
- # invocation currently uses forking, so should be used sparingly
117
- # because errors are not raised, and you cannot get results back
118
- # max_procs: - only run this many parallel processes at a time
128
+ # parallel: - true/false to execute in parallel, or an integer of how many
129
+ # sub-processes. Note that parallel invocation currently uses
130
+ # forking, so should be used sparingly because you cannot get
131
+ # results back
119
132
  # exception: - :ignore, :raise, :defer (wait until the end and raise the first
120
133
  # error), or a proc
121
- def with_each_shard(*args, parallel: false, max_procs: nil, exception: :raise, &block)
134
+ def with_each_shard(*args, parallel: false, exception: :raise, &block)
122
135
  raise ArgumentError, "wrong number of arguments (#{args.length} for 0...2)" if args.length > 2
123
136
 
124
137
  return Array.wrap(yield) unless default.is_a?(Shard)
@@ -133,14 +146,13 @@ module Switchman
133
146
  scope, classes = args
134
147
  end
135
148
 
136
- parallel = 1 if parallel == true
149
+ parallel = [Environment.cpu_count || 2, 2].min if parallel == true
137
150
  parallel = 0 if parallel == false || parallel.nil?
138
151
 
139
152
  scope ||= Shard.all
140
153
  scope = scope.order(::Arel.sql('database_server_id IS NOT NULL, database_server_id, id')) if ::ActiveRecord::Relation === scope && scope.order_values.empty?
141
154
 
142
- if parallel.positive?
143
- max_procs = determine_max_procs(max_procs, parallel)
155
+ if parallel > 1
144
156
  if ::ActiveRecord::Relation === scope
145
157
  # still need a post-uniq, cause the default database server could be NULL or Rails.env in the db
146
158
  database_servers = scope.reorder('database_server_id').select(:database_server_id).distinct.
@@ -148,39 +160,11 @@ module Switchman
148
160
  # nothing to do
149
161
  return if database_servers.count.zero?
150
162
 
151
- parallel = [(max_procs.to_f / database_servers.count).ceil, parallel].min if max_procs
152
-
153
163
  scopes = database_servers.map do |server|
154
- server_scope = server.shards.merge(scope)
155
- if parallel == 1
156
- subscopes = [server_scope]
157
- else
158
- subscopes = []
159
- total = server_scope.count
160
- ranges = []
161
- server_scope.find_ids_in_ranges(batch_size: (total.to_f / parallel).ceil) do |min, max|
162
- ranges << [min, max]
163
- end
164
- # create a half-open range on the last one
165
- ranges.last[1] = nil
166
- ranges.each do |min, max|
167
- subscope = server_scope.where('id>=?', min)
168
- subscope = subscope.where('id<=?', max) if max
169
- subscopes << subscope
170
- end
171
- end
172
- [server, subscopes]
164
+ [server, server.shards.merge(scope)]
173
165
  end.to_h
174
166
  else
175
167
  scopes = scope.group_by(&:database_server)
176
- if parallel > 1
177
- parallel = [(max_procs.to_f / scopes.count).ceil, parallel].min if max_procs
178
- scopes = scopes.map do |(server, shards)|
179
- [server, shards.in_groups(parallel, false).compact]
180
- end.to_h
181
- else
182
- scopes = scopes.map { |(server, shards)| [server, [shards]] }.to_h
183
- end
184
168
  end
185
169
 
186
170
  exception_pipes = []
@@ -206,80 +190,83 @@ module Switchman
206
190
  end
207
191
 
208
192
  # only one process; don't bother forking
209
- if scopes.length == 1 && parallel == 1
210
- return with_each_shard(scopes.first.last.first, classes, exception: exception,
211
- &block)
212
- end
193
+ return with_each_shard(scopes.first.last, classes, exception: exception, &block) if scopes.length == 1
213
194
 
214
195
  # clear connections prior to forking (no more queries will be executed in the parent,
215
196
  # and we want them gone so that we don't accidentally use them post-fork doing something
216
197
  # silly like dealloc'ing prepared statements)
217
198
  ::ActiveRecord::Base.clear_all_connections!
218
199
 
219
- scopes.each do |server, subscopes|
220
- subscopes.each_with_index do |subscope, idx|
221
- name = if subscopes.length > 1
222
- "#{server.id} #{idx + 1}"
223
- else
224
- server.id
225
- end
226
-
227
- exception_pipe = IO.pipe
228
- exception_pipes << exception_pipe
229
- pid, io_in, io_out, io_err = Open4.pfork4(lambda do
230
- Switchman.config[:on_fork_proc]&.call
231
-
232
- # set a pretty name for the process title, up to 128 characters
233
- # (we don't actually know the limit, depending on how the process
234
- # was started)
235
- # first, simplify the binary name by stripping directories,
236
- # then truncate arguments as necessary
237
- bin = File.basename($0) # Process.argv0 doesn't work on Ruby 2.5 (https://bugs.ruby-lang.org/issues/15887)
238
- max_length = 128 - bin.length - name.length - 3
239
- args = ARGV.join(' ')
240
- args = args[0..max_length] if max_length >= 0
241
- new_title = [bin, args, name].join(' ')
242
- Process.setproctitle(new_title)
243
-
244
- with_each_shard(subscope, classes, exception: exception, &block)
245
- exception_pipe.last.close
246
- rescue => e
247
- begin
248
- dumped = Marshal.dump(e)
249
- rescue
250
- # couldn't dump the exception; create a copy with just
251
- # the message and the backtrace
252
- e2 = e.class.new(e.message)
253
- e2.set_backtrace(e.backtrace)
254
- e2.instance_variable_set(:@active_shards, e.instance_variable_get(:@active_shards))
255
- dumped = Marshal.dump(e2)
256
- end
257
- exception_pipe.last.set_encoding(dumped.encoding)
258
- exception_pipe.last.write(dumped)
259
- exception_pipe.last.flush
260
- exception_pipe.last.close
261
- exit! 1
262
- end)
200
+ scopes.each do |server, subscope|
201
+ name = server.id
202
+
203
+ exception_pipe = IO.pipe
204
+ exception_pipes << exception_pipe
205
+ pid, io_in, io_out, io_err = Open4.pfork4(lambda do
206
+ Switchman.config[:on_fork_proc]&.call
207
+
208
+ # set a pretty name for the process title, up to 128 characters
209
+ # (we don't actually know the limit, depending on how the process
210
+ # was started)
211
+ # first, simplify the binary name by stripping directories,
212
+ # then truncate arguments as necessary
213
+ bin = File.basename($0) # Process.argv0 doesn't work on Ruby 2.5 (https://bugs.ruby-lang.org/issues/15887)
214
+ max_length = 128 - bin.length - name.length - 3
215
+ args = ARGV.join(' ')
216
+ args = args[0..max_length] if max_length >= 0
217
+ new_title = [bin, args, name].join(' ')
218
+ Process.setproctitle(new_title)
219
+
220
+ with_each_shard(subscope, classes, exception: exception, &block)
263
221
  exception_pipe.last.close
264
- pids << pid
265
- io_in.close # don't care about writing to stdin
266
- out_fds << io_out
267
- err_fds << io_err
268
- pid_to_name_map[pid] = name
269
- fd_to_name_map[io_out] = name
270
- fd_to_name_map[io_err] = name
271
-
272
- while max_procs && pids.count >= max_procs
273
- while max_procs && out_fds.count >= max_procs
274
- # wait for output if we've hit the max_procs limit
275
- wait_for_output.call
276
- end
277
- # we've gotten all the output from one fd so wait for its child process to exit
278
- found_pid, status = Process.wait2
279
- pids.delete(found_pid)
280
- errors << pid_to_name_map[found_pid] if status.exitstatus != 0
222
+ rescue Exception => e # rubocop:disable Lint/RescueException
223
+ begin
224
+ dumped = Marshal.dump(e)
225
+ dumped = nil if dumped.length > 64 * 1024
226
+ rescue
227
+ dumped = nil
228
+ end
229
+
230
+ if dumped.nil?
231
+ # couldn't dump the exception; create a copy with just
232
+ # the message and the backtrace
233
+ e2 = e.class.new(e.message)
234
+ backtrace = e.backtrace
235
+ # truncate excessively long backtraces
236
+ backtrace = backtrace[0...25] + ['...'] + backtrace[-25..] if backtrace.length > 50
237
+ e2.set_backtrace(backtrace)
238
+ e2.instance_variable_set(:@active_shards, e.instance_variable_get(:@active_shards))
239
+ dumped = Marshal.dump(e2)
281
240
  end
241
+ exception_pipe.last.set_encoding(dumped.encoding)
242
+ exception_pipe.last.write(dumped)
243
+ exception_pipe.last.flush
244
+ exception_pipe.last.close
245
+ exit! 1
246
+ end)
247
+ exception_pipe.last.close
248
+ pids << pid
249
+ io_in.close # don't care about writing to stdin
250
+ out_fds << io_out
251
+ err_fds << io_err
252
+ pid_to_name_map[pid] = name
253
+ fd_to_name_map[io_out] = name
254
+ fd_to_name_map[io_err] = name
255
+
256
+ while pids.count >= parallel
257
+ while out_fds.count >= parallel
258
+ # wait for output if we've hit the parallel limit
259
+ wait_for_output.call
260
+ end
261
+ # we've gotten all the output from one fd so wait for its child process to exit
262
+ found_pid, status = Process.wait2
263
+ pids.delete(found_pid)
264
+ errors << pid_to_name_map[found_pid] if status.exitstatus != 0
282
265
  end
266
+ # we've gotten all the output from one fd so wait for its child process to exit
267
+ found_pid, status = Process.wait2
268
+ pids.delete(found_pid)
269
+ errors << pid_to_name_map[found_pid] if status.exitstatus != 0
283
270
  end
284
271
 
285
272
  wait_for_output.call while out_fds.any? || err_fds.any?
@@ -402,7 +389,7 @@ module Switchman
402
389
  signed_id_operation(local_id) do |id|
403
390
  return nil if id > IDS_PER_SHARD
404
391
 
405
- $1.to_i * IDS_PER_SHARD + id
392
+ ($1.to_i * IDS_PER_SHARD) + id
406
393
  end
407
394
  when Integer, /^-?\d+$/
408
395
  any_id.to_i
@@ -480,23 +467,6 @@ module Switchman
480
467
  shard || source_shard || Shard.current
481
468
  end
482
469
 
483
- # given the provided option, determines whether we need to (and whether
484
- # it's possible) to determine a reasonable default.
485
- def determine_max_procs(max_procs_input, parallel_input = 2)
486
- max_procs = nil
487
- if max_procs_input
488
- max_procs = max_procs_input.to_i
489
- max_procs = nil if max_procs.zero?
490
- else
491
- return 1 if parallel_input.nil? || parallel_input < 1
492
-
493
- cpus = Environment.cpu_count
494
- max_procs = cpus * parallel_input if cpus&.positive?
495
- end
496
-
497
- max_procs
498
- end
499
-
500
470
  private
501
471
 
502
472
  def add_sharded_model(klass)
@@ -596,6 +566,10 @@ module Switchman
596
566
  Shard.default
597
567
  end
598
568
 
569
+ def original_id
570
+ id
571
+ end
572
+
599
573
  def activate(*classes, &block)
600
574
  shards = hashify_classes(classes)
601
575
  Shard.activate(shards, &block)
@@ -668,7 +642,7 @@ module Switchman
668
642
  return nil unless local_id
669
643
 
670
644
  self.class.signed_id_operation(local_id) do |abs_id|
671
- abs_id + id * IDS_PER_SHARD
645
+ abs_id + (id * IDS_PER_SHARD)
672
646
  end
673
647
  end
674
648
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  class AddDefaultShardIndex < ActiveRecord::Migration[4.2]
4
4
  def change
5
- Switchman::Shard.where(default: nil).update_all(default: false)
5
+ Switchman::Shard.where(default: nil).update_all(default: false) if Switchman::Shard.current.default?
6
6
  change_column_default :switchman_shards, :default, false
7
7
  change_column_null :switchman_shards, :default, false
8
8
  options = if connection.adapter_name == 'PostgreSQL'
@@ -4,7 +4,7 @@ class AddTimestampsToShards < ActiveRecord::Migration[4.2]
4
4
  disable_ddl_transaction!
5
5
 
6
6
  def change
7
- add_timestamps :switchman_shards, null: true
7
+ add_timestamps :switchman_shards, null: true, if_not_exists: true
8
8
  now = Time.now.utc
9
9
  Switchman::Shard.update_all(updated_at: now, created_at: now) if Switchman::Shard.current.default?
10
10
  change_column_null :switchman_shards, :updated_at, false
@@ -40,15 +40,6 @@ module Switchman
40
40
  ensure
41
41
  @last_query_at = Time.now
42
42
  end
43
-
44
- private
45
-
46
- def id_value_for_database(value)
47
- return super unless value.class.sharded_primary_key?
48
-
49
- # do this the Rails 4.2 way, so that if Shard.current != self.shard, the id gets transposed
50
- quote(value.id)
51
- end
52
43
  end
53
44
  end
54
45
  end
@@ -40,7 +40,11 @@ module Switchman
40
40
  if sharded_column?(attr_name)
41
41
  owner << <<-RUBY
42
42
  def global_#{attr_name}
43
- ::Switchman::Shard.global_id_for(original_#{attr_name}, shard)
43
+ raw_value = original_#{attr_name}
44
+ return nil if raw_value.nil?
45
+ return raw_value if raw_value > ::Switchman::Shard::IDS_PER_SHARD
46
+
47
+ ::Switchman::Shard.global_id_for(raw_value, shard)
44
48
  end
45
49
  RUBY
46
50
  else
@@ -52,7 +56,9 @@ module Switchman
52
56
  if sharded_column?(attr_name)
53
57
  owner << <<-RUBY
54
58
  def local_#{attr_name}
55
- ::Switchman::Shard.local_id_for(original_#{attr_name}).first
59
+ raw_value = original_#{attr_name}
60
+ return nil if raw_value.nil?
61
+ return raw_value % ::Switchman::Shard::IDS_PER_SHARD
56
62
  end
57
63
  RUBY
58
64
  else
@@ -104,7 +110,22 @@ module Switchman
104
110
  alias_method 'original_#{attr_name}', '#{attr_name}'
105
111
  # and replace with one that transposes the id
106
112
  def #{attr_name}
107
- ::Switchman::Shard.relative_id_for(original_#{attr_name}, shard, ::Switchman::Shard.current(#{connection_classes_code_for_reflection(reflection)}))
113
+ raw_value = original_#{attr_name}
114
+ return nil if raw_value.nil?
115
+
116
+ abs_raw_value = raw_value.abs
117
+ current_shard = ::Switchman::Shard.current(#{connection_classes_code_for_reflection(reflection)})
118
+ same_shard = shard == current_shard
119
+ return raw_value if same_shard && abs_raw_value < ::Switchman::Shard::IDS_PER_SHARD
120
+
121
+ value_shard_id = abs_raw_value / ::Switchman::Shard::IDS_PER_SHARD
122
+ # this is a stupid case when someone stuffed a global id for the current shard in instead
123
+ # of a local id
124
+ return raw_value % ::Switchman::Shard::IDS_PER_SHARD if value_shard_id == current_shard.id
125
+ return raw_value if !same_shard && abs_raw_value > ::Switchman::Shard::IDS_PER_SHARD
126
+ return shard.global_id_for(raw_value) if !same_shard && abs_raw_value < ::Switchman::Shard::IDS_PER_SHARD
127
+
128
+ ::Switchman::Shard.relative_id_for(raw_value, shard, current_shard)
108
129
  end
109
130
 
110
131
  alias_method 'original_#{attr_name}=', '#{attr_name}='
@@ -77,17 +77,27 @@ module Switchman
77
77
 
78
78
  default_shard
79
79
  end
80
+
81
+ def current_switchman_shard
82
+ connected_to_stack.reverse_each do |hash|
83
+ return hash[:switchman_shard] if hash[:switchman_shard] && hash[:klasses].include?(connection_classes)
84
+ end
85
+
86
+ Shard.default
87
+ end
80
88
  end
81
89
 
82
- def self.included(klass)
90
+ def self.prepended(klass)
83
91
  klass.singleton_class.prepend(ClassMethods)
84
- klass.set_callback(:initialize, :before) do
85
- @shard ||= if self.class.sharded_primary_key?
86
- Shard.shard_for(self[self.class.primary_key], Shard.current(self.class.connection_classes))
87
- else
88
- Shard.current(self.class.connection_classes)
89
- end
90
- end
92
+ end
93
+
94
+ def _run_initialize_callbacks
95
+ @shard ||= if self.class.sharded_primary_key?
96
+ Shard.shard_for(self[self.class.primary_key], Shard.current(self.class.connection_classes))
97
+ else
98
+ Shard.current(self.class.connection_classes)
99
+ end
100
+ super
91
101
  end
92
102
 
93
103
  def shard
@@ -107,12 +117,12 @@ module Switchman
107
117
 
108
118
  def save(*, **)
109
119
  @shard_set_in_stone = true
110
- (self.class.current_scope || self.class.default_scoped).shard(shard, :implicit).scoping { super }
120
+ super
111
121
  end
112
122
 
113
123
  def save!(*, **)
114
124
  @shard_set_in_stone = true
115
- (self.class.current_scope || self.class.default_scoped).shard(shard, :implicit).scoping { super }
125
+ super
116
126
  end
117
127
 
118
128
  def destroy
@@ -134,6 +144,12 @@ module Switchman
134
144
  end
135
145
  end
136
146
 
147
+ def with_transaction_returning_status
148
+ shard.activate(self.class.connection_classes) do
149
+ super
150
+ end
151
+ end
152
+
137
153
  def hash
138
154
  self.class.sharded_primary_key? ? self.class.hash ^ global_id.hash : super
139
155
  end
@@ -149,19 +165,20 @@ module Switchman
149
165
  copy
150
166
  end
151
167
 
152
- def quoted_id
153
- return super unless self.class.sharded_primary_key?
168
+ def update_columns(*)
169
+ db = shard.database_server
170
+ return db.unguard { super } if ::GuardRail.environment != db.guard_rail_environment
154
171
 
155
- # do this the Rails 4.2 way, so that if Shard.current != self.shard, the id gets transposed
156
- self.class.connection.quote(id)
172
+ super
157
173
  end
158
174
 
159
- def update_columns(*)
160
- db = Shard.current(self.class.connection_classes).database_server
161
- if ::GuardRail.environment == db.guard_rail_environment
162
- super
175
+ def id_for_database
176
+ if self.class.sharded_primary_key?
177
+ # It's an int, so so it's safe to just return it without passing it through anything else
178
+ # In theory we should do `@attributes[@primary_key].type.serialize(id)`, but that seems to have surprising side-effects
179
+ id
163
180
  else
164
- db.unguard { super }
181
+ super
165
182
  end
166
183
  end
167
184
 
@@ -182,7 +199,7 @@ module Switchman
182
199
  reflection.klass.connection_classes
183
200
  end
184
201
  else
185
- connection_classes
202
+ self.class.connection_classes
186
203
  end
187
204
  end
188
205
  end
@@ -5,18 +5,6 @@ require 'switchman/errors'
5
5
  module Switchman
6
6
  module ActiveRecord
7
7
  module ConnectionPool
8
- def shard
9
- shard_stack.last || Shard.default
10
- end
11
-
12
- def shard_stack
13
- unless (shard_stack = Thread.current.thread_variable_get(tls_key))
14
- shard_stack = Concurrent::Array.new
15
- Thread.current.thread_variable_set(tls_key, shard_stack)
16
- end
17
- shard_stack
18
- end
19
-
20
8
  def default_schema
21
9
  connection unless @schemas
22
10
  # default shard will not switch databases immediately, so it won't be set yet
@@ -26,15 +14,15 @@ module Switchman
26
14
 
27
15
  def checkout_new_connection
28
16
  conn = super
29
- conn.shard = shard
17
+ conn.shard = current_shard
30
18
  conn
31
19
  end
32
20
 
33
21
  def connection(switch_shard: true)
34
22
  conn = super()
35
- raise NonExistentShardError if shard.new_record?
23
+ raise NonExistentShardError if current_shard.new_record?
36
24
 
37
- switch_database(conn) if conn.shard != shard && switch_shard
25
+ switch_database(conn) if conn.shard != current_shard && switch_shard
38
26
  conn
39
27
  end
40
28
 
@@ -44,26 +32,18 @@ module Switchman
44
32
  flush
45
33
  end
46
34
 
47
- def remove_shard!(shard)
48
- synchronize do
49
- # The shard might be currently active, so we need to update our own shard
50
- self.shard = Shard.default if self.shard == shard
51
- # Update out any connections that may be using this shard
52
- @connections.each do |conn|
53
- # This will also update the connection's shard to the default shard
54
- switch_database(conn) if conn.shard == shard
55
- end
56
- end
57
- end
58
-
59
35
  def switch_database(conn)
60
- @schemas = conn.current_schemas if !@schemas && conn.adapter_name == 'PostgreSQL' && !shard.database_server.config[:shard_name]
36
+ @schemas = conn.current_schemas if !@schemas && conn.adapter_name == 'PostgreSQL' && !current_shard.database_server.config[:shard_name]
61
37
 
62
- conn.shard = shard
38
+ conn.shard = current_shard
63
39
  end
64
40
 
65
41
  private
66
42
 
43
+ def current_shard
44
+ connection_klass.current_switchman_shard
45
+ end
46
+
67
47
  def tls_key
68
48
  "#{object_id}_shard".to_sym
69
49
  end
@@ -14,16 +14,19 @@ module Switchman
14
14
 
15
15
  def connection
16
16
  conn = super
17
- ::ActiveRecord::Base.connection_pool.switch_database(conn) if conn.shard != ::ActiveRecord::Base.connection_pool.shard
17
+ ::ActiveRecord::Base.connection_pool.switch_database(conn) if conn.shard != ::ActiveRecord::Base.current_switchman_shard
18
18
  conn
19
19
  end
20
20
  end
21
21
 
22
22
  module Migrator
23
- # significant change: hash shard name, not database name
23
+ # significant change: just return MIGRATOR_SALT directly
24
+ # especially if you're going through pgbouncer, the database
25
+ # name you're accessing may not be consistent. it is NOT allowed
26
+ # to run migrations against multiple shards in the same database
27
+ # concurrently
24
28
  def generate_migrator_advisory_lock_id
25
- shard_name_hash = Zlib.crc32(Shard.current.name)
26
- ::ActiveRecord::Migrator::MIGRATOR_SALT * shard_name_hash
29
+ ::ActiveRecord::Migrator::MIGRATOR_SALT
27
30
  end
28
31
 
29
32
  # significant change: strip out prefer_secondary from config
@@ -11,6 +11,13 @@ module Switchman
11
11
  def update_columns(*)
12
12
  shard.activate(self.class.connection_classes) { super }
13
13
  end
14
+
15
+ def delete
16
+ db = shard.database_server
17
+ return db.unguard { super } unless ::GuardRail.environment == db.guard_rail_environment
18
+
19
+ super
20
+ end
14
21
  end
15
22
  end
16
23
  end
@@ -32,7 +32,7 @@ module Switchman
32
32
  end
33
33
 
34
34
  # copy/paste; use quote_local_table_name
35
- def drop_database(name) #:nodoc:
35
+ def drop_database(name) # :nodoc:
36
36
  execute "DROP DATABASE IF EXISTS #{quote_local_table_name(name)}"
37
37
  end
38
38
 
@@ -94,7 +94,7 @@ module Switchman
94
94
 
95
95
  while ids.first.present?
96
96
  ids.map!(&:to_i) if is_integer
97
- ids << ids.first + batch_size if loose_mode
97
+ ids << (ids.first + batch_size) if loose_mode
98
98
 
99
99
  yield(*ids)
100
100
  last_value = ids.last
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Switchman
4
+ module ActiveRecord
5
+ module TestFixtures
6
+ FORBIDDEN_DB_ENVS = %i[development production].freeze
7
+ def setup_fixtures(config = ::ActiveRecord::Base)
8
+ super
9
+
10
+ return unless run_in_transaction?
11
+
12
+ # Replace the one that activerecord natively uses with a switchman-optimized one
13
+ ::ActiveSupport::Notifications.unsubscribe(@connection_subscriber)
14
+ # Code adapted from the code in rails proper
15
+ @connection_subscriber = ::ActiveSupport::Notifications.subscribe('!connection.active_record') do |_, _, _, _, payload|
16
+ spec_name = payload[:spec_name] if payload.key?(:spec_name)
17
+ shard = payload[:shard] if payload.key?(:shard)
18
+ setup_shared_connection_pool
19
+
20
+ if spec_name && !FORBIDDEN_DB_ENVS.include?(shard)
21
+ begin
22
+ connection = ::ActiveRecord::Base.connection_handler.retrieve_connection(spec_name, shard: shard)
23
+ rescue ::ActiveRecord::ConnectionNotEstablished, ::ActiveRecord::NoDatabaseError
24
+ connection = nil
25
+ end
26
+
27
+ if connection && !@fixture_connections.include?(connection)
28
+ connection.begin_transaction joinable: false, _lazy: false
29
+ connection.pool.lock_thread = true if lock_threads
30
+ @fixture_connections << connection
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ def enlist_fixture_connections
37
+ setup_shared_connection_pool
38
+
39
+ ::ActiveRecord::Base.connection_handler.connection_pool_list.reject { |cp| FORBIDDEN_DB_ENVS.include?(cp.db_config.env_name.to_sym) }.map(&:connection)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -59,7 +59,7 @@ module Switchman
59
59
  end
60
60
 
61
61
  def database_servers
62
- unless @database_servers
62
+ if !@database_servers || @database_servers.empty?
63
63
  @database_servers = {}.with_indifferent_access
64
64
  ::ActiveRecord::Base.configurations.configurations.each do |config|
65
65
  if config.name.include?('/')
@@ -186,58 +186,56 @@ module Switchman
186
186
 
187
187
  name ||= "#{config[:database]}_shard_#{id}"
188
188
 
189
+ schema_already_existed = false
190
+ shard = nil
189
191
  Shard.connection.transaction do
190
- shard = Shard.create!(id: id,
191
- name: name,
192
- database_server_id: self.id)
193
- schema_already_existed = false
194
-
195
- begin
196
- self.class.creating_new_shard = true
197
- DatabaseServer.send(:reference_role, :deploy)
198
- ::ActiveRecord::Base.connected_to(shard: self.id.to_sym, role: :deploy) do
199
- if create_statement
200
- if ::ActiveRecord::Base.connection.select_value("SELECT 1 FROM pg_namespace WHERE nspname=#{::ActiveRecord::Base.connection.quote(name)}")
201
- schema_already_existed = true
202
- raise 'This schema already exists; cannot overwrite'
203
- end
204
- Array(create_statement.call).each do |stmt|
205
- ::ActiveRecord::Base.connection.execute(stmt)
206
- end
192
+ self.class.creating_new_shard = true
193
+ DatabaseServer.send(:reference_role, :deploy)
194
+ ::ActiveRecord::Base.connected_to(shard: self.id.to_sym, role: :deploy) do
195
+ shard = Shard.create!(id: id,
196
+ name: name,
197
+ database_server_id: self.id)
198
+ if create_statement
199
+ if ::ActiveRecord::Base.connection.select_value("SELECT 1 FROM pg_namespace WHERE nspname=#{::ActiveRecord::Base.connection.quote(name)}")
200
+ schema_already_existed = true
201
+ raise 'This schema already exists; cannot overwrite'
207
202
  end
208
- if config[:adapter] == 'postgresql'
209
- old_proc = ::ActiveRecord::Base.connection.raw_connection.set_notice_processor do
210
- end
203
+ Array(create_statement.call).each do |stmt|
204
+ ::ActiveRecord::Base.connection.execute(stmt)
205
+ end
206
+ end
207
+ if config[:adapter] == 'postgresql'
208
+ old_proc = ::ActiveRecord::Base.connection.raw_connection.set_notice_processor do
211
209
  end
212
- old_verbose = ::ActiveRecord::Migration.verbose
213
- ::ActiveRecord::Migration.verbose = false
214
-
215
- unless schema == false
216
- shard.activate(*Shard.sharded_models) do
217
- reset_column_information
218
-
219
- ::ActiveRecord::Base.connection.transaction(requires_new: true) do
220
- ::ActiveRecord::Base.connection.migration_context.migrate
221
- end
222
- reset_column_information
223
- ::ActiveRecord::Base.descendants.reject do |m|
224
- m <= UnshardedRecord || !m.table_exists?
225
- end.each(&:define_attribute_methods)
210
+ end
211
+ old_verbose = ::ActiveRecord::Migration.verbose
212
+ ::ActiveRecord::Migration.verbose = false
213
+
214
+ unless schema == false
215
+ shard.activate do
216
+ reset_column_information
217
+
218
+ ::ActiveRecord::Base.connection.transaction(requires_new: true) do
219
+ ::ActiveRecord::Base.connection.migration_context.migrate
226
220
  end
221
+ reset_column_information
222
+ ::ActiveRecord::Base.descendants.reject do |m|
223
+ m <= UnshardedRecord || !m.table_exists?
224
+ end.each(&:define_attribute_methods)
227
225
  end
228
- ensure
229
- ::ActiveRecord::Migration.verbose = old_verbose
230
- ::ActiveRecord::Base.connection.raw_connection.set_notice_processor(&old_proc) if old_proc
231
226
  end
232
- shard
233
- rescue
234
- shard.destroy
235
- shard.drop_database rescue nil unless schema_already_existed
236
- reset_column_information unless schema == false rescue nil
237
- raise
238
227
  ensure
239
- self.class.creating_new_shard = false
228
+ ::ActiveRecord::Migration.verbose = old_verbose
229
+ ::ActiveRecord::Base.connection.raw_connection.set_notice_processor(&old_proc) if old_proc
240
230
  end
231
+ shard
232
+ rescue
233
+ shard&.destroy
234
+ shard&.drop_database rescue nil unless schema_already_existed
235
+ reset_column_information unless schema == false rescue nil
236
+ raise
237
+ ensure
238
+ self.class.creating_new_shard = false
241
239
  end
242
240
  end
243
241
 
@@ -90,6 +90,7 @@ module Switchman
90
90
  require 'switchman/active_record/statement_cache'
91
91
  require 'switchman/active_record/tasks/database_tasks'
92
92
  require 'switchman/active_record/type_caster'
93
+ require 'switchman/active_record/test_fixtures'
93
94
  require 'switchman/arel'
94
95
  require 'switchman/call_super'
95
96
  require 'switchman/rails'
@@ -101,7 +102,7 @@ module Switchman
101
102
  self.default_shard = ::Rails.env.to_sym
102
103
  self.default_role = :primary
103
104
 
104
- include ActiveRecord::Base
105
+ prepend ActiveRecord::Base
105
106
  include ActiveRecord::AttributeMethods
106
107
  include ActiveRecord::Persistence
107
108
  singleton_class.prepend ActiveRecord::ModelSchema::ClassMethods
@@ -154,6 +155,8 @@ module Switchman
154
155
 
155
156
  ::ActiveRecord::Tasks::DatabaseTasks.singleton_class.prepend(ActiveRecord::Tasks::DatabaseTasks)
156
157
 
158
+ ::ActiveRecord::TestFixtures.prepend(ActiveRecord::TestFixtures)
159
+
157
160
  ::ActiveRecord::TypeCaster::Map.include(ActiveRecord::TypeCaster::Map)
158
161
  ::ActiveRecord::TypeCaster::Connection.include(ActiveRecord::TypeCaster::Connection)
159
162
 
@@ -107,32 +107,11 @@ module Switchman
107
107
  raise 'Sharding did not set up correctly' if @@sharding_failed
108
108
 
109
109
  Shard.clear_cache
110
- if use_transactional_tests
111
- Shard.default(reload: true)
112
- @shard1 = Shard.find(@shard1.id)
113
- @shard2 = Shard.find(@shard2.id)
114
- shards = [@shard2]
115
- shards << @shard1 unless @shard1.database_server == Shard.default.database_server
116
- shards.each do |shard|
117
- shard.activate do
118
- ::ActiveRecord::Base.connection.begin_transaction joinable: false
119
- end
120
- end
121
- end
122
110
  end
123
111
 
124
112
  klass.after do
125
113
  next if @@sharding_failed
126
114
 
127
- if use_transactional_tests
128
- shards = [@shard2]
129
- shards << @shard1 unless @shard1.database_server == Shard.default.database_server
130
- shards.each do |shard|
131
- shard.activate do
132
- ::ActiveRecord::Base.connection.rollback_transaction if ::ActiveRecord::Base.connection.transaction_open?
133
- end
134
- end
135
- end
136
115
  # clean up after specs
137
116
  DatabaseServer.all.each do |ds|
138
117
  if ds.fake? && ds != @shard2.database_server
@@ -143,7 +122,8 @@ module Switchman
143
122
  end
144
123
 
145
124
  klass.after(:all) do
146
- Shard.connection.update("TRUNCATE #{Shard.quoted_table_name} CASCADE")
125
+ # Don't truncate because that can create some fun cross-connection lock contention
126
+ Shard.delete_all
147
127
  Switchman.cache.delete('default_shard')
148
128
  Shard.default(reload: true)
149
129
  end
@@ -10,10 +10,10 @@ module Switchman
10
10
  return
11
11
  end
12
12
 
13
- if defined?(Shard)
14
- @active_shards = Shard.sharded_models.map do |klass|
15
- [klass, Shard.current(klass)]
16
- end.compact.to_h
13
+ begin
14
+ @active_shards = Shard.active_shards if defined?(Shard)
15
+ rescue ::ActiveRecord::ConnectionNotEstablished
16
+ # If we hit an error really early in boot, activerecord may not be initialized yet
17
17
  end
18
18
 
19
19
  super
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Switchman
4
- VERSION = '3.0.5'
4
+ VERSION = '3.0.6'
5
5
  end
@@ -46,7 +46,7 @@ module Switchman
46
46
  end
47
47
 
48
48
  def self.options
49
- { parallel: ENV['PARALLEL'].to_i, max_procs: ENV['MAX_PARALLEL_PROCS'] }
49
+ { parallel: ENV['PARALLEL'].to_i }
50
50
  end
51
51
 
52
52
  # classes - an array or proc, to activate as the current shard during the
@@ -80,14 +80,14 @@ module Switchman
80
80
  puts "Exception from #{e.current_shard.id}: #{e.current_shard.description}" if options[:parallel] != 0
81
81
  raise
82
82
 
83
- #::ActiveRecord::Base.configurations = old_configurations
83
+ # ::ActiveRecord::Base.configurations = old_configurations
84
84
  end
85
85
  end
86
86
  end
87
87
  end
88
88
 
89
89
  %w[db:migrate db:migrate:up db:migrate:down db:rollback].each do |task_name|
90
- shardify_task(task_name, classes: -> { Shard.sharded_models })
90
+ shardify_task(task_name)
91
91
  end
92
92
 
93
93
  def self.shard_scope(scope, raw_shard_ids)
@@ -201,14 +201,18 @@ module Switchman
201
201
  module PostgreSQLDatabaseTasks
202
202
  def structure_dump(filename, extra_flags = nil)
203
203
  set_psql_env
204
- args = ['-s', '-x', '-O', '-f', filename]
204
+ args = ['--schema-only', '--no-privileges', '--no-owner', '--file', filename]
205
205
  args.concat(Array(extra_flags)) if extra_flags
206
206
  shard = Shard.current.name
207
207
  serialized_search_path = shard
208
208
  args << "--schema=#{Shellwords.escape(shard)}"
209
209
 
210
- args << configuration['database']
210
+ ignore_tables = ::ActiveRecord::SchemaDumper.ignore_tables
211
+ args += ignore_tables.flat_map { |table| ['-T', table] } if ignore_tables.any?
212
+
213
+ args << db_config.database
211
214
  run_cmd('pg_dump', args, 'dumping')
215
+ remove_sql_header_comments(filename)
212
216
  File.open(filename, 'a') { |f| f << "SET search_path TO #{serialized_search_path};\n\n" }
213
217
  end
214
218
  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.5
4
+ version: 3.0.6
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: 2021-06-11 00:00:00.000000000 Z
13
+ date: 2022-02-11 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -18,7 +18,7 @@ dependencies:
18
18
  requirements:
19
19
  - - ">="
20
20
  - !ruby/object:Gem::Version
21
- version: '6.1'
21
+ version: 6.1.4
22
22
  - - "<"
23
23
  - !ruby/object:Gem::Version
24
24
  version: '6.2'
@@ -28,7 +28,7 @@ dependencies:
28
28
  requirements:
29
29
  - - ">="
30
30
  - !ruby/object:Gem::Version
31
- version: '6.1'
31
+ version: 6.1.4
32
32
  - - "<"
33
33
  - !ruby/object:Gem::Version
34
34
  version: '6.2'
@@ -86,14 +86,14 @@ dependencies:
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '2.1'
89
+ version: 2.3.0
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '2.1'
96
+ version: 2.3.0
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: byebug
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -275,6 +275,7 @@ files:
275
275
  - lib/switchman/active_record/statement_cache.rb
276
276
  - lib/switchman/active_record/table_definition.rb
277
277
  - lib/switchman/active_record/tasks/database_tasks.rb
278
+ - lib/switchman/active_record/test_fixtures.rb
278
279
  - lib/switchman/active_record/type_caster.rb
279
280
  - lib/switchman/active_support/cache.rb
280
281
  - lib/switchman/arel.rb
@@ -313,7 +314,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
313
314
  - !ruby/object:Gem::Version
314
315
  version: '0'
315
316
  requirements: []
316
- rubygems_version: 3.2.15
317
+ rubygems_version: 3.1.4
317
318
  signing_key:
318
319
  specification_version: 4
319
320
  summary: Rails sharding magic