switchman 3.0.2 → 3.0.9
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/app/models/switchman/shard.rb +112 -134
- 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/active_record/abstract_adapter.rb +0 -9
- data/lib/switchman/active_record/attribute_methods.rb +24 -3
- data/lib/switchman/active_record/base.rb +38 -21
- data/lib/switchman/active_record/calculations.rb +1 -1
- data/lib/switchman/active_record/connection_pool.rb +9 -29
- data/lib/switchman/active_record/migration.rb +7 -4
- data/lib/switchman/active_record/persistence.rb +7 -0
- data/lib/switchman/active_record/postgresql_adapter.rb +5 -1
- data/lib/switchman/active_record/predicate_builder.rb +1 -1
- data/lib/switchman/active_record/query_methods.rb +7 -3
- data/lib/switchman/active_record/relation.rb +10 -6
- data/lib/switchman/active_record/test_fixtures.rb +43 -0
- data/lib/switchman/arel.rb +28 -6
- data/lib/switchman/database_server.rb +45 -47
- data/lib/switchman/engine.rb +5 -1
- data/lib/switchman/r_spec_helper.rb +2 -17
- data/lib/switchman/rails.rb +1 -1
- data/lib/switchman/standard_error.rb +4 -4
- data/lib/switchman/version.rb +1 -1
- data/lib/tasks/switchman.rake +9 -5
- metadata +13 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a8f502f6ec406e1292800c9ae65cbb75e584f1059c466ec732c8aeb50379c1df
|
|
4
|
+
data.tar.gz: 823fd74a80bf27e0ddfd29d22a03f21f0b76e3d119005211414d4e998f59d5ad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f6010a838cbb0f17f74aa12a6859d41c3a6a44c0752b130077d9c762c1c4e7d37e32f0d06fb11aa26d6727b41e41d2b1873d1ac5f5889dc77815379148e332df
|
|
7
|
+
data.tar.gz: 7479c21a001fa920c4a81a9fb797a8468dd69dca4d190e636b9ceaf159381cf122c7e10395f3ecf4902bbd435841a318636375ae72130d7dfd8f9d4632d02002
|
|
@@ -39,12 +39,15 @@ 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
|
|
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
|
+
# Otherwise, rescue the fake default if the table doesn't exist
|
|
45
47
|
rescue
|
|
46
|
-
default
|
|
48
|
+
sharding_initialized ? default : nil
|
|
47
49
|
end
|
|
50
|
+
return default unless @default
|
|
48
51
|
|
|
49
52
|
# make sure this is not erroneously cached
|
|
50
53
|
@default.database_server.remove_instance_variable(:@primary_shard) if @default.database_server.instance_variable_defined?(:@primary_shard)
|
|
@@ -59,34 +62,38 @@ module Switchman
|
|
|
59
62
|
|
|
60
63
|
def current(klass = ::ActiveRecord::Base)
|
|
61
64
|
klass ||= ::ActiveRecord::Base
|
|
62
|
-
klass.
|
|
65
|
+
klass.current_switchman_shard
|
|
63
66
|
end
|
|
64
67
|
|
|
65
68
|
def activate(shards)
|
|
66
69
|
activated_classes = activate!(shards)
|
|
67
70
|
yield
|
|
68
71
|
ensure
|
|
69
|
-
activated_classes
|
|
70
|
-
klass.connection_pool.shard_stack.pop
|
|
72
|
+
activated_classes&.each do |klass|
|
|
71
73
|
klass.connected_to_stack.pop
|
|
72
74
|
end
|
|
73
75
|
end
|
|
74
76
|
|
|
75
77
|
def activate!(shards)
|
|
76
|
-
activated_classes =
|
|
78
|
+
activated_classes = nil
|
|
77
79
|
shards.each do |klass, shard|
|
|
78
80
|
next if klass == UnshardedRecord
|
|
79
81
|
|
|
80
82
|
next unless klass.current_shard != shard.database_server.id.to_sym ||
|
|
81
|
-
klass.
|
|
83
|
+
klass.current_switchman_shard != shard
|
|
82
84
|
|
|
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
|
|
85
|
+
(activated_classes ||= []) << klass
|
|
86
|
+
klass.connected_to_stack << { shard: shard.database_server.id.to_sym, klasses: [klass], switchman_shard: shard }
|
|
86
87
|
end
|
|
87
88
|
activated_classes
|
|
88
89
|
end
|
|
89
90
|
|
|
91
|
+
def active_shards
|
|
92
|
+
sharded_models.map do |klass|
|
|
93
|
+
[klass, current(klass)]
|
|
94
|
+
end.compact.to_h
|
|
95
|
+
end
|
|
96
|
+
|
|
90
97
|
def lookup(id)
|
|
91
98
|
id_i = id.to_i
|
|
92
99
|
return current if id_i == current.id || id == 'self'
|
|
@@ -103,6 +110,11 @@ module Switchman
|
|
|
103
110
|
cached_shards[id]
|
|
104
111
|
end
|
|
105
112
|
|
|
113
|
+
def preload_cache
|
|
114
|
+
cached_shards.reverse_merge!(active_shards.values.index_by(&:id))
|
|
115
|
+
cached_shards.reverse_merge!(all.index_by(&:id))
|
|
116
|
+
end
|
|
117
|
+
|
|
106
118
|
def clear_cache
|
|
107
119
|
cached_shards.clear
|
|
108
120
|
end
|
|
@@ -111,14 +123,13 @@ module Switchman
|
|
|
111
123
|
#
|
|
112
124
|
# * +shards+ - an array or relation of Shards to iterate over
|
|
113
125
|
# * +classes+ - an array of classes to activate
|
|
114
|
-
# parallel: - true/false to execute in parallel, or
|
|
115
|
-
# sub-processes
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
# max_procs: - only run this many parallel processes at a time
|
|
126
|
+
# parallel: - true/false to execute in parallel, or an integer of how many
|
|
127
|
+
# sub-processes. Note that parallel invocation currently uses
|
|
128
|
+
# forking, so should be used sparingly because you cannot get
|
|
129
|
+
# results back
|
|
119
130
|
# exception: - :ignore, :raise, :defer (wait until the end and raise the first
|
|
120
131
|
# error), or a proc
|
|
121
|
-
def with_each_shard(*args, parallel: false,
|
|
132
|
+
def with_each_shard(*args, parallel: false, exception: :raise, &block)
|
|
122
133
|
raise ArgumentError, "wrong number of arguments (#{args.length} for 0...2)" if args.length > 2
|
|
123
134
|
|
|
124
135
|
return Array.wrap(yield) unless default.is_a?(Shard)
|
|
@@ -133,14 +144,13 @@ module Switchman
|
|
|
133
144
|
scope, classes = args
|
|
134
145
|
end
|
|
135
146
|
|
|
136
|
-
parallel =
|
|
147
|
+
parallel = [Environment.cpu_count || 2, 2].min if parallel == true
|
|
137
148
|
parallel = 0 if parallel == false || parallel.nil?
|
|
138
149
|
|
|
139
150
|
scope ||= Shard.all
|
|
140
151
|
scope = scope.order(::Arel.sql('database_server_id IS NOT NULL, database_server_id, id')) if ::ActiveRecord::Relation === scope && scope.order_values.empty?
|
|
141
152
|
|
|
142
|
-
if parallel
|
|
143
|
-
max_procs = determine_max_procs(max_procs, parallel)
|
|
153
|
+
if parallel > 1
|
|
144
154
|
if ::ActiveRecord::Relation === scope
|
|
145
155
|
# still need a post-uniq, cause the default database server could be NULL or Rails.env in the db
|
|
146
156
|
database_servers = scope.reorder('database_server_id').select(:database_server_id).distinct.
|
|
@@ -148,39 +158,11 @@ module Switchman
|
|
|
148
158
|
# nothing to do
|
|
149
159
|
return if database_servers.count.zero?
|
|
150
160
|
|
|
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
|
|
161
|
+
scopes = database_servers.to_h do |server|
|
|
162
|
+
[server, server.shards.merge(scope)]
|
|
163
|
+
end
|
|
174
164
|
else
|
|
175
165
|
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
166
|
end
|
|
185
167
|
|
|
186
168
|
exception_pipes = []
|
|
@@ -206,80 +188,83 @@ module Switchman
|
|
|
206
188
|
end
|
|
207
189
|
|
|
208
190
|
# only one process; don't bother forking
|
|
209
|
-
|
|
210
|
-
return with_each_shard(scopes.first.last.first, classes, exception: exception,
|
|
211
|
-
&block)
|
|
212
|
-
end
|
|
191
|
+
return with_each_shard(scopes.first.last, classes, exception: exception, &block) if scopes.length == 1
|
|
213
192
|
|
|
214
193
|
# clear connections prior to forking (no more queries will be executed in the parent,
|
|
215
194
|
# and we want them gone so that we don't accidentally use them post-fork doing something
|
|
216
195
|
# silly like dealloc'ing prepared statements)
|
|
217
196
|
::ActiveRecord::Base.clear_all_connections!
|
|
218
197
|
|
|
219
|
-
scopes.each do |server,
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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)
|
|
198
|
+
scopes.each do |server, subscope|
|
|
199
|
+
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(' ')
|
|
216
|
+
Process.setproctitle(new_title)
|
|
217
|
+
|
|
218
|
+
with_each_shard(subscope, classes, exception: exception, &block)
|
|
263
219
|
exception_pipe.last.close
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
|
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
|
|
281
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
|
|
282
263
|
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
|
|
283
268
|
end
|
|
284
269
|
|
|
285
270
|
wait_for_output.call while out_fds.any? || err_fds.any?
|
|
@@ -402,7 +387,7 @@ module Switchman
|
|
|
402
387
|
signed_id_operation(local_id) do |id|
|
|
403
388
|
return nil if id > IDS_PER_SHARD
|
|
404
389
|
|
|
405
|
-
$1.to_i * IDS_PER_SHARD + id
|
|
390
|
+
($1.to_i * IDS_PER_SHARD) + id
|
|
406
391
|
end
|
|
407
392
|
when Integer, /^-?\d+$/
|
|
408
393
|
any_id.to_i
|
|
@@ -480,32 +465,19 @@ module Switchman
|
|
|
480
465
|
shard || source_shard || Shard.current
|
|
481
466
|
end
|
|
482
467
|
|
|
483
|
-
|
|
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
|
|
468
|
+
private
|
|
496
469
|
|
|
497
|
-
|
|
470
|
+
def sharding_initialized
|
|
471
|
+
@sharding_initialized ||= false
|
|
498
472
|
end
|
|
499
473
|
|
|
500
|
-
private
|
|
501
|
-
|
|
502
474
|
def add_sharded_model(klass)
|
|
503
475
|
@sharded_models = (sharded_models + [klass]).freeze
|
|
504
476
|
initialize_sharding
|
|
505
477
|
end
|
|
506
478
|
|
|
507
479
|
def initialize_sharding
|
|
508
|
-
full_connects_to_hash = DatabaseServer.all.
|
|
480
|
+
full_connects_to_hash = DatabaseServer.all.to_h { |db| [db.id.to_sym, db.connects_to_hash] }
|
|
509
481
|
sharded_models.each do |klass|
|
|
510
482
|
connects_to_hash = full_connects_to_hash.deep_dup
|
|
511
483
|
if klass == UnshardedRecord
|
|
@@ -526,6 +498,8 @@ module Switchman
|
|
|
526
498
|
|
|
527
499
|
klass.connects_to shards: connects_to_hash
|
|
528
500
|
end
|
|
501
|
+
|
|
502
|
+
@sharding_initialized = true
|
|
529
503
|
end
|
|
530
504
|
|
|
531
505
|
# in-process caching
|
|
@@ -596,6 +570,10 @@ module Switchman
|
|
|
596
570
|
Shard.default
|
|
597
571
|
end
|
|
598
572
|
|
|
573
|
+
def original_id
|
|
574
|
+
id
|
|
575
|
+
end
|
|
576
|
+
|
|
599
577
|
def activate(*classes, &block)
|
|
600
578
|
shards = hashify_classes(classes)
|
|
601
579
|
Shard.activate(shards, &block)
|
|
@@ -668,7 +646,7 @@ module Switchman
|
|
|
668
646
|
return nil unless local_id
|
|
669
647
|
|
|
670
648
|
self.class.signed_id_operation(local_id) do |abs_id|
|
|
671
|
-
abs_id + id * IDS_PER_SHARD
|
|
649
|
+
abs_id + (id * IDS_PER_SHARD)
|
|
672
650
|
end
|
|
673
651
|
end
|
|
674
652
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
90
|
+
def self.prepended(klass)
|
|
83
91
|
klass.singleton_class.prepend(ClassMethods)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
120
|
+
super
|
|
111
121
|
end
|
|
112
122
|
|
|
113
123
|
def save!(*, **)
|
|
114
124
|
@shard_set_in_stone = true
|
|
115
|
-
|
|
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
|
|
153
|
-
|
|
168
|
+
def update_columns(*)
|
|
169
|
+
db = shard.database_server
|
|
170
|
+
return db.unguard { super } if ::GuardRail.environment != db.guard_rail_environment
|
|
154
171
|
|
|
155
|
-
|
|
156
|
-
self.class.connection.quote(id)
|
|
172
|
+
super
|
|
157
173
|
end
|
|
158
174
|
|
|
159
|
-
def
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
181
|
+
super
|
|
165
182
|
end
|
|
166
183
|
end
|
|
167
184
|
|
|
@@ -172,7 +189,7 @@ module Switchman
|
|
|
172
189
|
if reflection
|
|
173
190
|
if reflection.options[:polymorphic]
|
|
174
191
|
begin
|
|
175
|
-
read_attribute(reflection.foreign_type)&.constantize&.connection_classes
|
|
192
|
+
read_attribute(reflection.foreign_type)&.constantize&.connection_classes || ::ActiveRecord::Base
|
|
176
193
|
rescue NameError
|
|
177
194
|
# in case someone is abusing foreign_type to not point to an actual class
|
|
178
195
|
::ActiveRecord::Base
|
|
@@ -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
|
|
@@ -83,7 +83,7 @@ module Switchman
|
|
|
83
83
|
if opts[:association]
|
|
84
84
|
key_ids = calculated_data.collect { |row| row[opts[:group_aliases].first] }
|
|
85
85
|
key_records = opts[:association].klass.base_class.where(id: key_ids)
|
|
86
|
-
key_records = key_records.
|
|
86
|
+
key_records = key_records.to_h { |r| [Shard.relative_id_for(r, shard, target_shard), r] }
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
calculated_data.map do |row|
|
|
@@ -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 =
|
|
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
|
|
23
|
+
raise NonExistentShardError if current_shard.new_record?
|
|
36
24
|
|
|
37
|
-
switch_database(conn) if conn.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' && !
|
|
36
|
+
@schemas = conn.current_schemas if !@schemas && conn.adapter_name == 'PostgreSQL' && !current_shard.database_server.config[:shard_name]
|
|
61
37
|
|
|
62
|
-
conn.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.
|
|
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:
|
|
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
|
-
|
|
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)
|
|
35
|
+
def drop_database(name) # :nodoc:
|
|
36
36
|
execute "DROP DATABASE IF EXISTS #{quote_local_table_name(name)}"
|
|
37
37
|
end
|
|
38
38
|
|
|
@@ -87,6 +87,10 @@ module Switchman
|
|
|
87
87
|
name.quoted
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
+
def with_global_table_name(&block)
|
|
91
|
+
with_local_table_name(false, &block)
|
|
92
|
+
end
|
|
93
|
+
|
|
90
94
|
def with_local_table_name(enable = true) # rubocop:disable Style/OptionalBooleanParameter
|
|
91
95
|
old_value = @use_local_table_name
|
|
92
96
|
@use_local_table_name = enable
|
|
@@ -239,6 +239,10 @@ module Switchman
|
|
|
239
239
|
connection.with_local_table_name { super }
|
|
240
240
|
end
|
|
241
241
|
|
|
242
|
+
def table_name_matches?(from)
|
|
243
|
+
connection.with_global_table_name { super }
|
|
244
|
+
end
|
|
245
|
+
|
|
242
246
|
def transpose_predicates(predicates,
|
|
243
247
|
source_shard,
|
|
244
248
|
target_shard,
|
|
@@ -266,11 +270,11 @@ module Switchman
|
|
|
266
270
|
right_node = or_expr.right
|
|
267
271
|
new_left_predicates = transpose_single_predicate(left_node, source_shard,
|
|
268
272
|
target_shard, remove_nonlocal_primary_keys: remove_nonlocal_primary_keys)
|
|
269
|
-
or_expr.instance_variable_set(:@left, new_left_predicates) if new_left_predicates != left_node
|
|
270
273
|
new_right_predicates = transpose_single_predicate(right_node, source_shard,
|
|
271
274
|
target_shard, remove_nonlocal_primary_keys: remove_nonlocal_primary_keys)
|
|
272
|
-
|
|
273
|
-
|
|
275
|
+
return predicate if new_left_predicates == left_node && new_right_predicates == right_node
|
|
276
|
+
|
|
277
|
+
return ::Arel::Nodes::Grouping.new ::Arel::Nodes::Or.new(new_left_predicates, new_right_predicates)
|
|
274
278
|
end
|
|
275
279
|
return predicate unless predicate.is_a?(::Arel::Nodes::Binary) || predicate.is_a?(::Arel::Nodes::HomogeneousIn)
|
|
276
280
|
return predicate unless predicate.left.is_a?(::Arel::Attributes::Attribute)
|
|
@@ -63,7 +63,7 @@ module Switchman
|
|
|
63
63
|
%I[update_all delete_all].each do |method|
|
|
64
64
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
65
65
|
def #{method}(*args)
|
|
66
|
-
result = self.activate { |relation| relation.call_super(#{method.inspect}, Relation, *args) }
|
|
66
|
+
result = self.activate(unordered: true) { |relation| relation.call_super(#{method.inspect}, Relation, *args) }
|
|
67
67
|
result = result.sum if result.is_a?(Array)
|
|
68
68
|
result
|
|
69
69
|
end
|
|
@@ -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
|
|
@@ -103,7 +103,7 @@ module Switchman
|
|
|
103
103
|
end
|
|
104
104
|
end
|
|
105
105
|
|
|
106
|
-
def activate(&block)
|
|
106
|
+
def activate(unordered: false, &block)
|
|
107
107
|
shards = all_shards
|
|
108
108
|
if Array === shards && shards.length == 1
|
|
109
109
|
if shards.first == DefaultShard || shards.first == Shard.current(klass.connection_classes)
|
|
@@ -124,7 +124,7 @@ module Switchman
|
|
|
124
124
|
|
|
125
125
|
shard_results = relation.activate(&block)
|
|
126
126
|
|
|
127
|
-
if shard_results.present?
|
|
127
|
+
if shard_results.present? && !unordered
|
|
128
128
|
can_order ||= can_order_cross_shard_results? unless order_values.empty?
|
|
129
129
|
raise OrderOnMultiShardQuery if !can_order && !order_values.empty? && result_count.positive?
|
|
130
130
|
|
|
@@ -148,8 +148,12 @@ module Switchman
|
|
|
148
148
|
results.sort! do |l, r|
|
|
149
149
|
result = 0
|
|
150
150
|
order_values.each do |ov|
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
if l.is_a?(::ActiveRecord::Base)
|
|
152
|
+
a = l.attribute(ov.expr.name)
|
|
153
|
+
b = r.attribute(ov.expr.name)
|
|
154
|
+
else
|
|
155
|
+
a, b = l, r
|
|
156
|
+
end
|
|
153
157
|
next if a == b
|
|
154
158
|
|
|
155
159
|
if a.nil? || b.nil?
|
|
@@ -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
|
data/lib/switchman/arel.rb
CHANGED
|
@@ -11,22 +11,44 @@ module Switchman
|
|
|
11
11
|
module Visitors
|
|
12
12
|
module ToSql
|
|
13
13
|
# rubocop:disable Naming/MethodName
|
|
14
|
+
# rubocop:disable Naming/MethodParameterName
|
|
14
15
|
|
|
15
|
-
def visit_Arel_Nodes_TableAlias(
|
|
16
|
-
o, collector = args
|
|
16
|
+
def visit_Arel_Nodes_TableAlias(o, collector)
|
|
17
17
|
collector = visit o.relation, collector
|
|
18
18
|
collector << ' '
|
|
19
19
|
collector << quote_local_table_name(o.name)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def visit_Arel_Attributes_Attribute(
|
|
23
|
-
o = args.first
|
|
22
|
+
def visit_Arel_Attributes_Attribute(o, collector)
|
|
24
23
|
join_name = o.relation.table_alias || o.relation.name
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
collector << quote_local_table_name(join_name) << '.' << quote_column_name(o.name)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def visit_Arel_Nodes_HomogeneousIn(o, collector)
|
|
28
|
+
collector.preparable = false
|
|
29
|
+
|
|
30
|
+
collector << quote_local_table_name(o.table_name) << '.' << quote_column_name(o.column_name)
|
|
31
|
+
|
|
32
|
+
collector << if o.type == :in
|
|
33
|
+
' IN ('
|
|
34
|
+
else
|
|
35
|
+
' NOT IN ('
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
values = o.casted_values
|
|
39
|
+
|
|
40
|
+
if values.empty?
|
|
41
|
+
collector << @connection.quote(nil)
|
|
42
|
+
else
|
|
43
|
+
collector.add_binds(values, o.proc_for_binds, &bind_block)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
collector << ')'
|
|
47
|
+
collector
|
|
27
48
|
end
|
|
28
49
|
|
|
29
50
|
# rubocop:enable Naming/MethodName
|
|
51
|
+
# rubocop:enable Naming/MethodParameterName
|
|
30
52
|
|
|
31
53
|
def quote_local_table_name(name)
|
|
32
54
|
return name if ::Arel::Nodes::SqlLiteral === name
|
|
@@ -59,7 +59,7 @@ module Switchman
|
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def database_servers
|
|
62
|
-
|
|
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?('/')
|
|
@@ -89,13 +89,13 @@ module Switchman
|
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
def connects_to_hash
|
|
92
|
-
self.class.all_roles.
|
|
92
|
+
self.class.all_roles.to_h do |role|
|
|
93
93
|
config_role = role
|
|
94
94
|
config_role = :primary unless roles.include?(role)
|
|
95
95
|
config_name = :"#{id}/#{config_role}"
|
|
96
96
|
config_name = :primary if id == ::Rails.env && config_role == :primary
|
|
97
97
|
[role.to_sym, config_name]
|
|
98
|
-
end
|
|
98
|
+
end
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
def destroy
|
|
@@ -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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
end
|
|
203
|
+
Array(create_statement.call).each do |stmt|
|
|
204
|
+
::ActiveRecord::Base.connection.execute(stmt)
|
|
211
205
|
end
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
206
|
+
end
|
|
207
|
+
if config[:adapter] == 'postgresql'
|
|
208
|
+
old_proc = ::ActiveRecord::Base.connection.raw_connection.set_notice_processor do
|
|
209
|
+
end
|
|
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
|
-
|
|
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
|
|
data/lib/switchman/engine.rb
CHANGED
|
@@ -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
|
-
|
|
105
|
+
prepend ActiveRecord::Base
|
|
105
106
|
include ActiveRecord::AttributeMethods
|
|
106
107
|
include ActiveRecord::Persistence
|
|
107
108
|
singleton_class.prepend ActiveRecord::ModelSchema::ClassMethods
|
|
@@ -150,9 +151,12 @@ module Switchman
|
|
|
150
151
|
::ActiveRecord::Relation.include(CallSuper)
|
|
151
152
|
|
|
152
153
|
::ActiveRecord::PredicateBuilder::AssociationQueryValue.prepend(ActiveRecord::PredicateBuilder::AssociationQueryValue)
|
|
154
|
+
::ActiveRecord::PredicateBuilder::PolymorphicArrayValue.prepend(ActiveRecord::PredicateBuilder::AssociationQueryValue)
|
|
153
155
|
|
|
154
156
|
::ActiveRecord::Tasks::DatabaseTasks.singleton_class.prepend(ActiveRecord::Tasks::DatabaseTasks)
|
|
155
157
|
|
|
158
|
+
::ActiveRecord::TestFixtures.prepend(ActiveRecord::TestFixtures)
|
|
159
|
+
|
|
156
160
|
::ActiveRecord::TypeCaster::Map.include(ActiveRecord::TypeCaster::Map)
|
|
157
161
|
::ActiveRecord::TypeCaster::Connection.include(ActiveRecord::TypeCaster::Connection)
|
|
158
162
|
|
|
@@ -111,28 +111,12 @@ module Switchman
|
|
|
111
111
|
Shard.default(reload: true)
|
|
112
112
|
@shard1 = Shard.find(@shard1.id)
|
|
113
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
114
|
end
|
|
122
115
|
end
|
|
123
116
|
|
|
124
117
|
klass.after do
|
|
125
118
|
next if @@sharding_failed
|
|
126
119
|
|
|
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
120
|
# clean up after specs
|
|
137
121
|
DatabaseServer.all.each do |ds|
|
|
138
122
|
if ds.fake? && ds != @shard2.database_server
|
|
@@ -143,7 +127,8 @@ module Switchman
|
|
|
143
127
|
end
|
|
144
128
|
|
|
145
129
|
klass.after(:all) do
|
|
146
|
-
|
|
130
|
+
# Don't truncate because that can create some fun cross-connection lock contention
|
|
131
|
+
Shard.delete_all
|
|
147
132
|
Switchman.cache.delete('default_shard')
|
|
148
133
|
Shard.default(reload: true)
|
|
149
134
|
end
|
data/lib/switchman/rails.rb
CHANGED
|
@@ -10,10 +10,10 @@ module Switchman
|
|
|
10
10
|
return
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
@active_shards = Shard.
|
|
15
|
-
|
|
16
|
-
|
|
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
|
data/lib/switchman/version.rb
CHANGED
data/lib/tasks/switchman.rake
CHANGED
|
@@ -46,7 +46,7 @@ module Switchman
|
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def self.options
|
|
49
|
-
{ parallel: ENV['PARALLEL'].to_i
|
|
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
|
-
|
|
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
|
|
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 = ['-
|
|
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
|
-
|
|
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,16 +1,16 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: switchman
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.0.
|
|
4
|
+
version: 3.0.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Cody Cutrer
|
|
8
8
|
- James Williams
|
|
9
9
|
- Jacob Fugal
|
|
10
|
-
autorequire:
|
|
10
|
+
autorequire:
|
|
11
11
|
bindir: bin
|
|
12
12
|
cert_chain: []
|
|
13
|
-
date:
|
|
13
|
+
date: 2022-03-09 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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
@@ -297,8 +298,9 @@ files:
|
|
|
297
298
|
homepage: http://www.instructure.com/
|
|
298
299
|
licenses:
|
|
299
300
|
- MIT
|
|
300
|
-
metadata:
|
|
301
|
-
|
|
301
|
+
metadata:
|
|
302
|
+
rubygems_mfa_required: 'true'
|
|
303
|
+
post_install_message:
|
|
302
304
|
rdoc_options: []
|
|
303
305
|
require_paths:
|
|
304
306
|
- lib
|
|
@@ -313,8 +315,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
313
315
|
- !ruby/object:Gem::Version
|
|
314
316
|
version: '0'
|
|
315
317
|
requirements: []
|
|
316
|
-
rubygems_version: 3.
|
|
317
|
-
signing_key:
|
|
318
|
+
rubygems_version: 3.1.4
|
|
319
|
+
signing_key:
|
|
318
320
|
specification_version: 4
|
|
319
321
|
summary: Rails sharding magic
|
|
320
322
|
test_files: []
|