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 +4 -4
- data/app/models/switchman/shard.rb +105 -131
- 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 +37 -20
- 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 +1 -1
- data/lib/switchman/active_record/relation.rb +1 -1
- data/lib/switchman/active_record/test_fixtures.rb +43 -0
- data/lib/switchman/database_server.rb +43 -45
- data/lib/switchman/engine.rb +4 -1
- data/lib/switchman/r_spec_helper.rb +2 -22
- data/lib/switchman/standard_error.rb +4 -4
- data/lib/switchman/version.rb +1 -1
- data/lib/tasks/switchman.rake +9 -5
- metadata +8 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 81aeec21e056026642c6aa15bda349e97c8194212f8b79222b4581d5cb2a9909
|
4
|
+
data.tar.gz: 77bfd14eaf245a399ec6a5b48a9f3106daac6df393d8c55d7dad5eae56966f15
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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.
|
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
|
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.
|
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
|
115
|
-
# sub-processes
|
116
|
-
#
|
117
|
-
#
|
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,
|
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 =
|
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
|
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
|
-
|
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
|
-
|
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,
|
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)
|
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
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
#
|
278
|
-
|
279
|
-
|
280
|
-
|
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
|
-
|
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
|
|
@@ -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 =
|
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
|
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
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
|
-
|
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
|
@@ -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
|
-
|
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
|
-
|
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,7 +1,7 @@
|
|
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.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:
|
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:
|
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
|
@@ -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.
|
317
|
+
rubygems_version: 3.1.4
|
317
318
|
signing_key:
|
318
319
|
specification_version: 4
|
319
320
|
summary: Rails sharding magic
|