switchman 2.2.2 → 3.0.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 +10 -2
- data/app/models/switchman/shard.rb +272 -275
- data/app/models/switchman/unsharded_record.rb +7 -0
- data/db/migrate/20130328212039_create_switchman_shards.rb +1 -1
- data/db/migrate/20130328224244_create_default_shard.rb +5 -5
- data/db/migrate/20161206323434_add_back_default_string_limits_switchman.rb +1 -0
- data/db/migrate/20180828183945_add_default_shard_index.rb +2 -2
- data/db/migrate/20180828192111_add_timestamps_to_shards.rb +7 -5
- data/db/migrate/20190114212900_add_unique_name_indexes.rb +5 -3
- data/lib/switchman/action_controller/caching.rb +2 -2
- data/lib/switchman/active_record/abstract_adapter.rb +1 -0
- data/lib/switchman/active_record/association.rb +78 -89
- data/lib/switchman/active_record/attribute_methods.rb +58 -73
- data/lib/switchman/active_record/base.rb +59 -60
- data/lib/switchman/active_record/calculations.rb +74 -67
- data/lib/switchman/active_record/connection_pool.rb +14 -43
- data/lib/switchman/active_record/database_configurations/database_config.rb +13 -0
- data/lib/switchman/active_record/database_configurations.rb +34 -0
- data/lib/switchman/active_record/finder_methods.rb +11 -16
- data/lib/switchman/active_record/log_subscriber.rb +4 -8
- data/lib/switchman/active_record/migration.rb +7 -51
- data/lib/switchman/active_record/model_schema.rb +1 -1
- data/lib/switchman/active_record/persistence.rb +3 -14
- data/lib/switchman/active_record/postgresql_adapter.rb +125 -169
- data/lib/switchman/active_record/predicate_builder.rb +2 -2
- data/lib/switchman/active_record/query_cache.rb +18 -19
- data/lib/switchman/active_record/query_methods.rb +172 -197
- data/lib/switchman/active_record/reflection.rb +7 -22
- data/lib/switchman/active_record/relation.rb +30 -78
- data/lib/switchman/active_record/spawn_methods.rb +27 -29
- data/lib/switchman/active_record/statement_cache.rb +18 -35
- data/lib/switchman/active_record/tasks/database_tasks.rb +16 -0
- data/lib/switchman/active_support/cache.rb +3 -5
- data/lib/switchman/arel.rb +13 -8
- data/lib/switchman/database_server.rb +121 -142
- data/lib/switchman/default_shard.rb +52 -16
- data/lib/switchman/engine.rb +62 -59
- data/lib/switchman/environment.rb +4 -8
- data/lib/switchman/errors.rb +1 -0
- data/lib/switchman/guard_rail/relation.rb +5 -7
- data/lib/switchman/guard_rail.rb +6 -19
- data/lib/switchman/r_spec_helper.rb +29 -37
- data/lib/switchman/rails.rb +14 -12
- data/lib/switchman/schema_cache.rb +1 -9
- data/lib/switchman/sharded_instrumenter.rb +1 -1
- data/lib/switchman/standard_error.rb +15 -3
- data/lib/switchman/test_helper.rb +6 -4
- data/lib/switchman/version.rb +1 -1
- data/lib/switchman.rb +3 -5
- data/lib/tasks/switchman.rake +55 -71
- metadata +90 -48
- data/lib/switchman/active_record/batches.rb +0 -11
- data/lib/switchman/active_record/connection_handler.rb +0 -190
- data/lib/switchman/active_record/where_clause_factory.rb +0 -36
- data/lib/switchman/connection_pool_proxy.rb +0 -173
@@ -6,44 +6,24 @@ require 'switchman/environment'
|
|
6
6
|
require 'switchman/errors'
|
7
7
|
|
8
8
|
module Switchman
|
9
|
-
class Shard <
|
9
|
+
class Shard < UnshardedRecord
|
10
10
|
# ten trillion possible ids per shard. yup.
|
11
11
|
IDS_PER_SHARD = 10_000_000_000_000
|
12
12
|
|
13
|
-
CATEGORIES =
|
14
|
-
{
|
15
|
-
# special cased to mean all other models
|
16
|
-
:primary => nil,
|
17
|
-
# special cased to not allow activating a shard other than the default
|
18
|
-
:unsharded => [Shard]
|
19
|
-
}
|
20
|
-
private_constant :CATEGORIES
|
21
|
-
@connection_specification_name = 'unsharded'
|
22
|
-
|
23
|
-
if defined?(::ProtectedAttributes)
|
24
|
-
attr_accessible :default, :name, :database_server
|
25
|
-
end
|
26
|
-
|
27
13
|
# only allow one default
|
28
|
-
validates_uniqueness_of :default, :
|
14
|
+
validates_uniqueness_of :default, if: ->(s) { s.default? }
|
29
15
|
|
30
16
|
after_save :clear_cache
|
31
17
|
after_destroy :clear_cache
|
32
18
|
|
33
|
-
after_rollback :on_rollback
|
34
|
-
|
35
19
|
scope :primary, -> { where(name: nil).order(:database_server_id, :id).distinct_on(:database_server_id) }
|
36
20
|
|
37
21
|
class << self
|
38
|
-
def
|
39
|
-
|
22
|
+
def sharded_models
|
23
|
+
@sharded_models ||= [::ActiveRecord::Base, UnshardedRecord].freeze
|
40
24
|
end
|
41
25
|
|
42
|
-
def default(
|
43
|
-
if reload_deprecated
|
44
|
-
reload = reload_deprecated
|
45
|
-
::ActiveSupport::Deprecation.warn("positional reload parameter to Switchman::Shard.default is deprecated; use `reload: true`")
|
46
|
-
end
|
26
|
+
def default(reload: false, with_fallback: false)
|
47
27
|
if !@default || reload
|
48
28
|
# Have to create a dummy object so that several key methods still work
|
49
29
|
# (it's easier to do this in one place here, and just assume that sharding
|
@@ -54,74 +34,68 @@ module Switchman
|
|
54
34
|
|
55
35
|
# if we already have a default shard in place, and the caller wants
|
56
36
|
# to use it as a fallback, use that instead of the dummy instance
|
57
|
-
if with_fallback && @default
|
58
|
-
default = @default
|
59
|
-
end
|
37
|
+
default = @default if with_fallback && @default
|
60
38
|
|
61
39
|
# the first time we need a dummy dummy for re-entrancy to avoid looping on ourselves
|
62
40
|
@default ||= default
|
63
41
|
|
64
42
|
# Now find the actual record, if it exists; rescue the fake default if the table doesn't exist
|
65
43
|
@default = begin
|
66
|
-
find_cached(
|
44
|
+
find_cached('default_shard') { Shard.where(default: true).take } || default
|
67
45
|
rescue
|
68
46
|
default
|
69
47
|
end
|
70
48
|
|
71
|
-
# rebuild current shard activations - it might have "another" default shard serialized there
|
72
|
-
active_shards.replace(active_shards.dup.map do |category, shard|
|
73
|
-
shard = Shard.lookup((!shard || shard.default?) ? 'default' : shard.id)
|
74
|
-
[category, shard]
|
75
|
-
end.to_h)
|
76
|
-
|
77
|
-
activate!(primary: @default) if active_shards.empty?
|
78
|
-
|
79
49
|
# make sure this is not erroneously cached
|
80
|
-
if @default.database_server.instance_variable_defined?(:@primary_shard)
|
81
|
-
@default.database_server.remove_instance_variable(:@primary_shard)
|
82
|
-
end
|
50
|
+
@default.database_server.remove_instance_variable(:@primary_shard) if @default.database_server.instance_variable_defined?(:@primary_shard)
|
83
51
|
|
84
52
|
# and finally, check for cached references to the default shard on the existing connection
|
85
|
-
|
86
|
-
|
53
|
+
sharded_models.each do |klass|
|
54
|
+
klass.connection.shard = @default if klass.connected? && klass.connection.shard.default?
|
87
55
|
end
|
88
56
|
end
|
89
57
|
@default
|
90
58
|
end
|
91
59
|
|
92
|
-
def current(
|
93
|
-
|
60
|
+
def current(klass = ::ActiveRecord::Base)
|
61
|
+
klass ||= ::ActiveRecord::Base
|
62
|
+
klass.connection_pool.shard
|
94
63
|
end
|
95
64
|
|
96
65
|
def activate(shards)
|
97
|
-
|
66
|
+
activated_classes = activate!(shards)
|
98
67
|
yield
|
99
68
|
ensure
|
100
|
-
|
69
|
+
activated_classes.each do |klass|
|
70
|
+
klass.connection_pool.shard_stack.pop
|
71
|
+
klass.connected_to_stack.pop
|
72
|
+
end
|
101
73
|
end
|
102
74
|
|
103
75
|
def activate!(shards)
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
unless
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
76
|
+
activated_classes = []
|
77
|
+
shards.each do |klass, shard|
|
78
|
+
next if klass == UnshardedRecord
|
79
|
+
|
80
|
+
next unless klass.current_shard != shard.database_server.id.to_sym ||
|
81
|
+
klass.connection_pool.shard != shard
|
82
|
+
|
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
|
113
86
|
end
|
114
|
-
|
87
|
+
activated_classes
|
115
88
|
end
|
116
89
|
|
117
90
|
def lookup(id)
|
118
91
|
id_i = id.to_i
|
119
92
|
return current if id_i == current.id || id == 'self'
|
120
93
|
return default if id_i == default.id || id.nil? || id == 'default'
|
94
|
+
|
121
95
|
id = id_i
|
122
|
-
raise ArgumentError if id
|
96
|
+
raise ArgumentError if id.zero?
|
123
97
|
|
124
|
-
unless cached_shards.
|
98
|
+
unless cached_shards.key?(id)
|
125
99
|
cached_shards[id] = Shard.default.activate do
|
126
100
|
find_cached(['shard', id]) { find_by(id: id) }
|
127
101
|
end
|
@@ -129,11 +103,6 @@ module Switchman
|
|
129
103
|
cached_shards[id]
|
130
104
|
end
|
131
105
|
|
132
|
-
def preload_cache
|
133
|
-
cached_shards.reverse_merge!(active_shards.values.index_by(&:id))
|
134
|
-
cached_shards.reverse_merge!(all.index_by(&:id))
|
135
|
-
end
|
136
|
-
|
137
106
|
def clear_cache
|
138
107
|
cached_shards.clear
|
139
108
|
end
|
@@ -141,63 +110,77 @@ module Switchman
|
|
141
110
|
# ==== Parameters
|
142
111
|
#
|
143
112
|
# * +shards+ - an array or relation of Shards to iterate over
|
144
|
-
# * +
|
145
|
-
#
|
146
|
-
#
|
147
|
-
#
|
148
|
-
#
|
149
|
-
#
|
150
|
-
# :
|
113
|
+
# * +classes+ - an array of classes to activate
|
114
|
+
# parallel: - true/false to execute in parallel, or a integer of how many
|
115
|
+
# sub-processes per database server. Note that parallel
|
116
|
+
# invocation currently uses forking, so should be used sparingly
|
117
|
+
# because errors are not raised, and you cannot get results back
|
118
|
+
# max_procs: - only run this many parallel processes at a time
|
119
|
+
# exception: - :ignore, :raise, :defer (wait until the end and raise the first
|
151
120
|
# error), or a proc
|
152
|
-
def with_each_shard(*args)
|
153
|
-
raise ArgumentError, "wrong number of arguments (#{args.length} for 0...
|
121
|
+
def with_each_shard(*args, parallel: false, max_procs: nil, exception: :raise, &block)
|
122
|
+
raise ArgumentError, "wrong number of arguments (#{args.length} for 0...2)" if args.length > 2
|
154
123
|
|
155
|
-
unless default.is_a?(Shard)
|
156
|
-
return Array.wrap(yield)
|
157
|
-
end
|
124
|
+
return Array.wrap(yield) unless default.is_a?(Shard)
|
158
125
|
|
159
|
-
options = args.extract_options!
|
160
126
|
if args.length == 1
|
161
|
-
if Array === args.first && args.first.first.is_a?(
|
162
|
-
|
127
|
+
if Array === args.first && args.first.first.is_a?(Class)
|
128
|
+
classes = args.first
|
163
129
|
else
|
164
130
|
scope = args.first
|
165
131
|
end
|
166
132
|
else
|
167
|
-
scope,
|
133
|
+
scope, classes = args
|
168
134
|
end
|
169
135
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
parallel = case options[:parallel]
|
174
|
-
when true
|
175
|
-
[Environment.cpu_count || 2, 2].min
|
176
|
-
when false, nil
|
177
|
-
1
|
178
|
-
else
|
179
|
-
options[:parallel]
|
180
|
-
end
|
181
|
-
options.delete(:parallel)
|
136
|
+
parallel = 1 if parallel == true
|
137
|
+
parallel = 0 if parallel == false || parallel.nil?
|
182
138
|
|
183
139
|
scope ||= Shard.all
|
184
|
-
if ::ActiveRecord::Relation === scope && scope.order_values.empty?
|
185
|
-
scope = scope.order(::Arel.sql("database_server_id IS NOT NULL, database_server_id, id"))
|
186
|
-
end
|
140
|
+
scope = scope.order(::Arel.sql('database_server_id IS NOT NULL, database_server_id, id')) if ::ActiveRecord::Relation === scope && scope.order_values.empty?
|
187
141
|
|
188
|
-
if parallel
|
142
|
+
if parallel.positive?
|
143
|
+
max_procs = determine_max_procs(max_procs, parallel)
|
189
144
|
if ::ActiveRecord::Relation === scope
|
190
145
|
# still need a post-uniq, cause the default database server could be NULL or Rails.env in the db
|
191
146
|
database_servers = scope.reorder('database_server_id').select(:database_server_id).distinct.
|
192
|
-
|
147
|
+
map(&:database_server).compact.uniq
|
193
148
|
# nothing to do
|
194
|
-
return if database_servers.count
|
149
|
+
return if database_servers.count.zero?
|
195
150
|
|
196
|
-
|
197
|
-
|
198
|
-
|
151
|
+
parallel = [(max_procs.to_f / database_servers.count).ceil, parallel].min if max_procs
|
152
|
+
|
153
|
+
scopes = database_servers.map do |server|
|
154
|
+
server_scope = server.shards.merge(scope)
|
155
|
+
if parallel == 1
|
156
|
+
subscopes = [server_scope]
|
157
|
+
else
|
158
|
+
subscopes = []
|
159
|
+
total = server_scope.count
|
160
|
+
ranges = []
|
161
|
+
server_scope.find_ids_in_ranges(batch_size: (total.to_f / parallel).ceil) do |min, max|
|
162
|
+
ranges << [min, max]
|
163
|
+
end
|
164
|
+
# create a half-open range on the last one
|
165
|
+
ranges.last[1] = nil
|
166
|
+
ranges.each do |min, max|
|
167
|
+
subscope = server_scope.where('id>=?', min)
|
168
|
+
subscope = subscope.where('id<=?', max) if max
|
169
|
+
subscopes << subscope
|
170
|
+
end
|
171
|
+
end
|
172
|
+
[server, subscopes]
|
173
|
+
end.to_h
|
199
174
|
else
|
200
175
|
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
|
201
184
|
end
|
202
185
|
|
203
186
|
exception_pipes = []
|
@@ -208,8 +191,8 @@ module Switchman
|
|
208
191
|
fd_to_name_map = {}
|
209
192
|
errors = []
|
210
193
|
|
211
|
-
wait_for_output = lambda do
|
212
|
-
ready,
|
194
|
+
wait_for_output = lambda do
|
195
|
+
ready, = IO.select(out_fds + err_fds)
|
213
196
|
ready.each do |fd|
|
214
197
|
if fd.eof?
|
215
198
|
fd.close
|
@@ -223,8 +206,9 @@ module Switchman
|
|
223
206
|
end
|
224
207
|
|
225
208
|
# only one process; don't bother forking
|
226
|
-
if scopes.length == 1
|
227
|
-
return with_each_shard(scopes.first.last,
|
209
|
+
if scopes.length == 1 && parallel == 1
|
210
|
+
return with_each_shard(scopes.first.last.first, classes, exception: exception,
|
211
|
+
&block)
|
228
212
|
end
|
229
213
|
|
230
214
|
# clear connections prior to forking (no more queries will be executed in the parent,
|
@@ -232,13 +216,17 @@ module Switchman
|
|
232
216
|
# silly like dealloc'ing prepared statements)
|
233
217
|
::ActiveRecord::Base.clear_all_connections!
|
234
218
|
|
235
|
-
scopes.each do |server,
|
236
|
-
|
219
|
+
scopes.each do |server, subscopes|
|
220
|
+
subscopes.each_with_index do |subscope, idx|
|
221
|
+
name = if subscopes.length > 1
|
222
|
+
"#{server.id} #{idx + 1}"
|
223
|
+
else
|
224
|
+
server.id
|
225
|
+
end
|
237
226
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
begin
|
227
|
+
exception_pipe = IO.pipe
|
228
|
+
exception_pipes << exception_pipe
|
229
|
+
pid, io_in, io_out, io_err = Open4.pfork4(lambda do
|
242
230
|
Switchman.config[:on_fork_proc]&.call
|
243
231
|
|
244
232
|
# set a pretty name for the process title, up to 128 characters
|
@@ -246,70 +234,55 @@ module Switchman
|
|
246
234
|
# was started)
|
247
235
|
# first, simplify the binary name by stripping directories,
|
248
236
|
# then truncate arguments as necessary
|
249
|
-
bin = File.basename($0)
|
237
|
+
bin = File.basename($0) # Process.argv0 doesn't work on Ruby 2.5 (https://bugs.ruby-lang.org/issues/15887)
|
250
238
|
max_length = 128 - bin.length - name.length - 3
|
251
|
-
args = ARGV.join(
|
252
|
-
if max_length >= 0
|
253
|
-
|
254
|
-
end
|
255
|
-
new_title = [bin, args, name].join(" ")
|
239
|
+
args = ARGV.join(' ')
|
240
|
+
args = args[0..max_length] if max_length >= 0
|
241
|
+
new_title = [bin, args, name].join(' ')
|
256
242
|
Process.setproctitle(new_title)
|
257
243
|
|
258
|
-
with_each_shard(subscope,
|
244
|
+
with_each_shard(subscope, classes, exception: exception, &block)
|
259
245
|
exception_pipe.last.close
|
260
|
-
rescue
|
246
|
+
rescue => e
|
261
247
|
begin
|
262
248
|
dumped = Marshal.dump(e)
|
263
|
-
dumped = nil if dumped.length > 64 * 1024
|
264
249
|
rescue
|
265
|
-
dumped = nil
|
266
|
-
end
|
267
|
-
|
268
|
-
if dumped.nil?
|
269
250
|
# couldn't dump the exception; create a copy with just
|
270
251
|
# the message and the backtrace
|
271
252
|
e2 = e.class.new(e.message)
|
272
|
-
|
273
|
-
# truncate excessively long backtraces
|
274
|
-
if backtrace.length > 50
|
275
|
-
backtrace = backtrace[0...25] + ['...'] + backtrace[-25..-1]
|
276
|
-
end
|
277
|
-
e2.set_backtrace(backtrace)
|
253
|
+
e2.set_backtrace(e.backtrace)
|
278
254
|
e2.instance_variable_set(:@active_shards, e.instance_variable_get(:@active_shards))
|
279
255
|
dumped = Marshal.dump(e2)
|
280
256
|
end
|
281
|
-
|
282
257
|
exception_pipe.last.set_encoding(dumped.encoding)
|
283
258
|
exception_pipe.last.write(dumped)
|
284
259
|
exception_pipe.last.flush
|
285
260
|
exception_pipe.last.close
|
286
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
|
287
281
|
end
|
288
|
-
end)
|
289
|
-
exception_pipe.last.close
|
290
|
-
pids << pid
|
291
|
-
io_in.close # don't care about writing to stdin
|
292
|
-
out_fds << io_out
|
293
|
-
err_fds << io_err
|
294
|
-
pid_to_name_map[pid] = name
|
295
|
-
fd_to_name_map[io_out] = name
|
296
|
-
fd_to_name_map[io_err] = name
|
297
|
-
|
298
|
-
while pids.count >= parallel
|
299
|
-
while out_fds.count >= parallel
|
300
|
-
# wait for output if we've hit the parallel limit
|
301
|
-
wait_for_output.call(out_fds, err_fds, fd_to_name_map)
|
302
|
-
end
|
303
|
-
# we've gotten all the output from one fd so wait for its child process to exit
|
304
|
-
found_pid, status = Process.wait2
|
305
|
-
pids.delete(found_pid)
|
306
|
-
errors << pid_to_name_map[found_pid] if status.exitstatus != 0
|
307
282
|
end
|
308
283
|
end
|
309
284
|
|
310
|
-
while out_fds.any? || err_fds.any?
|
311
|
-
wait_for_output.call(out_fds, err_fds, fd_to_name_map)
|
312
|
-
end
|
285
|
+
wait_for_output.call while out_fds.any? || err_fds.any?
|
313
286
|
pids.each do |pid|
|
314
287
|
_, status = Process.waitpid2(pid)
|
315
288
|
errors << pid_to_name_map[pid] if status.exitstatus != 0
|
@@ -317,67 +290,51 @@ module Switchman
|
|
317
290
|
|
318
291
|
# check for an exception; we only re-raise the first one
|
319
292
|
exception_pipes.each do |exception_pipe|
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
end
|
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
|
328
300
|
end
|
329
301
|
|
330
302
|
unless errors.empty?
|
331
|
-
raise ParallelShardExecError
|
303
|
+
raise ParallelShardExecError,
|
304
|
+
"The following subprocesses did not exit cleanly: #{errors.sort.join(', ')}"
|
332
305
|
end
|
306
|
+
|
333
307
|
return
|
334
308
|
end
|
335
309
|
|
336
|
-
|
310
|
+
classes ||= []
|
337
311
|
|
338
312
|
previous_shard = nil
|
339
|
-
close_connections_if_needed = lambda do |shard|
|
340
|
-
# prune the prior connection unless it happened to be the same
|
341
|
-
if previous_shard && shard != previous_shard && !previous_shard.database_server.shareable?
|
342
|
-
previous_shard.activate do
|
343
|
-
::GuardRail.activated_environments.each do |env|
|
344
|
-
::GuardRail.activate(env) do
|
345
|
-
if ::ActiveRecord::Base.connected? && ::ActiveRecord::Base.connection.open_transactions == 0
|
346
|
-
::ActiveRecord::Base.connection_pool.current_pool.disconnect!
|
347
|
-
end
|
348
|
-
end
|
349
|
-
end
|
350
|
-
end
|
351
|
-
end
|
352
|
-
end
|
353
|
-
|
354
313
|
result = []
|
355
|
-
|
314
|
+
ex = nil
|
356
315
|
scope.each do |shard|
|
357
316
|
# shard references a database server that isn't configured in this environment
|
358
317
|
next unless shard.database_server
|
359
|
-
|
360
|
-
shard.activate(*
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
raise
|
374
|
-
end
|
318
|
+
|
319
|
+
shard.activate(*classes) do
|
320
|
+
result.concat Array.wrap(yield)
|
321
|
+
rescue
|
322
|
+
case exception
|
323
|
+
when :ignore
|
324
|
+
# ignore
|
325
|
+
when :defer
|
326
|
+
ex ||= $!
|
327
|
+
when Proc
|
328
|
+
exception.call
|
329
|
+
# when :raise
|
330
|
+
else
|
331
|
+
raise
|
375
332
|
end
|
376
333
|
end
|
377
334
|
previous_shard = shard
|
378
335
|
end
|
379
|
-
|
380
|
-
|
336
|
+
raise ex if ex
|
337
|
+
|
381
338
|
result
|
382
339
|
end
|
383
340
|
|
@@ -386,22 +343,22 @@ module Switchman
|
|
386
343
|
array.each do |object|
|
387
344
|
partition_object = partition_proc ? partition_proc.call(object) : object
|
388
345
|
case partition_object
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
end
|
397
|
-
next
|
398
|
-
else
|
399
|
-
shard = partition_object.shard
|
346
|
+
when Shard
|
347
|
+
shard = partition_object
|
348
|
+
when ::ActiveRecord::Base
|
349
|
+
if partition_object.respond_to?(:associated_shards)
|
350
|
+
partition_object.associated_shards.each do |a_shard|
|
351
|
+
shard_arrays[a_shard] ||= []
|
352
|
+
shard_arrays[a_shard] << object
|
400
353
|
end
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
354
|
+
next
|
355
|
+
else
|
356
|
+
shard = partition_object.shard
|
357
|
+
end
|
358
|
+
when Integer, /^\d+$/, /^(\d+)~(\d+)$/
|
359
|
+
local_id, shard = Shard.local_id_for(partition_object)
|
360
|
+
local_id ||= partition_object
|
361
|
+
object = local_id unless partition_proc
|
405
362
|
end
|
406
363
|
shard ||= Shard.current
|
407
364
|
shard_arrays[shard] ||= []
|
@@ -410,7 +367,7 @@ module Switchman
|
|
410
367
|
# TODO: use with_each_shard (or vice versa) to get
|
411
368
|
# connection management and parallelism benefits
|
412
369
|
shard_arrays.inject([]) do |results, (shard, objects)|
|
413
|
-
results.concat
|
370
|
+
results.concat(shard.activate { Array.wrap(yield objects) })
|
414
371
|
end
|
415
372
|
end
|
416
373
|
|
@@ -422,7 +379,7 @@ module Switchman
|
|
422
379
|
# stay as provided. This assumes no consumer
|
423
380
|
# will return a nil value from the block.
|
424
381
|
def signed_id_operation(input_id)
|
425
|
-
sign = input_id
|
382
|
+
sign = input_id.negative? ? -1 : 1
|
426
383
|
output = yield input_id.abs
|
427
384
|
output * sign
|
428
385
|
end
|
@@ -430,9 +387,10 @@ module Switchman
|
|
430
387
|
# converts an AR object, integral id, string id, or string short-global-id to a
|
431
388
|
# integral id. nil if it can't be interpreted
|
432
389
|
def integral_id_for(any_id)
|
433
|
-
|
434
|
-
|
435
|
-
|
390
|
+
case any_id
|
391
|
+
when ::Arel::Nodes::Casted
|
392
|
+
any_id = any_id.value
|
393
|
+
when ::Arel::Nodes::BindParam
|
436
394
|
any_id = any_id.value.value_before_type_cast
|
437
395
|
end
|
438
396
|
|
@@ -443,12 +401,11 @@ module Switchman
|
|
443
401
|
local_id = $2.to_i
|
444
402
|
signed_id_operation(local_id) do |id|
|
445
403
|
return nil if id > IDS_PER_SHARD
|
404
|
+
|
446
405
|
$1.to_i * IDS_PER_SHARD + id
|
447
406
|
end
|
448
407
|
when Integer, /^-?\d+$/
|
449
408
|
any_id.to_i
|
450
|
-
else
|
451
|
-
nil
|
452
409
|
end
|
453
410
|
end
|
454
411
|
|
@@ -460,11 +417,12 @@ module Switchman
|
|
460
417
|
def local_id_for(any_id)
|
461
418
|
id = integral_id_for(any_id)
|
462
419
|
return NIL_NIL_ID unless id
|
420
|
+
|
463
421
|
return_shard = nil
|
464
422
|
local_id = signed_id_operation(id) do |abs_id|
|
465
423
|
if abs_id < IDS_PER_SHARD
|
466
424
|
abs_id
|
467
|
-
elsif return_shard = lookup(abs_id / IDS_PER_SHARD)
|
425
|
+
elsif (return_shard = lookup(abs_id / IDS_PER_SHARD))
|
468
426
|
abs_id % IDS_PER_SHARD
|
469
427
|
else
|
470
428
|
return NIL_NIL_ID
|
@@ -481,8 +439,10 @@ module Switchman
|
|
481
439
|
integral_id = integral_id_for(any_id)
|
482
440
|
local_id, shard = local_id_for(integral_id)
|
483
441
|
return integral_id unless local_id
|
442
|
+
|
484
443
|
shard ||= source_shard
|
485
444
|
return local_id if shard == target_shard
|
445
|
+
|
486
446
|
shard.global_id_for(local_id)
|
487
447
|
end
|
488
448
|
|
@@ -493,6 +453,7 @@ module Switchman
|
|
493
453
|
local_id, shard = local_id_for(any_id)
|
494
454
|
return any_id unless local_id
|
495
455
|
return local_id unless shard
|
456
|
+
|
496
457
|
"#{shard.id}~#{local_id}"
|
497
458
|
end
|
498
459
|
|
@@ -501,6 +462,7 @@ module Switchman
|
|
501
462
|
def global_id_for(any_id, source_shard = nil)
|
502
463
|
id = integral_id_for(any_id)
|
503
464
|
return any_id unless id
|
465
|
+
|
504
466
|
signed_id_operation(id) do |abs_id|
|
505
467
|
if abs_id >= IDS_PER_SHARD
|
506
468
|
abs_id
|
@@ -513,11 +475,59 @@ module Switchman
|
|
513
475
|
|
514
476
|
def shard_for(any_id, source_shard = nil)
|
515
477
|
return any_id.shard if any_id.is_a?(::ActiveRecord::Base)
|
478
|
+
|
516
479
|
_, shard = local_id_for(any_id)
|
517
480
|
shard || source_shard || Shard.current
|
518
481
|
end
|
519
482
|
|
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
|
+
|
520
500
|
private
|
501
|
+
|
502
|
+
def add_sharded_model(klass)
|
503
|
+
@sharded_models = (sharded_models + [klass]).freeze
|
504
|
+
initialize_sharding
|
505
|
+
end
|
506
|
+
|
507
|
+
def initialize_sharding
|
508
|
+
full_connects_to_hash = DatabaseServer.all.map { |db| [db.id.to_sym, db.connects_to_hash] }.to_h
|
509
|
+
sharded_models.each do |klass|
|
510
|
+
connects_to_hash = full_connects_to_hash.deep_dup
|
511
|
+
if klass == UnshardedRecord
|
512
|
+
# no need to mention other databases for the unsharded category
|
513
|
+
connects_to_hash = { ::Rails.env.to_sym => DatabaseServer.find(nil).connects_to_hash }
|
514
|
+
end
|
515
|
+
|
516
|
+
# prune things we're already connected to
|
517
|
+
if klass.connection_specification_name == klass.name
|
518
|
+
connects_to_hash.each do |(db_name, role_hash)|
|
519
|
+
role_hash.each_key do |role|
|
520
|
+
role_hash.delete(role) if klass.connection_handler.retrieve_connection_pool(
|
521
|
+
klass.connection_specification_name, role: role, shard: db_name
|
522
|
+
)
|
523
|
+
end
|
524
|
+
end
|
525
|
+
end
|
526
|
+
|
527
|
+
klass.connects_to shards: connects_to_hash
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
521
531
|
# in-process caching
|
522
532
|
def cached_shards
|
523
533
|
@cached_shards ||= {}.compare_by_identity
|
@@ -548,10 +558,6 @@ module Switchman
|
|
548
558
|
shard = nil unless shard.database_server
|
549
559
|
shard
|
550
560
|
end
|
551
|
-
|
552
|
-
def active_shards
|
553
|
-
Thread.current[:active_shards] ||= {}.compare_by_identity
|
554
|
-
end
|
555
561
|
end
|
556
562
|
|
557
563
|
def name
|
@@ -565,11 +571,11 @@ module Switchman
|
|
565
571
|
|
566
572
|
def name=(name)
|
567
573
|
write_attribute(:name, @name = name)
|
568
|
-
remove_instance_variable(:@name) if name
|
574
|
+
remove_instance_variable(:@name) if name.nil?
|
569
575
|
end
|
570
576
|
|
571
577
|
def database_server
|
572
|
-
@database_server ||= DatabaseServer.find(
|
578
|
+
@database_server ||= DatabaseServer.find(database_server_id)
|
573
579
|
end
|
574
580
|
|
575
581
|
def database_server=(database_server)
|
@@ -590,23 +596,21 @@ module Switchman
|
|
590
596
|
Shard.default
|
591
597
|
end
|
592
598
|
|
593
|
-
def activate(*
|
594
|
-
shards =
|
595
|
-
Shard.activate(shards)
|
596
|
-
yield
|
597
|
-
end
|
599
|
+
def activate(*classes, &block)
|
600
|
+
shards = hashify_classes(classes)
|
601
|
+
Shard.activate(shards, &block)
|
598
602
|
end
|
599
603
|
|
600
604
|
# for use from console ONLY
|
601
|
-
def activate!(*
|
602
|
-
shards =
|
605
|
+
def activate!(*classes)
|
606
|
+
shards = hashify_classes(classes)
|
603
607
|
Shard.activate!(shards)
|
604
608
|
nil
|
605
609
|
end
|
606
610
|
|
607
611
|
# custom serialization, since shard is self-referential
|
608
|
-
def _dump(
|
609
|
-
|
612
|
+
def _dump(_depth)
|
613
|
+
id.to_s
|
610
614
|
end
|
611
615
|
|
612
616
|
def self._load(str)
|
@@ -614,45 +618,45 @@ module Switchman
|
|
614
618
|
end
|
615
619
|
|
616
620
|
def drop_database
|
617
|
-
raise(
|
621
|
+
raise('Cannot drop the database of the default shard') if default?
|
618
622
|
return unless read_attribute(:name)
|
619
623
|
|
620
624
|
begin
|
621
|
-
adapter =
|
625
|
+
adapter = database_server.config[:adapter]
|
622
626
|
sharding_config = Switchman.config || {}
|
623
627
|
drop_statement = sharding_config[adapter]&.[](:drop_statement)
|
624
628
|
drop_statement ||= sharding_config[:drop_statement]
|
625
629
|
if drop_statement
|
626
630
|
drop_statement = Array(drop_statement).dup.
|
627
|
-
|
631
|
+
map { |statement| statement.gsub('%{name}', name) }
|
628
632
|
end
|
629
633
|
|
630
634
|
case adapter
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
end
|
635
|
+
when 'mysql', 'mysql2'
|
636
|
+
activate do
|
637
|
+
::GuardRail.activate(:deploy) do
|
638
|
+
drop_statement ||= "DROP DATABASE #{name}"
|
639
|
+
Array(drop_statement).each do |stmt|
|
640
|
+
::ActiveRecord::Base.connection.execute(stmt)
|
638
641
|
end
|
639
642
|
end
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
ensure
|
652
|
-
conn.raw_connection.set_notice_processor(&old_proc) if old_proc
|
643
|
+
end
|
644
|
+
when 'postgresql'
|
645
|
+
activate do
|
646
|
+
::GuardRail.activate(:deploy) do
|
647
|
+
# Shut up, Postgres!
|
648
|
+
conn = ::ActiveRecord::Base.connection
|
649
|
+
old_proc = conn.raw_connection.set_notice_processor {}
|
650
|
+
begin
|
651
|
+
drop_statement ||= "DROP SCHEMA #{name} CASCADE"
|
652
|
+
Array(drop_statement).each do |stmt|
|
653
|
+
::ActiveRecord::Base.connection.execute(stmt)
|
653
654
|
end
|
655
|
+
ensure
|
656
|
+
conn.raw_connection.set_notice_processor(&old_proc) if old_proc
|
654
657
|
end
|
655
658
|
end
|
659
|
+
end
|
656
660
|
end
|
657
661
|
rescue
|
658
662
|
logger.info "Drop failed: #{$!}"
|
@@ -662,8 +666,9 @@ module Switchman
|
|
662
666
|
# takes an id local to this shard, and returns a global id
|
663
667
|
def global_id_for(local_id)
|
664
668
|
return nil unless local_id
|
669
|
+
|
665
670
|
self.class.signed_id_operation(local_id) do |abs_id|
|
666
|
-
abs_id +
|
671
|
+
abs_id + id * IDS_PER_SHARD
|
667
672
|
end
|
668
673
|
end
|
669
674
|
|
@@ -673,7 +678,8 @@ module Switchman
|
|
673
678
|
end
|
674
679
|
|
675
680
|
def destroy
|
676
|
-
raise(
|
681
|
+
raise('Cannot destroy the default shard') if default?
|
682
|
+
|
677
683
|
super
|
678
684
|
end
|
679
685
|
|
@@ -682,31 +688,22 @@ module Switchman
|
|
682
688
|
def clear_cache
|
683
689
|
Shard.default.activate do
|
684
690
|
Switchman.cache.delete(['shard', id].join('/'))
|
685
|
-
Switchman.cache.delete(
|
691
|
+
Switchman.cache.delete('default_shard') if default?
|
686
692
|
end
|
687
|
-
self.class.clear_cache
|
688
693
|
end
|
689
694
|
|
690
695
|
def default_name
|
691
696
|
database_server.shard_name(self)
|
692
697
|
end
|
693
698
|
|
694
|
-
def
|
695
|
-
|
696
|
-
|
697
|
-
next unless pool.is_a?(ConnectionPoolProxy)
|
698
|
-
|
699
|
-
pool.remove_shard!(self)
|
700
|
-
end
|
701
|
-
end
|
702
|
-
|
703
|
-
def hashify_categories(categories)
|
704
|
-
if categories.empty?
|
705
|
-
{ :primary => self }
|
699
|
+
def hashify_classes(classes)
|
700
|
+
if classes.empty?
|
701
|
+
{ ::ActiveRecord::Base => self }
|
706
702
|
else
|
707
|
-
|
703
|
+
classes.each_with_object({}) do |klass, h|
|
704
|
+
h[klass] = self
|
705
|
+
end
|
708
706
|
end
|
709
707
|
end
|
710
|
-
|
711
708
|
end
|
712
709
|
end
|