switchman 2.1.3 → 2.2.0
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 +83 -128
- data/lib/switchman/active_record/base.rb +11 -11
- data/lib/switchman/active_record/migration.rb +6 -3
- data/lib/switchman/active_record/persistence.rb +10 -1
- data/lib/switchman/active_record/postgresql_adapter.rb +1 -1
- data/lib/switchman/engine.rb +1 -1
- data/lib/switchman/version.rb +1 -1
- data/lib/tasks/switchman.rake +1 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cd39304f0dfae8329f95979c1cb2075d09a73cace1b2befa5f62c3100e863ca2
|
4
|
+
data.tar.gz: 742dd9cdbd63e3124b9ae4a9e0d73b56a071c43011f3ec0cdf43681f4977c116
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5355bcac300f60b1c2d626998ebd97a52871c4beff4bc597d2acc1f0439b7365b1a7478f91ec5a05bd8faa2df545e5444341ac296c826f1ab701372180a0282c
|
7
|
+
data.tar.gz: 54a69a012b6d48b91efd5417a71a536a9da8b02283284c5e9327121dcce0c458a1f065d6357012b9cbeba1dbba0603357a64505e69a16de886b9d34add5d8976
|
@@ -129,6 +129,11 @@ module Switchman
|
|
129
129
|
cached_shards[id]
|
130
130
|
end
|
131
131
|
|
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
|
+
|
132
137
|
def clear_cache
|
133
138
|
cached_shards.clear
|
134
139
|
end
|
@@ -139,10 +144,9 @@ module Switchman
|
|
139
144
|
# * +categories+ - an array of categories to activate
|
140
145
|
# * +options+ -
|
141
146
|
# :parallel - true/false to execute in parallel, or a integer of how many
|
142
|
-
# sub-processes
|
143
|
-
#
|
144
|
-
#
|
145
|
-
# :max_procs - only run this many parallel processes at a time
|
147
|
+
# sub-processes. Note that parallel invocation currently uses
|
148
|
+
# forking, so should be used sparingly because you cannot get
|
149
|
+
# results back
|
146
150
|
# :exception - :ignore, :raise, :defer (wait until the end and raise the first
|
147
151
|
# error), or a proc
|
148
152
|
def with_each_shard(*args)
|
@@ -163,11 +167,14 @@ module Switchman
|
|
163
167
|
scope, categories = args
|
164
168
|
end
|
165
169
|
|
170
|
+
# back-compat
|
171
|
+
options[:parallel] = options.delete(:max_procs) if options.key?(:max_procs)
|
172
|
+
|
166
173
|
parallel = case options[:parallel]
|
167
174
|
when true
|
168
|
-
|
175
|
+
[Environment.cpu_count || 2, 2].min
|
169
176
|
when false, nil
|
170
|
-
|
177
|
+
1
|
171
178
|
else
|
172
179
|
options[:parallel]
|
173
180
|
end
|
@@ -178,47 +185,19 @@ module Switchman
|
|
178
185
|
scope = scope.order(::Arel.sql("database_server_id IS NOT NULL, database_server_id, id"))
|
179
186
|
end
|
180
187
|
|
181
|
-
if parallel >
|
182
|
-
max_procs = determine_max_procs(options.delete(:max_procs), parallel)
|
188
|
+
if parallel > 1
|
183
189
|
if ::ActiveRecord::Relation === scope
|
184
190
|
# still need a post-uniq, cause the default database server could be NULL or Rails.env in the db
|
185
191
|
database_servers = scope.reorder('database_server_id').select(:database_server_id).distinct.
|
186
192
|
map(&:database_server).compact.uniq
|
187
193
|
# nothing to do
|
188
194
|
return if database_servers.count == 0
|
189
|
-
parallel = [(max_procs.to_f / database_servers.count).ceil, parallel].min if max_procs
|
190
195
|
|
191
196
|
scopes = Hash[database_servers.map do |server|
|
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]
|
197
|
+
[server, server.shards.merge(scope)]
|
211
198
|
end]
|
212
199
|
else
|
213
200
|
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
201
|
end
|
223
202
|
|
224
203
|
exception_pipes = []
|
@@ -244,8 +223,8 @@ module Switchman
|
|
244
223
|
end
|
245
224
|
|
246
225
|
# only one process; don't bother forking
|
247
|
-
if scopes.length == 1
|
248
|
-
return with_each_shard(scopes.first.last
|
226
|
+
if scopes.length == 1
|
227
|
+
return with_each_shard(scopes.first.last, categories, options) { yield }
|
249
228
|
end
|
250
229
|
|
251
230
|
# clear connections prior to forking (no more queries will be executed in the parent,
|
@@ -253,84 +232,78 @@ module Switchman
|
|
253
232
|
# silly like dealloc'ing prepared statements)
|
254
233
|
::ActiveRecord::Base.clear_all_connections!
|
255
234
|
|
256
|
-
scopes.each do |server,
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
235
|
+
scopes.each do |server, subscope|
|
236
|
+
name = server.id
|
237
|
+
|
238
|
+
exception_pipe = IO.pipe
|
239
|
+
exception_pipes << exception_pipe
|
240
|
+
pid, io_in, io_out, io_err = Open4.pfork4(lambda do
|
241
|
+
begin
|
242
|
+
Switchman.config[:on_fork_proc]&.call
|
243
|
+
|
244
|
+
# set a pretty name for the process title, up to 128 characters
|
245
|
+
# (we don't actually know the limit, depending on how the process
|
246
|
+
# was started)
|
247
|
+
# first, simplify the binary name by stripping directories,
|
248
|
+
# then truncate arguments as necessary
|
249
|
+
bin = File.basename($0) # Process.argv0 doesn't work on Ruby 2.5 (https://bugs.ruby-lang.org/issues/15887)
|
250
|
+
max_length = 128 - bin.length - name.length - 3
|
251
|
+
args = ARGV.join(" ")
|
252
|
+
if max_length >= 0
|
253
|
+
args = args[0..max_length]
|
254
|
+
end
|
255
|
+
new_title = [bin, args, name].join(" ")
|
256
|
+
Process.setproctitle(new_title)
|
263
257
|
|
264
|
-
|
265
|
-
|
266
|
-
|
258
|
+
with_each_shard(subscope, categories, options) { yield }
|
259
|
+
exception_pipe.last.close
|
260
|
+
rescue Exception => e
|
267
261
|
begin
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
# first, simplify the binary name by stripping directories,
|
274
|
-
# then truncate arguments as necessary
|
275
|
-
bin = File.basename($0) # Process.argv0 doesn't work on Ruby 2.5 (https://bugs.ruby-lang.org/issues/15887)
|
276
|
-
max_length = 128 - bin.length - name.length - 3
|
277
|
-
args = ARGV.join(" ")
|
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
|
262
|
+
dumped = Marshal.dump(e)
|
263
|
+
dumped = nil if dumped.length > 64 * 1024
|
264
|
+
rescue
|
265
|
+
dumped = nil
|
266
|
+
end
|
293
267
|
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
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)
|
268
|
+
if dumped.nil?
|
269
|
+
# couldn't dump the exception; create a copy with just
|
270
|
+
# the message and the backtrace
|
271
|
+
e2 = e.class.new(e.message)
|
272
|
+
backtrace = e.backtrace
|
273
|
+
# truncate excessively long backtraces
|
274
|
+
if backtrace.length > 50
|
275
|
+
backtrace = backtrace[0...25] + ['...'] + backtrace[-25..-1]
|
306
276
|
end
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
exception_pipe.last.flush
|
311
|
-
exception_pipe.last.close
|
312
|
-
exit! 1
|
277
|
+
e2.set_backtrace(backtrace)
|
278
|
+
e2.instance_variable_set(:@active_shards, e.instance_variable_get(:@active_shards))
|
279
|
+
dumped = Marshal.dump(e2)
|
313
280
|
end
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
281
|
+
|
282
|
+
exception_pipe.last.set_encoding(dumped.encoding)
|
283
|
+
exception_pipe.last.write(dumped)
|
284
|
+
exception_pipe.last.flush
|
285
|
+
exception_pipe.last.close
|
286
|
+
exit! 1
|
287
|
+
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)
|
333
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
|
334
307
|
end
|
335
308
|
end
|
336
309
|
|
@@ -544,24 +517,6 @@ module Switchman
|
|
544
517
|
shard || source_shard || Shard.current
|
545
518
|
end
|
546
519
|
|
547
|
-
# given the provided option, determines whether we need to (and whether
|
548
|
-
# it's possible) to determine a reasonable default.
|
549
|
-
def determine_max_procs(max_procs_input, parallel_input=2)
|
550
|
-
max_procs = nil
|
551
|
-
if max_procs_input
|
552
|
-
max_procs = max_procs_input.to_i
|
553
|
-
max_procs = nil if max_procs == 0
|
554
|
-
else
|
555
|
-
return 1 if parallel_input.nil? || parallel_input < 1
|
556
|
-
cpus = Environment.cpu_count
|
557
|
-
if cpus && cpus > 0
|
558
|
-
max_procs = cpus * parallel_input
|
559
|
-
end
|
560
|
-
end
|
561
|
-
|
562
|
-
return max_procs
|
563
|
-
end
|
564
|
-
|
565
520
|
private
|
566
521
|
# in-process caching
|
567
522
|
def cached_shards
|
@@ -80,17 +80,17 @@ module Switchman
|
|
80
80
|
end
|
81
81
|
end
|
82
82
|
|
83
|
-
def self.
|
84
|
-
klass.
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
end
|
83
|
+
def self.prepended(klass)
|
84
|
+
klass.singleton_class.prepend(ClassMethods)
|
85
|
+
end
|
86
|
+
|
87
|
+
def _run_initialize_callbacks
|
88
|
+
@shard ||= if self.class.sharded_primary_key?
|
89
|
+
Shard.shard_for(self[self.class.primary_key], Shard.current(self.class.shard_category))
|
90
|
+
else
|
91
|
+
Shard.current(self.class.shard_category)
|
93
92
|
end
|
93
|
+
super
|
94
94
|
end
|
95
95
|
|
96
96
|
def shard
|
@@ -158,7 +158,7 @@ module Switchman
|
|
158
158
|
end
|
159
159
|
|
160
160
|
def update_columns(*)
|
161
|
-
db =
|
161
|
+
db = shard.database_server
|
162
162
|
if ::GuardRail.environment != db.guard_rail_environment
|
163
163
|
return db.unguard { super }
|
164
164
|
else
|
@@ -30,10 +30,13 @@ module Switchman
|
|
30
30
|
end
|
31
31
|
|
32
32
|
module Migrator
|
33
|
-
# significant change:
|
33
|
+
# significant change: just return MIGRATOR_SALT directly
|
34
|
+
# especially if you're going through pgbouncer, the database
|
35
|
+
# name you're accessing may not be consistent. it is NOT allowed
|
36
|
+
# to run migrations against multiple shards in the same database
|
37
|
+
# concurrently
|
34
38
|
def generate_migrator_advisory_lock_id
|
35
|
-
|
36
|
-
::ActiveRecord::Migrator::MIGRATOR_SALT * shard_name_hash
|
39
|
+
::ActiveRecord::Migrator::MIGRATOR_SALT
|
37
40
|
end
|
38
41
|
|
39
42
|
if ::Rails.version >= '6.0'
|
@@ -13,6 +13,15 @@ module Switchman
|
|
13
13
|
shard.activate(self.class.shard_category) { super }
|
14
14
|
end
|
15
15
|
end
|
16
|
+
|
17
|
+
def delete
|
18
|
+
db = shard.database_server
|
19
|
+
if ::GuardRail.environment != db.guard_rail_environment
|
20
|
+
return db.unguard { super }
|
21
|
+
else
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
16
25
|
end
|
17
26
|
end
|
18
|
-
end
|
27
|
+
end
|
@@ -102,7 +102,7 @@ module Switchman
|
|
102
102
|
WHERE i.relkind = 'i'
|
103
103
|
AND d.indisprimary = 'f'
|
104
104
|
AND t.relname = '#{table_name}'
|
105
|
-
AND
|
105
|
+
AND t.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = '#{shard.name}' )
|
106
106
|
ORDER BY i.relname
|
107
107
|
SQL
|
108
108
|
|
data/lib/switchman/engine.rb
CHANGED
@@ -95,7 +95,7 @@ module Switchman
|
|
95
95
|
|
96
96
|
::StandardError.include(StandardError)
|
97
97
|
|
98
|
-
|
98
|
+
prepend ActiveRecord::Base
|
99
99
|
include ActiveRecord::AttributeMethods
|
100
100
|
include ActiveRecord::Persistence
|
101
101
|
singleton_class.prepend ActiveRecord::ModelSchema::ClassMethods
|
data/lib/switchman/version.rb
CHANGED
data/lib/tasks/switchman.rake
CHANGED
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: 2.
|
4
|
+
version: 2.2.0
|
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: 2021-
|
13
|
+
date: 2021-08-20 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: railties
|