switchman 2.1.3 → 2.2.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/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
|