switchman 3.0.2 → 3.1.0
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 +4 -4
- data/Rakefile +1 -1
- data/db/migrate/20180828183945_add_default_shard_index.rb +1 -1
- data/db/migrate/20180828192111_add_timestamps_to_shards.rb +1 -1
- data/lib/switchman/action_controller/caching.rb +2 -2
- data/lib/switchman/active_record/abstract_adapter.rb +2 -13
- data/lib/switchman/active_record/associations.rb +223 -0
- data/lib/switchman/active_record/attribute_methods.rb +144 -63
- data/lib/switchman/active_record/base.rb +100 -43
- data/lib/switchman/active_record/calculations.rb +12 -5
- data/lib/switchman/active_record/connection_pool.rb +9 -31
- data/lib/switchman/active_record/database_configurations.rb +18 -2
- data/lib/switchman/active_record/finder_methods.rb +2 -2
- data/lib/switchman/active_record/migration.rb +7 -4
- data/lib/switchman/active_record/model_schema.rb +1 -1
- data/lib/switchman/active_record/persistence.rb +7 -2
- data/lib/switchman/active_record/postgresql_adapter.rb +6 -2
- data/lib/switchman/active_record/predicate_builder.rb +1 -1
- data/lib/switchman/active_record/query_methods.rb +27 -14
- data/lib/switchman/active_record/reflection.rb +1 -1
- data/lib/switchman/active_record/relation.rb +25 -24
- data/lib/switchman/active_record/statement_cache.rb +2 -2
- data/lib/switchman/active_record/table_definition.rb +1 -1
- data/lib/switchman/active_record/test_fixtures.rb +43 -0
- data/lib/switchman/active_support/cache.rb +16 -0
- data/lib/switchman/arel.rb +28 -6
- data/lib/switchman/database_server.rb +71 -65
- data/lib/switchman/default_shard.rb +0 -2
- data/lib/switchman/engine.rb +67 -125
- data/lib/switchman/errors.rb +4 -2
- data/lib/switchman/guard_rail/relation.rb +6 -9
- data/lib/switchman/guard_rail.rb +5 -0
- data/lib/switchman/parallel.rb +68 -0
- data/lib/switchman/r_spec_helper.rb +5 -17
- data/lib/switchman/rails.rb +1 -4
- data/{app/models → lib}/switchman/shard.rb +61 -188
- data/lib/switchman/sharded_instrumenter.rb +1 -1
- data/lib/switchman/standard_error.rb +11 -12
- data/{app/models → lib}/switchman/unsharded_record.rb +1 -1
- data/lib/switchman/version.rb +1 -1
- data/lib/switchman.rb +22 -2
- data/lib/tasks/switchman.rake +24 -13
- metadata +24 -22
- data/lib/switchman/active_record/association.rb +0 -206
- data/lib/switchman/open4.rb +0 -80
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'switchman/database_server'
|
|
4
|
-
require 'switchman/default_shard'
|
|
5
|
-
require 'switchman/environment'
|
|
6
|
-
require 'switchman/errors'
|
|
7
|
-
|
|
8
3
|
module Switchman
|
|
9
4
|
class Shard < UnshardedRecord
|
|
10
5
|
# ten trillion possible ids per shard. yup.
|
|
@@ -39,7 +34,7 @@ module Switchman
|
|
|
39
34
|
# the first time we need a dummy dummy for re-entrancy to avoid looping on ourselves
|
|
40
35
|
@default ||= default
|
|
41
36
|
|
|
42
|
-
# Now find the actual record, if it exists
|
|
37
|
+
# Now find the actual record, if it exists
|
|
43
38
|
@default = begin
|
|
44
39
|
find_cached('default_shard') { Shard.where(default: true).take } || default
|
|
45
40
|
rescue
|
|
@@ -59,34 +54,38 @@ module Switchman
|
|
|
59
54
|
|
|
60
55
|
def current(klass = ::ActiveRecord::Base)
|
|
61
56
|
klass ||= ::ActiveRecord::Base
|
|
62
|
-
klass.
|
|
57
|
+
klass.current_switchman_shard
|
|
63
58
|
end
|
|
64
59
|
|
|
65
60
|
def activate(shards)
|
|
66
61
|
activated_classes = activate!(shards)
|
|
67
62
|
yield
|
|
68
63
|
ensure
|
|
69
|
-
activated_classes
|
|
70
|
-
klass.connection_pool.shard_stack.pop
|
|
64
|
+
activated_classes&.each do |klass|
|
|
71
65
|
klass.connected_to_stack.pop
|
|
72
66
|
end
|
|
73
67
|
end
|
|
74
68
|
|
|
75
69
|
def activate!(shards)
|
|
76
|
-
activated_classes =
|
|
70
|
+
activated_classes = nil
|
|
77
71
|
shards.each do |klass, shard|
|
|
78
72
|
next if klass == UnshardedRecord
|
|
79
73
|
|
|
80
74
|
next unless klass.current_shard != shard.database_server.id.to_sym ||
|
|
81
|
-
klass.
|
|
75
|
+
klass.current_switchman_shard != shard
|
|
82
76
|
|
|
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
|
|
77
|
+
(activated_classes ||= []) << klass
|
|
78
|
+
klass.connected_to_stack << { shard: shard.database_server.id.to_sym, klasses: [klass], switchman_shard: shard }
|
|
86
79
|
end
|
|
87
80
|
activated_classes
|
|
88
81
|
end
|
|
89
82
|
|
|
83
|
+
def active_shards
|
|
84
|
+
sharded_models.map do |klass|
|
|
85
|
+
[klass, current(klass)]
|
|
86
|
+
end.compact.to_h
|
|
87
|
+
end
|
|
88
|
+
|
|
90
89
|
def lookup(id)
|
|
91
90
|
id_i = id.to_i
|
|
92
91
|
return current if id_i == current.id || id == 'self'
|
|
@@ -103,6 +102,11 @@ module Switchman
|
|
|
103
102
|
cached_shards[id]
|
|
104
103
|
end
|
|
105
104
|
|
|
105
|
+
def preload_cache
|
|
106
|
+
cached_shards.reverse_merge!(active_shards.values.index_by(&:id))
|
|
107
|
+
cached_shards.reverse_merge!(all.index_by(&:id))
|
|
108
|
+
end
|
|
109
|
+
|
|
106
110
|
def clear_cache
|
|
107
111
|
cached_shards.clear
|
|
108
112
|
end
|
|
@@ -111,14 +115,12 @@ module Switchman
|
|
|
111
115
|
#
|
|
112
116
|
# * +shards+ - an array or relation of Shards to iterate over
|
|
113
117
|
# * +classes+ - an array of classes to activate
|
|
114
|
-
# parallel: - true/false to execute in parallel, or
|
|
115
|
-
# sub-processes
|
|
116
|
-
#
|
|
117
|
-
# because errors are not raised, and you cannot get results back
|
|
118
|
-
# max_procs: - only run this many parallel processes at a time
|
|
118
|
+
# parallel: - true/false to execute in parallel, or an integer of how many
|
|
119
|
+
# sub-processes. Note that parallel invocation currently uses
|
|
120
|
+
# forking.
|
|
119
121
|
# exception: - :ignore, :raise, :defer (wait until the end and raise the first
|
|
120
122
|
# error), or a proc
|
|
121
|
-
def with_each_shard(*args, parallel: false,
|
|
123
|
+
def with_each_shard(*args, parallel: false, exception: :raise, &block)
|
|
122
124
|
raise ArgumentError, "wrong number of arguments (#{args.length} for 0...2)" if args.length > 2
|
|
123
125
|
|
|
124
126
|
return Array.wrap(yield) unless default.is_a?(Shard)
|
|
@@ -133,14 +135,13 @@ module Switchman
|
|
|
133
135
|
scope, classes = args
|
|
134
136
|
end
|
|
135
137
|
|
|
136
|
-
parallel =
|
|
138
|
+
parallel = [Environment.cpu_count || 2, 2].min if parallel == true
|
|
137
139
|
parallel = 0 if parallel == false || parallel.nil?
|
|
138
140
|
|
|
139
141
|
scope ||= Shard.all
|
|
140
142
|
scope = scope.order(::Arel.sql('database_server_id IS NOT NULL, database_server_id, id')) if ::ActiveRecord::Relation === scope && scope.order_values.empty?
|
|
141
143
|
|
|
142
|
-
if parallel
|
|
143
|
-
max_procs = determine_max_procs(max_procs, parallel)
|
|
144
|
+
if parallel > 1
|
|
144
145
|
if ::ActiveRecord::Relation === scope
|
|
145
146
|
# still need a post-uniq, cause the default database server could be NULL or Rails.env in the db
|
|
146
147
|
database_servers = scope.reorder('database_server_id').select(:database_server_id).distinct.
|
|
@@ -148,67 +149,11 @@ module Switchman
|
|
|
148
149
|
# nothing to do
|
|
149
150
|
return if database_servers.count.zero?
|
|
150
151
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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]
|
|
173
|
-
end.to_h
|
|
152
|
+
scopes = database_servers.to_h do |server|
|
|
153
|
+
[server, scope.merge(server.shards)]
|
|
154
|
+
end
|
|
174
155
|
else
|
|
175
156
|
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
|
-
end
|
|
185
|
-
|
|
186
|
-
exception_pipes = []
|
|
187
|
-
pids = []
|
|
188
|
-
out_fds = []
|
|
189
|
-
err_fds = []
|
|
190
|
-
pid_to_name_map = {}
|
|
191
|
-
fd_to_name_map = {}
|
|
192
|
-
errors = []
|
|
193
|
-
|
|
194
|
-
wait_for_output = lambda do
|
|
195
|
-
ready, = IO.select(out_fds + err_fds)
|
|
196
|
-
ready.each do |fd|
|
|
197
|
-
if fd.eof?
|
|
198
|
-
fd.close
|
|
199
|
-
out_fds.delete(fd)
|
|
200
|
-
err_fds.delete(fd)
|
|
201
|
-
next
|
|
202
|
-
end
|
|
203
|
-
line = fd.readline
|
|
204
|
-
puts "#{fd_to_name_map[fd]}: #{line}"
|
|
205
|
-
end
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
# 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
157
|
end
|
|
213
158
|
|
|
214
159
|
# clear connections prior to forking (no more queries will be executed in the parent,
|
|
@@ -216,95 +161,36 @@ module Switchman
|
|
|
216
161
|
# silly like dealloc'ing prepared statements)
|
|
217
162
|
::ActiveRecord::Base.clear_all_connections!
|
|
218
163
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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)
|
|
263
|
-
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
|
|
281
|
-
end
|
|
164
|
+
parent_process_name = `ps -ocommand= -p#{Process.pid}`.slice(/#{$0}.*/)
|
|
165
|
+
ret = ::Parallel.map(scopes, in_processes: scopes.length > 1 ? parallel : 0) do |server, subscope|
|
|
166
|
+
name = server.id
|
|
167
|
+
# rubocop:disable Style/GlobalStdStream
|
|
168
|
+
$stdout = Parallel::PrefixingIO.new(name, STDOUT)
|
|
169
|
+
$stderr = Parallel::PrefixingIO.new(name, STDERR)
|
|
170
|
+
# rubocop:enable Style/GlobalStdStream
|
|
171
|
+
begin
|
|
172
|
+
max_length = 128 - name.length - 3
|
|
173
|
+
short_parent_name = parent_process_name[0..max_length] if max_length >= 0
|
|
174
|
+
new_title = [short_parent_name, name].join(' ')
|
|
175
|
+
Process.setproctitle(new_title)
|
|
176
|
+
Switchman.config[:on_fork_proc]&.call
|
|
177
|
+
with_each_shard(subscope, classes, exception: exception, &block).map { |result| Parallel::ResultWrapper.new(result) }
|
|
178
|
+
rescue => e
|
|
179
|
+
logger.error e.full_message
|
|
180
|
+
Parallel::QuietExceptionWrapper.new(name, ::Parallel::ExceptionWrapper.new(e))
|
|
282
181
|
end
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
wait_for_output.call while out_fds.any? || err_fds.any?
|
|
286
|
-
pids.each do |pid|
|
|
287
|
-
_, status = Process.waitpid2(pid)
|
|
288
|
-
errors << pid_to_name_map[pid] if status.exitstatus != 0
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
# check for an exception; we only re-raise the first one
|
|
292
|
-
exception_pipes.each do |exception_pipe|
|
|
293
|
-
serialized_exception = exception_pipe.first.read
|
|
294
|
-
next if serialized_exception.empty?
|
|
295
|
-
|
|
296
|
-
ex = Marshal.load(serialized_exception) # rubocop:disable Security/MarshalLoad
|
|
297
|
-
raise ex
|
|
298
|
-
ensure
|
|
299
|
-
exception_pipe.first.close
|
|
300
|
-
end
|
|
182
|
+
end.flatten
|
|
301
183
|
|
|
184
|
+
errors = ret.select { |val| val.is_a?(Parallel::QuietExceptionWrapper) }
|
|
302
185
|
unless errors.empty?
|
|
303
|
-
raise
|
|
304
|
-
|
|
186
|
+
raise errors.first.exception if errors.length == 1
|
|
187
|
+
|
|
188
|
+
raise Errors::ParallelShardExecError,
|
|
189
|
+
"The following database server(s) did not finish processing cleanly: #{errors.map(&:name).sort.join(', ')}",
|
|
190
|
+
cause: errors.first.exception
|
|
305
191
|
end
|
|
306
192
|
|
|
307
|
-
return
|
|
193
|
+
return ret.map(&:result)
|
|
308
194
|
end
|
|
309
195
|
|
|
310
196
|
classes ||= []
|
|
@@ -402,7 +288,7 @@ module Switchman
|
|
|
402
288
|
signed_id_operation(local_id) do |id|
|
|
403
289
|
return nil if id > IDS_PER_SHARD
|
|
404
290
|
|
|
405
|
-
$1.to_i * IDS_PER_SHARD + id
|
|
291
|
+
($1.to_i * IDS_PER_SHARD) + id
|
|
406
292
|
end
|
|
407
293
|
when Integer, /^-?\d+$/
|
|
408
294
|
any_id.to_i
|
|
@@ -480,32 +366,15 @@ module Switchman
|
|
|
480
366
|
shard || source_shard || Shard.current
|
|
481
367
|
end
|
|
482
368
|
|
|
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
369
|
private
|
|
501
370
|
|
|
502
371
|
def add_sharded_model(klass)
|
|
503
372
|
@sharded_models = (sharded_models + [klass]).freeze
|
|
504
|
-
|
|
373
|
+
configure_connects_to
|
|
505
374
|
end
|
|
506
375
|
|
|
507
|
-
def
|
|
508
|
-
full_connects_to_hash = DatabaseServer.all.
|
|
376
|
+
def configure_connects_to
|
|
377
|
+
full_connects_to_hash = DatabaseServer.all.to_h { |db| [db.id.to_sym, db.connects_to_hash] }
|
|
509
378
|
sharded_models.each do |klass|
|
|
510
379
|
connects_to_hash = full_connects_to_hash.deep_dup
|
|
511
380
|
if klass == UnshardedRecord
|
|
@@ -596,6 +465,10 @@ module Switchman
|
|
|
596
465
|
Shard.default
|
|
597
466
|
end
|
|
598
467
|
|
|
468
|
+
def original_id
|
|
469
|
+
id
|
|
470
|
+
end
|
|
471
|
+
|
|
599
472
|
def activate(*classes, &block)
|
|
600
473
|
shards = hashify_classes(classes)
|
|
601
474
|
Shard.activate(shards, &block)
|
|
@@ -668,7 +541,7 @@ module Switchman
|
|
|
668
541
|
return nil unless local_id
|
|
669
542
|
|
|
670
543
|
self.class.signed_id_operation(local_id) do |abs_id|
|
|
671
|
-
abs_id + id * IDS_PER_SHARD
|
|
544
|
+
abs_id + (id * IDS_PER_SHARD)
|
|
672
545
|
end
|
|
673
546
|
end
|
|
674
547
|
|
|
@@ -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:
|
|
19
|
+
env: ::Rails.version < '7.0' ? @shard_host.pool.connection_klass&.current_role : @shard_host.pool.connection_class&.current_role
|
|
20
20
|
}
|
|
21
21
|
end
|
|
22
22
|
super name, payload
|
|
@@ -3,20 +3,19 @@
|
|
|
3
3
|
module Switchman
|
|
4
4
|
module StandardError
|
|
5
5
|
def initialize(*args)
|
|
6
|
-
|
|
7
|
-
#
|
|
8
|
-
if is_a?(::
|
|
9
|
-
super
|
|
10
|
-
return
|
|
11
|
-
end
|
|
6
|
+
super
|
|
7
|
+
# These seem to get themselves into a bad state if we try to lookup shards while processing
|
|
8
|
+
return if is_a?(IO::EAGAINWaitReadable)
|
|
12
9
|
|
|
13
|
-
if
|
|
14
|
-
@active_shards = Shard.sharded_models.map do |klass|
|
|
15
|
-
[klass, Shard.current(klass)]
|
|
16
|
-
end.compact.to_h
|
|
17
|
-
end
|
|
10
|
+
return if Thread.current[:switchman_error_handler]
|
|
18
11
|
|
|
19
|
-
|
|
12
|
+
begin
|
|
13
|
+
Thread.current[:switchman_error_handler] = true
|
|
14
|
+
|
|
15
|
+
@active_shards ||= Shard.active_shards
|
|
16
|
+
ensure
|
|
17
|
+
Thread.current[:switchman_error_handler] = nil
|
|
18
|
+
end
|
|
20
19
|
end
|
|
21
20
|
|
|
22
21
|
def current_shard(klass = ::ActiveRecord::Base)
|
data/lib/switchman/version.rb
CHANGED
data/lib/switchman.rb
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'guard_rail'
|
|
4
|
-
require '
|
|
5
|
-
|
|
4
|
+
require 'zeitwerk'
|
|
5
|
+
|
|
6
|
+
class SwitchmanInflector < Zeitwerk::GemInflector
|
|
7
|
+
def camelize(basename, abspath)
|
|
8
|
+
if basename =~ /\Apostgresql_(.*)/
|
|
9
|
+
'PostgreSQL' + super($1, abspath)
|
|
10
|
+
else
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
loader = Zeitwerk::Loader.for_gem
|
|
17
|
+
loader.inflector = SwitchmanInflector.new(__FILE__)
|
|
18
|
+
loader.setup
|
|
6
19
|
|
|
7
20
|
module Switchman
|
|
8
21
|
def self.config
|
|
@@ -18,5 +31,12 @@ module Switchman
|
|
|
18
31
|
@cache = cache
|
|
19
32
|
end
|
|
20
33
|
|
|
34
|
+
def self.foreign_key_check(name, type, limit: nil)
|
|
35
|
+
puts "WARNING: All foreign keys need to be 8-byte integers. #{name} looks like a foreign key. If so, please add the option: `:limit => 8`" if name.to_s =~ /_id\z/ && type.to_s == 'integer' && limit.to_i < 8
|
|
36
|
+
end
|
|
37
|
+
|
|
21
38
|
class OrderOnMultiShardQuery < RuntimeError; end
|
|
22
39
|
end
|
|
40
|
+
|
|
41
|
+
# Load the engine and everything associated at gem load time
|
|
42
|
+
Switchman::Engine
|
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
|
|
@@ -46,13 +57,11 @@ module Switchman
|
|
|
46
57
|
end
|
|
47
58
|
|
|
48
59
|
def self.options
|
|
49
|
-
{ parallel: ENV['PARALLEL'].to_i
|
|
60
|
+
{ parallel: ENV['PARALLEL'].to_i }
|
|
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,17 +86,15 @@ 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
|
|
87
94
|
end
|
|
88
95
|
|
|
89
96
|
%w[db:migrate db:migrate:up db:migrate:down db:rollback].each do |task_name|
|
|
90
|
-
shardify_task(task_name
|
|
97
|
+
shardify_task(task_name)
|
|
91
98
|
end
|
|
92
99
|
|
|
93
100
|
def self.shard_scope(scope, raw_shard_ids)
|
|
@@ -201,14 +208,18 @@ module Switchman
|
|
|
201
208
|
module PostgreSQLDatabaseTasks
|
|
202
209
|
def structure_dump(filename, extra_flags = nil)
|
|
203
210
|
set_psql_env
|
|
204
|
-
args = ['-
|
|
211
|
+
args = ['--schema-only', '--no-privileges', '--no-owner', '--file', filename]
|
|
205
212
|
args.concat(Array(extra_flags)) if extra_flags
|
|
206
213
|
shard = Shard.current.name
|
|
207
214
|
serialized_search_path = shard
|
|
208
215
|
args << "--schema=#{Shellwords.escape(shard)}"
|
|
209
216
|
|
|
210
|
-
|
|
217
|
+
ignore_tables = ::ActiveRecord::SchemaDumper.ignore_tables
|
|
218
|
+
args += ignore_tables.flat_map { |table| ['-T', table] } if ignore_tables.any?
|
|
219
|
+
|
|
220
|
+
args << db_config.database
|
|
211
221
|
run_cmd('pg_dump', args, 'dumping')
|
|
222
|
+
remove_sql_header_comments(filename)
|
|
212
223
|
File.open(filename, 'a') { |f| f << "SET search_path TO #{serialized_search_path};\n\n" }
|
|
213
224
|
end
|
|
214
225
|
end
|