switchman 2.1.0 → 3.0.6
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 +270 -343
- 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 +8 -6
- 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 +0 -8
- data/lib/switchman/active_record/association.rb +78 -89
- data/lib/switchman/active_record/attribute_methods.rb +127 -52
- data/lib/switchman/active_record/base.rb +83 -67
- data/lib/switchman/active_record/calculations.rb +73 -66
- data/lib/switchman/active_record/connection_pool.rb +12 -59
- 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 +19 -45
- data/lib/switchman/active_record/model_schema.rb +1 -1
- data/lib/switchman/active_record/persistence.rb +11 -6
- data/lib/switchman/active_record/postgresql_adapter.rb +33 -161
- data/lib/switchman/active_record/predicate_builder.rb +1 -1
- data/lib/switchman/active_record/query_cache.rb +18 -19
- data/lib/switchman/active_record/query_methods.rb +178 -193
- data/lib/switchman/active_record/reflection.rb +7 -22
- data/lib/switchman/active_record/relation.rb +32 -29
- 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_record/test_fixtures.rb +43 -0
- data/lib/switchman/active_support/cache.rb +3 -5
- data/lib/switchman/arel.rb +13 -8
- data/lib/switchman/database_server.rb +130 -154
- data/lib/switchman/default_shard.rb +52 -16
- data/lib/switchman/engine.rb +65 -58
- 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 -57
- data/lib/switchman/rails.rb +14 -12
- data/lib/switchman/sharded_instrumenter.rb +1 -1
- data/lib/switchman/standard_error.rb +15 -3
- data/lib/switchman/test_helper.rb +5 -3
- data/lib/switchman/version.rb +1 -1
- data/lib/switchman.rb +3 -3
- data/lib/tasks/switchman.rake +61 -72
- 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
- data/lib/switchman/schema_cache.rb +0 -28
@@ -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,77 @@ 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
|
-
# Now find the actual record, if it exists
|
42
|
+
# Now find the actual record, if it exists
|
65
43
|
@default = begin
|
66
|
-
find_cached(
|
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
|
67
49
|
rescue
|
68
50
|
default
|
69
51
|
end
|
70
|
-
|
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?
|
52
|
+
return default unless @default
|
78
53
|
|
79
54
|
# 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
|
55
|
+
@default.database_server.remove_instance_variable(:@primary_shard) if @default.database_server.instance_variable_defined?(:@primary_shard)
|
83
56
|
|
84
57
|
# and finally, check for cached references to the default shard on the existing connection
|
85
|
-
|
86
|
-
|
58
|
+
sharded_models.each do |klass|
|
59
|
+
klass.connection.shard = @default if klass.connected? && klass.connection.shard.default?
|
87
60
|
end
|
88
61
|
end
|
89
62
|
@default
|
90
63
|
end
|
91
64
|
|
92
|
-
def current(
|
93
|
-
|
65
|
+
def current(klass = ::ActiveRecord::Base)
|
66
|
+
klass ||= ::ActiveRecord::Base
|
67
|
+
klass.current_switchman_shard
|
94
68
|
end
|
95
69
|
|
96
70
|
def activate(shards)
|
97
|
-
|
71
|
+
activated_classes = activate!(shards)
|
98
72
|
yield
|
99
73
|
ensure
|
100
|
-
|
74
|
+
activated_classes&.each do |klass|
|
75
|
+
klass.connected_to_stack.pop
|
76
|
+
end
|
101
77
|
end
|
102
78
|
|
103
79
|
def activate!(shards)
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
unless
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
80
|
+
activated_classes = nil
|
81
|
+
shards.each do |klass, shard|
|
82
|
+
next if klass == UnshardedRecord
|
83
|
+
|
84
|
+
next unless klass.current_shard != shard.database_server.id.to_sym ||
|
85
|
+
klass.current_switchman_shard != shard
|
86
|
+
|
87
|
+
(activated_classes ||= []) << klass
|
88
|
+
klass.connected_to_stack << { shard: shard.database_server.id.to_sym, klasses: [klass], switchman_shard: shard }
|
113
89
|
end
|
114
|
-
|
90
|
+
activated_classes
|
91
|
+
end
|
92
|
+
|
93
|
+
def active_shards
|
94
|
+
sharded_models.map do |klass|
|
95
|
+
[klass, current(klass)]
|
96
|
+
end.compact.to_h
|
115
97
|
end
|
116
98
|
|
117
99
|
def lookup(id)
|
118
100
|
id_i = id.to_i
|
119
101
|
return current if id_i == current.id || id == 'self'
|
120
102
|
return default if id_i == default.id || id.nil? || id == 'default'
|
103
|
+
|
121
104
|
id = id_i
|
122
|
-
raise ArgumentError if id
|
105
|
+
raise ArgumentError if id.zero?
|
123
106
|
|
124
|
-
unless cached_shards.
|
107
|
+
unless cached_shards.key?(id)
|
125
108
|
cached_shards[id] = Shard.default.activate do
|
126
109
|
find_cached(['shard', id]) { find_by(id: id) }
|
127
110
|
end
|
@@ -129,6 +112,11 @@ module Switchman
|
|
129
112
|
cached_shards[id]
|
130
113
|
end
|
131
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
|
+
|
132
120
|
def clear_cache
|
133
121
|
cached_shards.clear
|
134
122
|
end
|
@@ -136,89 +124,47 @@ module Switchman
|
|
136
124
|
# ==== Parameters
|
137
125
|
#
|
138
126
|
# * +shards+ - an array or relation of Shards to iterate over
|
139
|
-
# * +
|
140
|
-
#
|
141
|
-
#
|
142
|
-
#
|
143
|
-
#
|
144
|
-
#
|
145
|
-
# :max_procs - only run this many parallel processes at a time
|
146
|
-
# :exception - :ignore, :raise, :defer (wait until the end and raise the first
|
127
|
+
# * +classes+ - an array of classes to activate
|
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
|
132
|
+
# exception: - :ignore, :raise, :defer (wait until the end and raise the first
|
147
133
|
# error), or a proc
|
148
|
-
def with_each_shard(*args)
|
149
|
-
raise ArgumentError, "wrong number of arguments (#{args.length} for 0...
|
134
|
+
def with_each_shard(*args, parallel: false, exception: :raise, &block)
|
135
|
+
raise ArgumentError, "wrong number of arguments (#{args.length} for 0...2)" if args.length > 2
|
150
136
|
|
151
|
-
unless default.is_a?(Shard)
|
152
|
-
return Array.wrap(yield)
|
153
|
-
end
|
137
|
+
return Array.wrap(yield) unless default.is_a?(Shard)
|
154
138
|
|
155
|
-
options = args.extract_options!
|
156
139
|
if args.length == 1
|
157
|
-
if Array === args.first && args.first.first.is_a?(
|
158
|
-
|
140
|
+
if Array === args.first && args.first.first.is_a?(Class)
|
141
|
+
classes = args.first
|
159
142
|
else
|
160
143
|
scope = args.first
|
161
144
|
end
|
162
145
|
else
|
163
|
-
scope,
|
146
|
+
scope, classes = args
|
164
147
|
end
|
165
148
|
|
166
|
-
parallel =
|
167
|
-
|
168
|
-
1
|
169
|
-
when false, nil
|
170
|
-
0
|
171
|
-
else
|
172
|
-
options[:parallel]
|
173
|
-
end
|
174
|
-
options.delete(:parallel)
|
149
|
+
parallel = [Environment.cpu_count || 2, 2].min if parallel == true
|
150
|
+
parallel = 0 if parallel == false || parallel.nil?
|
175
151
|
|
176
152
|
scope ||= Shard.all
|
177
|
-
if ::ActiveRecord::Relation === scope && scope.order_values.empty?
|
178
|
-
scope = scope.order(::Arel.sql("database_server_id IS NOT NULL, database_server_id, id"))
|
179
|
-
end
|
153
|
+
scope = scope.order(::Arel.sql('database_server_id IS NOT NULL, database_server_id, id')) if ::ActiveRecord::Relation === scope && scope.order_values.empty?
|
180
154
|
|
181
|
-
if parallel >
|
182
|
-
max_procs = determine_max_procs(options.delete(:max_procs), parallel)
|
155
|
+
if parallel > 1
|
183
156
|
if ::ActiveRecord::Relation === scope
|
184
157
|
# still need a post-uniq, cause the default database server could be NULL or Rails.env in the db
|
185
158
|
database_servers = scope.reorder('database_server_id').select(:database_server_id).distinct.
|
186
|
-
|
159
|
+
map(&:database_server).compact.uniq
|
187
160
|
# nothing to do
|
188
|
-
return if database_servers.count
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
if parallel == 1
|
194
|
-
subscopes = [server_scope]
|
195
|
-
else
|
196
|
-
subscopes = []
|
197
|
-
total = server_scope.count
|
198
|
-
ranges = []
|
199
|
-
server_scope.find_ids_in_ranges(:batch_size => (total.to_f / parallel).ceil) do |min, max|
|
200
|
-
ranges << [min, max]
|
201
|
-
end
|
202
|
-
# create a half-open range on the last one
|
203
|
-
ranges.last[1] = nil
|
204
|
-
ranges.each do |min, max|
|
205
|
-
subscope = server_scope.where("id>=?", min)
|
206
|
-
subscope = subscope.where("id<=?", max) if max
|
207
|
-
subscopes << subscope
|
208
|
-
end
|
209
|
-
end
|
210
|
-
[server, subscopes]
|
211
|
-
end]
|
161
|
+
return if database_servers.count.zero?
|
162
|
+
|
163
|
+
scopes = database_servers.map do |server|
|
164
|
+
[server, server.shards.merge(scope)]
|
165
|
+
end.to_h
|
212
166
|
else
|
213
167
|
scopes = scope.group_by(&:database_server)
|
214
|
-
if parallel > 1
|
215
|
-
parallel = [(max_procs.to_f / scopes.count).ceil, parallel].min if max_procs
|
216
|
-
scopes = Hash[scopes.map do |(server, shards)|
|
217
|
-
[server, shards.in_groups(parallel, false).compact]
|
218
|
-
end]
|
219
|
-
else
|
220
|
-
scopes = Hash[scopes.map { |(server, shards)| [server, [shards]] }]
|
221
|
-
end
|
222
168
|
end
|
223
169
|
|
224
170
|
exception_pipes = []
|
@@ -229,8 +175,8 @@ module Switchman
|
|
229
175
|
fd_to_name_map = {}
|
230
176
|
errors = []
|
231
177
|
|
232
|
-
wait_for_output = lambda do
|
233
|
-
ready,
|
178
|
+
wait_for_output = lambda do
|
179
|
+
ready, = IO.select(out_fds + err_fds)
|
234
180
|
ready.each do |fd|
|
235
181
|
if fd.eof?
|
236
182
|
fd.close
|
@@ -244,99 +190,86 @@ module Switchman
|
|
244
190
|
end
|
245
191
|
|
246
192
|
# only one process; don't bother forking
|
247
|
-
|
248
|
-
return with_each_shard(scopes.first.last.first, categories, options) { yield }
|
249
|
-
end
|
193
|
+
return with_each_shard(scopes.first.last, classes, exception: exception, &block) if scopes.length == 1
|
250
194
|
|
251
195
|
# clear connections prior to forking (no more queries will be executed in the parent,
|
252
196
|
# and we want them gone so that we don't accidentally use them post-fork doing something
|
253
197
|
# silly like dealloc'ing prepared statements)
|
254
198
|
::ActiveRecord::Base.clear_all_connections!
|
255
199
|
|
256
|
-
scopes.each do |server,
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
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)
|
221
|
+
exception_pipe.last.close
|
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
|
262
228
|
end
|
263
229
|
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
if max_length >= 0
|
279
|
-
args = args[0..max_length]
|
280
|
-
end
|
281
|
-
new_title = [bin, args, name].join(" ")
|
282
|
-
Process.setproctitle(new_title)
|
283
|
-
|
284
|
-
with_each_shard(subscope, categories, options) { yield }
|
285
|
-
exception_pipe.last.close
|
286
|
-
rescue Exception => e
|
287
|
-
begin
|
288
|
-
dumped = Marshal.dump(e)
|
289
|
-
dumped = nil if dumped.length > 64 * 1024
|
290
|
-
rescue
|
291
|
-
dumped = nil
|
292
|
-
end
|
293
|
-
|
294
|
-
if dumped.nil?
|
295
|
-
# couldn't dump the exception; create a copy with just
|
296
|
-
# the message and the backtrace
|
297
|
-
e2 = e.class.new(e.message)
|
298
|
-
backtrace = e.backtrace
|
299
|
-
# truncate excessively long backtraces
|
300
|
-
if backtrace.length > 50
|
301
|
-
backtrace = backtrace[0...25] + ['...'] + backtrace[-25..-1]
|
302
|
-
end
|
303
|
-
e2.set_backtrace(backtrace)
|
304
|
-
e2.instance_variable_set(:@active_shards, e.instance_variable_get(:@active_shards))
|
305
|
-
dumped = Marshal.dump(e2)
|
306
|
-
end
|
307
|
-
|
308
|
-
exception_pipe.last.set_encoding(dumped.encoding)
|
309
|
-
exception_pipe.last.write(dumped)
|
310
|
-
exception_pipe.last.flush
|
311
|
-
exception_pipe.last.close
|
312
|
-
exit! 1
|
313
|
-
end
|
314
|
-
end)
|
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)
|
240
|
+
end
|
241
|
+
exception_pipe.last.set_encoding(dumped.encoding)
|
242
|
+
exception_pipe.last.write(dumped)
|
243
|
+
exception_pipe.last.flush
|
315
244
|
exception_pipe.last.close
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
#
|
330
|
-
|
331
|
-
pids.delete(found_pid)
|
332
|
-
errors << pid_to_name_map[found_pid] if status.exitstatus != 0
|
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
|
333
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
|
334
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
|
335
270
|
end
|
336
271
|
|
337
|
-
while out_fds.any? || err_fds.any?
|
338
|
-
wait_for_output.call(out_fds, err_fds, fd_to_name_map)
|
339
|
-
end
|
272
|
+
wait_for_output.call while out_fds.any? || err_fds.any?
|
340
273
|
pids.each do |pid|
|
341
274
|
_, status = Process.waitpid2(pid)
|
342
275
|
errors << pid_to_name_map[pid] if status.exitstatus != 0
|
@@ -344,67 +277,51 @@ module Switchman
|
|
344
277
|
|
345
278
|
# check for an exception; we only re-raise the first one
|
346
279
|
exception_pipes.each do |exception_pipe|
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
end
|
280
|
+
serialized_exception = exception_pipe.first.read
|
281
|
+
next if serialized_exception.empty?
|
282
|
+
|
283
|
+
ex = Marshal.load(serialized_exception) # rubocop:disable Security/MarshalLoad
|
284
|
+
raise ex
|
285
|
+
ensure
|
286
|
+
exception_pipe.first.close
|
355
287
|
end
|
356
288
|
|
357
289
|
unless errors.empty?
|
358
|
-
raise ParallelShardExecError
|
290
|
+
raise ParallelShardExecError,
|
291
|
+
"The following subprocesses did not exit cleanly: #{errors.sort.join(', ')}"
|
359
292
|
end
|
293
|
+
|
360
294
|
return
|
361
295
|
end
|
362
296
|
|
363
|
-
|
297
|
+
classes ||= []
|
364
298
|
|
365
299
|
previous_shard = nil
|
366
|
-
close_connections_if_needed = lambda do |shard|
|
367
|
-
# prune the prior connection unless it happened to be the same
|
368
|
-
if previous_shard && shard != previous_shard && !previous_shard.database_server.shareable?
|
369
|
-
previous_shard.activate do
|
370
|
-
::GuardRail.activated_environments.each do |env|
|
371
|
-
::GuardRail.activate(env) do
|
372
|
-
if ::ActiveRecord::Base.connected? && ::ActiveRecord::Base.connection.open_transactions == 0
|
373
|
-
::ActiveRecord::Base.connection_pool.current_pool.disconnect!
|
374
|
-
end
|
375
|
-
end
|
376
|
-
end
|
377
|
-
end
|
378
|
-
end
|
379
|
-
end
|
380
|
-
|
381
300
|
result = []
|
382
|
-
|
301
|
+
ex = nil
|
383
302
|
scope.each do |shard|
|
384
303
|
# shard references a database server that isn't configured in this environment
|
385
304
|
next unless shard.database_server
|
386
|
-
|
387
|
-
shard.activate(*
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
raise
|
401
|
-
end
|
305
|
+
|
306
|
+
shard.activate(*classes) do
|
307
|
+
result.concat Array.wrap(yield)
|
308
|
+
rescue
|
309
|
+
case exception
|
310
|
+
when :ignore
|
311
|
+
# ignore
|
312
|
+
when :defer
|
313
|
+
ex ||= $!
|
314
|
+
when Proc
|
315
|
+
exception.call
|
316
|
+
# when :raise
|
317
|
+
else
|
318
|
+
raise
|
402
319
|
end
|
403
320
|
end
|
404
321
|
previous_shard = shard
|
405
322
|
end
|
406
|
-
|
407
|
-
|
323
|
+
raise ex if ex
|
324
|
+
|
408
325
|
result
|
409
326
|
end
|
410
327
|
|
@@ -413,22 +330,22 @@ module Switchman
|
|
413
330
|
array.each do |object|
|
414
331
|
partition_object = partition_proc ? partition_proc.call(object) : object
|
415
332
|
case partition_object
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
end
|
424
|
-
next
|
425
|
-
else
|
426
|
-
shard = partition_object.shard
|
333
|
+
when Shard
|
334
|
+
shard = partition_object
|
335
|
+
when ::ActiveRecord::Base
|
336
|
+
if partition_object.respond_to?(:associated_shards)
|
337
|
+
partition_object.associated_shards.each do |a_shard|
|
338
|
+
shard_arrays[a_shard] ||= []
|
339
|
+
shard_arrays[a_shard] << object
|
427
340
|
end
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
341
|
+
next
|
342
|
+
else
|
343
|
+
shard = partition_object.shard
|
344
|
+
end
|
345
|
+
when Integer, /^\d+$/, /^(\d+)~(\d+)$/
|
346
|
+
local_id, shard = Shard.local_id_for(partition_object)
|
347
|
+
local_id ||= partition_object
|
348
|
+
object = local_id unless partition_proc
|
432
349
|
end
|
433
350
|
shard ||= Shard.current
|
434
351
|
shard_arrays[shard] ||= []
|
@@ -437,7 +354,7 @@ module Switchman
|
|
437
354
|
# TODO: use with_each_shard (or vice versa) to get
|
438
355
|
# connection management and parallelism benefits
|
439
356
|
shard_arrays.inject([]) do |results, (shard, objects)|
|
440
|
-
results.concat
|
357
|
+
results.concat(shard.activate { Array.wrap(yield objects) })
|
441
358
|
end
|
442
359
|
end
|
443
360
|
|
@@ -449,7 +366,7 @@ module Switchman
|
|
449
366
|
# stay as provided. This assumes no consumer
|
450
367
|
# will return a nil value from the block.
|
451
368
|
def signed_id_operation(input_id)
|
452
|
-
sign = input_id
|
369
|
+
sign = input_id.negative? ? -1 : 1
|
453
370
|
output = yield input_id.abs
|
454
371
|
output * sign
|
455
372
|
end
|
@@ -457,9 +374,10 @@ module Switchman
|
|
457
374
|
# converts an AR object, integral id, string id, or string short-global-id to a
|
458
375
|
# integral id. nil if it can't be interpreted
|
459
376
|
def integral_id_for(any_id)
|
460
|
-
|
461
|
-
|
462
|
-
|
377
|
+
case any_id
|
378
|
+
when ::Arel::Nodes::Casted
|
379
|
+
any_id = any_id.value
|
380
|
+
when ::Arel::Nodes::BindParam
|
463
381
|
any_id = any_id.value.value_before_type_cast
|
464
382
|
end
|
465
383
|
|
@@ -470,12 +388,11 @@ module Switchman
|
|
470
388
|
local_id = $2.to_i
|
471
389
|
signed_id_operation(local_id) do |id|
|
472
390
|
return nil if id > IDS_PER_SHARD
|
473
|
-
|
391
|
+
|
392
|
+
($1.to_i * IDS_PER_SHARD) + id
|
474
393
|
end
|
475
394
|
when Integer, /^-?\d+$/
|
476
395
|
any_id.to_i
|
477
|
-
else
|
478
|
-
nil
|
479
396
|
end
|
480
397
|
end
|
481
398
|
|
@@ -487,11 +404,12 @@ module Switchman
|
|
487
404
|
def local_id_for(any_id)
|
488
405
|
id = integral_id_for(any_id)
|
489
406
|
return NIL_NIL_ID unless id
|
407
|
+
|
490
408
|
return_shard = nil
|
491
409
|
local_id = signed_id_operation(id) do |abs_id|
|
492
410
|
if abs_id < IDS_PER_SHARD
|
493
411
|
abs_id
|
494
|
-
elsif return_shard = lookup(abs_id / IDS_PER_SHARD)
|
412
|
+
elsif (return_shard = lookup(abs_id / IDS_PER_SHARD))
|
495
413
|
abs_id % IDS_PER_SHARD
|
496
414
|
else
|
497
415
|
return NIL_NIL_ID
|
@@ -508,8 +426,10 @@ module Switchman
|
|
508
426
|
integral_id = integral_id_for(any_id)
|
509
427
|
local_id, shard = local_id_for(integral_id)
|
510
428
|
return integral_id unless local_id
|
429
|
+
|
511
430
|
shard ||= source_shard
|
512
431
|
return local_id if shard == target_shard
|
432
|
+
|
513
433
|
shard.global_id_for(local_id)
|
514
434
|
end
|
515
435
|
|
@@ -520,6 +440,7 @@ module Switchman
|
|
520
440
|
local_id, shard = local_id_for(any_id)
|
521
441
|
return any_id unless local_id
|
522
442
|
return local_id unless shard
|
443
|
+
|
523
444
|
"#{shard.id}~#{local_id}"
|
524
445
|
end
|
525
446
|
|
@@ -528,6 +449,7 @@ module Switchman
|
|
528
449
|
def global_id_for(any_id, source_shard = nil)
|
529
450
|
id = integral_id_for(any_id)
|
530
451
|
return any_id unless id
|
452
|
+
|
531
453
|
signed_id_operation(id) do |abs_id|
|
532
454
|
if abs_id >= IDS_PER_SHARD
|
533
455
|
abs_id
|
@@ -540,29 +462,42 @@ module Switchman
|
|
540
462
|
|
541
463
|
def shard_for(any_id, source_shard = nil)
|
542
464
|
return any_id.shard if any_id.is_a?(::ActiveRecord::Base)
|
465
|
+
|
543
466
|
_, shard = local_id_for(any_id)
|
544
467
|
shard || source_shard || Shard.current
|
545
468
|
end
|
546
469
|
|
547
|
-
|
548
|
-
|
549
|
-
def
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
470
|
+
private
|
471
|
+
|
472
|
+
def add_sharded_model(klass)
|
473
|
+
@sharded_models = (sharded_models + [klass]).freeze
|
474
|
+
initialize_sharding
|
475
|
+
end
|
476
|
+
|
477
|
+
def initialize_sharding
|
478
|
+
full_connects_to_hash = DatabaseServer.all.map { |db| [db.id.to_sym, db.connects_to_hash] }.to_h
|
479
|
+
sharded_models.each do |klass|
|
480
|
+
connects_to_hash = full_connects_to_hash.deep_dup
|
481
|
+
if klass == UnshardedRecord
|
482
|
+
# no need to mention other databases for the unsharded category
|
483
|
+
connects_to_hash = { ::Rails.env.to_sym => DatabaseServer.find(nil).connects_to_hash }
|
559
484
|
end
|
560
|
-
end
|
561
485
|
|
562
|
-
|
486
|
+
# prune things we're already connected to
|
487
|
+
if klass.connection_specification_name == klass.name
|
488
|
+
connects_to_hash.each do |(db_name, role_hash)|
|
489
|
+
role_hash.each_key do |role|
|
490
|
+
role_hash.delete(role) if klass.connection_handler.retrieve_connection_pool(
|
491
|
+
klass.connection_specification_name, role: role, shard: db_name
|
492
|
+
)
|
493
|
+
end
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
klass.connects_to shards: connects_to_hash
|
498
|
+
end
|
563
499
|
end
|
564
500
|
|
565
|
-
private
|
566
501
|
# in-process caching
|
567
502
|
def cached_shards
|
568
503
|
@cached_shards ||= {}.compare_by_identity
|
@@ -593,10 +528,6 @@ module Switchman
|
|
593
528
|
shard = nil unless shard.database_server
|
594
529
|
shard
|
595
530
|
end
|
596
|
-
|
597
|
-
def active_shards
|
598
|
-
Thread.current[:active_shards] ||= {}.compare_by_identity
|
599
|
-
end
|
600
531
|
end
|
601
532
|
|
602
533
|
def name
|
@@ -610,11 +541,11 @@ module Switchman
|
|
610
541
|
|
611
542
|
def name=(name)
|
612
543
|
write_attribute(:name, @name = name)
|
613
|
-
remove_instance_variable(:@name) if name
|
544
|
+
remove_instance_variable(:@name) if name.nil?
|
614
545
|
end
|
615
546
|
|
616
547
|
def database_server
|
617
|
-
@database_server ||= DatabaseServer.find(
|
548
|
+
@database_server ||= DatabaseServer.find(database_server_id)
|
618
549
|
end
|
619
550
|
|
620
551
|
def database_server=(database_server)
|
@@ -635,23 +566,25 @@ module Switchman
|
|
635
566
|
Shard.default
|
636
567
|
end
|
637
568
|
|
638
|
-
def
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
569
|
+
def original_id
|
570
|
+
id
|
571
|
+
end
|
572
|
+
|
573
|
+
def activate(*classes, &block)
|
574
|
+
shards = hashify_classes(classes)
|
575
|
+
Shard.activate(shards, &block)
|
643
576
|
end
|
644
577
|
|
645
578
|
# for use from console ONLY
|
646
|
-
def activate!(*
|
647
|
-
shards =
|
579
|
+
def activate!(*classes)
|
580
|
+
shards = hashify_classes(classes)
|
648
581
|
Shard.activate!(shards)
|
649
582
|
nil
|
650
583
|
end
|
651
584
|
|
652
585
|
# custom serialization, since shard is self-referential
|
653
|
-
def _dump(
|
654
|
-
|
586
|
+
def _dump(_depth)
|
587
|
+
id.to_s
|
655
588
|
end
|
656
589
|
|
657
590
|
def self._load(str)
|
@@ -659,45 +592,45 @@ module Switchman
|
|
659
592
|
end
|
660
593
|
|
661
594
|
def drop_database
|
662
|
-
raise(
|
595
|
+
raise('Cannot drop the database of the default shard') if default?
|
663
596
|
return unless read_attribute(:name)
|
664
597
|
|
665
598
|
begin
|
666
|
-
adapter =
|
599
|
+
adapter = database_server.config[:adapter]
|
667
600
|
sharding_config = Switchman.config || {}
|
668
601
|
drop_statement = sharding_config[adapter]&.[](:drop_statement)
|
669
602
|
drop_statement ||= sharding_config[:drop_statement]
|
670
603
|
if drop_statement
|
671
604
|
drop_statement = Array(drop_statement).dup.
|
672
|
-
|
605
|
+
map { |statement| statement.gsub('%{name}', name) }
|
673
606
|
end
|
674
607
|
|
675
608
|
case adapter
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
end
|
609
|
+
when 'mysql', 'mysql2'
|
610
|
+
activate do
|
611
|
+
::GuardRail.activate(:deploy) do
|
612
|
+
drop_statement ||= "DROP DATABASE #{name}"
|
613
|
+
Array(drop_statement).each do |stmt|
|
614
|
+
::ActiveRecord::Base.connection.execute(stmt)
|
683
615
|
end
|
684
616
|
end
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
ensure
|
697
|
-
conn.raw_connection.set_notice_processor(&old_proc) if old_proc
|
617
|
+
end
|
618
|
+
when 'postgresql'
|
619
|
+
activate do
|
620
|
+
::GuardRail.activate(:deploy) do
|
621
|
+
# Shut up, Postgres!
|
622
|
+
conn = ::ActiveRecord::Base.connection
|
623
|
+
old_proc = conn.raw_connection.set_notice_processor {}
|
624
|
+
begin
|
625
|
+
drop_statement ||= "DROP SCHEMA #{name} CASCADE"
|
626
|
+
Array(drop_statement).each do |stmt|
|
627
|
+
::ActiveRecord::Base.connection.execute(stmt)
|
698
628
|
end
|
629
|
+
ensure
|
630
|
+
conn.raw_connection.set_notice_processor(&old_proc) if old_proc
|
699
631
|
end
|
700
632
|
end
|
633
|
+
end
|
701
634
|
end
|
702
635
|
rescue
|
703
636
|
logger.info "Drop failed: #{$!}"
|
@@ -707,8 +640,9 @@ module Switchman
|
|
707
640
|
# takes an id local to this shard, and returns a global id
|
708
641
|
def global_id_for(local_id)
|
709
642
|
return nil unless local_id
|
643
|
+
|
710
644
|
self.class.signed_id_operation(local_id) do |abs_id|
|
711
|
-
abs_id +
|
645
|
+
abs_id + (id * IDS_PER_SHARD)
|
712
646
|
end
|
713
647
|
end
|
714
648
|
|
@@ -718,7 +652,8 @@ module Switchman
|
|
718
652
|
end
|
719
653
|
|
720
654
|
def destroy
|
721
|
-
raise(
|
655
|
+
raise('Cannot destroy the default shard') if default?
|
656
|
+
|
722
657
|
super
|
723
658
|
end
|
724
659
|
|
@@ -727,7 +662,7 @@ module Switchman
|
|
727
662
|
def clear_cache
|
728
663
|
Shard.default.activate do
|
729
664
|
Switchman.cache.delete(['shard', id].join('/'))
|
730
|
-
Switchman.cache.delete(
|
665
|
+
Switchman.cache.delete('default_shard') if default?
|
731
666
|
end
|
732
667
|
self.class.clear_cache
|
733
668
|
end
|
@@ -736,22 +671,14 @@ module Switchman
|
|
736
671
|
database_server.shard_name(self)
|
737
672
|
end
|
738
673
|
|
739
|
-
def
|
740
|
-
|
741
|
-
|
742
|
-
next unless pool.is_a?(ConnectionPoolProxy)
|
743
|
-
|
744
|
-
pool.remove_shard!(self)
|
745
|
-
end
|
746
|
-
end
|
747
|
-
|
748
|
-
def hashify_categories(categories)
|
749
|
-
if categories.empty?
|
750
|
-
{ :primary => self }
|
674
|
+
def hashify_classes(classes)
|
675
|
+
if classes.empty?
|
676
|
+
{ ::ActiveRecord::Base => self }
|
751
677
|
else
|
752
|
-
|
678
|
+
classes.each_with_object({}) do |klass, h|
|
679
|
+
h[klass] = self
|
680
|
+
end
|
753
681
|
end
|
754
682
|
end
|
755
|
-
|
756
683
|
end
|
757
684
|
end
|