switchman 3.0.24 → 4.2.4
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 +16 -15
- data/db/migrate/20180828183945_add_default_shard_index.rb +1 -1
- data/db/migrate/20190114212900_add_unique_name_indexes.rb +10 -4
- data/lib/switchman/active_record/abstract_adapter.rb +11 -7
- data/lib/switchman/active_record/associations.rb +157 -50
- data/lib/switchman/active_record/attribute_methods.rb +192 -101
- data/lib/switchman/active_record/base.rb +136 -33
- data/lib/switchman/active_record/calculations.rb +91 -48
- data/lib/switchman/active_record/connection_handler.rb +18 -0
- data/lib/switchman/active_record/connection_pool.rb +41 -6
- data/lib/switchman/active_record/database_configurations.rb +23 -13
- data/lib/switchman/active_record/finder_methods.rb +22 -16
- data/lib/switchman/active_record/log_subscriber.rb +3 -6
- data/lib/switchman/active_record/migration.rb +42 -17
- data/lib/switchman/active_record/model_schema.rb +1 -1
- data/lib/switchman/active_record/pending_migration_connection.rb +17 -0
- data/lib/switchman/active_record/persistence.rb +32 -2
- data/lib/switchman/active_record/postgresql_adapter.rb +37 -22
- data/lib/switchman/active_record/predicate_builder.rb +2 -2
- data/lib/switchman/active_record/query_cache.rb +26 -17
- data/lib/switchman/active_record/query_methods.rb +249 -142
- data/lib/switchman/active_record/reflection.rb +10 -3
- data/lib/switchman/active_record/relation.rb +103 -32
- data/lib/switchman/active_record/spawn_methods.rb +3 -7
- data/lib/switchman/active_record/statement_cache.rb +13 -9
- data/lib/switchman/active_record/table_definition.rb +1 -1
- data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
- data/lib/switchman/active_record/test_fixtures.rb +71 -25
- data/lib/switchman/active_support/cache.rb +9 -4
- data/lib/switchman/arel.rb +16 -25
- data/lib/switchman/call_super.rb +2 -2
- data/lib/switchman/database_server.rb +68 -34
- data/lib/switchman/default_shard.rb +14 -3
- data/lib/switchman/engine.rb +36 -19
- data/lib/switchman/environment.rb +2 -2
- data/lib/switchman/errors.rb +13 -0
- data/lib/switchman/guard_rail/relation.rb +3 -3
- data/lib/switchman/parallel.rb +6 -6
- data/lib/switchman/r_spec_helper.rb +12 -11
- data/lib/switchman/shard.rb +182 -72
- data/lib/switchman/sharded_instrumenter.rb +9 -3
- data/lib/switchman/shared_schema_cache.rb +11 -0
- data/lib/switchman/standard_error.rb +4 -0
- data/lib/switchman/test_helper.rb +3 -3
- data/lib/switchman/version.rb +1 -1
- data/lib/switchman.rb +27 -15
- data/lib/tasks/switchman.rake +96 -60
- metadata +18 -168
data/lib/switchman/shard.rb
CHANGED
|
@@ -5,14 +5,57 @@ module Switchman
|
|
|
5
5
|
# ten trillion possible ids per shard. yup.
|
|
6
6
|
IDS_PER_SHARD = 10_000_000_000_000
|
|
7
7
|
|
|
8
|
+
# rubocop:disable Style/SymbolProc -- transforming to a lambda produces "no receiver given"
|
|
8
9
|
# only allow one default
|
|
9
10
|
validates_uniqueness_of :default, if: ->(s) { s.default? }
|
|
11
|
+
# rubocop:enable Style/SymbolProc
|
|
10
12
|
|
|
11
13
|
after_save :clear_cache
|
|
12
14
|
after_destroy :clear_cache
|
|
13
15
|
|
|
14
16
|
scope :primary, -> { where(name: nil).order(:database_server_id, :id).distinct_on(:database_server_id) }
|
|
15
17
|
|
|
18
|
+
scope :in_region, (lambda do |region, include_regionless: true|
|
|
19
|
+
next in_current_region if region.nil?
|
|
20
|
+
|
|
21
|
+
dbs_by_region = DatabaseServer.group_by(&:region)
|
|
22
|
+
db_count_in_this_region = dbs_by_region[region]&.length.to_i
|
|
23
|
+
db_count_in_this_region += dbs_by_region[nil]&.length.to_i if include_regionless
|
|
24
|
+
non_existent_database_servers = Shard.send(:non_existent_database_servers)
|
|
25
|
+
db_count_in_other_regions = DatabaseServer.all.length -
|
|
26
|
+
db_count_in_this_region +
|
|
27
|
+
non_existent_database_servers.length
|
|
28
|
+
|
|
29
|
+
dbs_in_this_region = dbs_by_region[region]&.map(&:id) || []
|
|
30
|
+
dbs_in_this_region += dbs_by_region[nil]&.map(&:id) || [] if include_regionless
|
|
31
|
+
|
|
32
|
+
if db_count_in_this_region <= db_count_in_other_regions
|
|
33
|
+
if dbs_in_this_region.include?(Shard.default.database_server.id)
|
|
34
|
+
where("database_server_id IN (?) OR database_server_id IS NULL", dbs_in_this_region)
|
|
35
|
+
else
|
|
36
|
+
where(database_server_id: dbs_in_this_region)
|
|
37
|
+
end
|
|
38
|
+
elsif db_count_in_other_regions.zero?
|
|
39
|
+
all
|
|
40
|
+
else
|
|
41
|
+
dbs_not_in_this_region = DatabaseServer.map(&:id) - dbs_in_this_region + non_existent_database_servers
|
|
42
|
+
if dbs_in_this_region.include?(Shard.default.database_server.id)
|
|
43
|
+
where("database_server_id NOT IN (?) OR database_server_id IS NULL", dbs_not_in_this_region)
|
|
44
|
+
else
|
|
45
|
+
where.not(database_server_id: dbs_not_in_this_region)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end)
|
|
49
|
+
|
|
50
|
+
scope :in_current_region, (lambda do |include_regionless: true|
|
|
51
|
+
# sharding isn't set up? maybe we're in tests, or a somehow degraded environment
|
|
52
|
+
# either way there's only one shard, and we always want to see it
|
|
53
|
+
return [default] unless default.is_a?(Switchman::Shard)
|
|
54
|
+
return all if !Switchman.region || DatabaseServer.none?(&:region)
|
|
55
|
+
|
|
56
|
+
in_region(Switchman.region, include_regionless:)
|
|
57
|
+
end)
|
|
58
|
+
|
|
16
59
|
class << self
|
|
17
60
|
def sharded_models
|
|
18
61
|
@sharded_models ||= [::ActiveRecord::Base, UnshardedRecord].freeze
|
|
@@ -36,13 +79,15 @@ module Switchman
|
|
|
36
79
|
|
|
37
80
|
# Now find the actual record, if it exists
|
|
38
81
|
@default = begin
|
|
39
|
-
find_cached(
|
|
82
|
+
find_cached("default_shard") { Shard.where(default: true).take } || default
|
|
40
83
|
rescue
|
|
41
84
|
default
|
|
42
85
|
end
|
|
43
86
|
|
|
44
87
|
# make sure this is not erroneously cached
|
|
45
|
-
|
|
88
|
+
if @default.database_server.instance_variable_defined?(:@primary_shard)
|
|
89
|
+
@default.database_server.remove_instance_variable(:@primary_shard)
|
|
90
|
+
end
|
|
46
91
|
|
|
47
92
|
# and finally, check for cached references to the default shard on the existing connection
|
|
48
93
|
sharded_models.each do |klass|
|
|
@@ -75,28 +120,30 @@ module Switchman
|
|
|
75
120
|
klass.current_switchman_shard != shard
|
|
76
121
|
|
|
77
122
|
(activated_classes ||= []) << klass
|
|
78
|
-
klass.connected_to_stack << { shard: shard.database_server.id.to_sym,
|
|
123
|
+
klass.connected_to_stack << { shard: shard.database_server.id.to_sym,
|
|
124
|
+
klasses: [klass],
|
|
125
|
+
switchman_shard: shard }
|
|
79
126
|
end
|
|
80
127
|
activated_classes
|
|
81
128
|
end
|
|
82
129
|
|
|
83
130
|
def active_shards
|
|
84
|
-
sharded_models.
|
|
131
|
+
sharded_models.filter_map do |klass|
|
|
85
132
|
[klass, current(klass)]
|
|
86
|
-
end.
|
|
133
|
+
end.to_h
|
|
87
134
|
end
|
|
88
135
|
|
|
89
136
|
def lookup(id)
|
|
90
137
|
id_i = id.to_i
|
|
91
|
-
return current if id_i == current.id || id ==
|
|
92
|
-
return default if id_i == default.id || id.nil? || id ==
|
|
138
|
+
return current if id_i == current.id || id == "self"
|
|
139
|
+
return default if id_i == default.id || id.nil? || id == "default"
|
|
93
140
|
|
|
94
141
|
id = id_i
|
|
95
142
|
raise ArgumentError if id.zero?
|
|
96
143
|
|
|
97
144
|
unless cached_shards.key?(id)
|
|
98
145
|
cached_shards[id] = Shard.default.activate do
|
|
99
|
-
find_cached([
|
|
146
|
+
find_cached(["shard", id]) { find_by(id:) }
|
|
100
147
|
end
|
|
101
148
|
end
|
|
102
149
|
cached_shards[id]
|
|
@@ -120,7 +167,8 @@ module Switchman
|
|
|
120
167
|
# forking.
|
|
121
168
|
# exception: - :ignore, :raise, :defer (wait until the end and raise the first
|
|
122
169
|
# error), or a proc
|
|
123
|
-
|
|
170
|
+
# output: - :simple, :decorated (with database_server_id:shard_name), custom lambda transformer
|
|
171
|
+
def with_each_shard(*args, parallel: false, exception: :raise, output: :simple)
|
|
124
172
|
raise ArgumentError, "wrong number of arguments (#{args.length} for 0...2)" if args.length > 2
|
|
125
173
|
|
|
126
174
|
return Array.wrap(yield) unless default.is_a?(Shard)
|
|
@@ -135,19 +183,28 @@ module Switchman
|
|
|
135
183
|
scope, classes = args
|
|
136
184
|
end
|
|
137
185
|
|
|
186
|
+
output = if output == :decorated
|
|
187
|
+
->(arg) { "#{Shard.current.description}: #{arg}" }
|
|
188
|
+
elsif output == :simple
|
|
189
|
+
nil
|
|
190
|
+
else
|
|
191
|
+
output
|
|
192
|
+
end
|
|
138
193
|
parallel = [Environment.cpu_count || 2, 2].min if parallel == true
|
|
139
194
|
parallel = 0 if parallel == false || parallel.nil?
|
|
140
195
|
|
|
141
196
|
scope ||= Shard.all
|
|
142
|
-
|
|
197
|
+
if ::ActiveRecord::Relation === scope && scope.order_values.empty?
|
|
198
|
+
scope = scope.order(::Arel.sql("database_server_id IS NOT NULL, database_server_id, id"))
|
|
199
|
+
end
|
|
143
200
|
|
|
144
201
|
if parallel > 1
|
|
145
202
|
if ::ActiveRecord::Relation === scope
|
|
146
203
|
# still need a post-uniq, cause the default database server could be NULL or Rails.env in the db
|
|
147
|
-
database_servers = scope.reorder(
|
|
148
|
-
|
|
204
|
+
database_servers = scope.reorder("database_server_id").select(:database_server_id).distinct
|
|
205
|
+
.filter_map(&:database_server).uniq
|
|
149
206
|
# nothing to do
|
|
150
|
-
return if database_servers.
|
|
207
|
+
return if database_servers.none?
|
|
151
208
|
|
|
152
209
|
scopes = database_servers.to_h do |server|
|
|
153
210
|
[server, scope.merge(server.shards)]
|
|
@@ -159,25 +216,29 @@ module Switchman
|
|
|
159
216
|
# clear connections prior to forking (no more queries will be executed in the parent,
|
|
160
217
|
# and we want them gone so that we don't accidentally use them post-fork doing something
|
|
161
218
|
# silly like dealloc'ing prepared statements)
|
|
162
|
-
::ActiveRecord::Base.clear_all_connections!
|
|
219
|
+
::ActiveRecord::Base.connection_handler.clear_all_connections!(:all)
|
|
163
220
|
|
|
164
|
-
parent_process_name =
|
|
165
|
-
ret = ::Parallel.map(scopes, in_processes: scopes.length > 1 ? parallel : 0) do |server, subscope|
|
|
221
|
+
parent_process_name = sanitized_process_title
|
|
222
|
+
ret = ::Parallel.map(scopes, in_processes: (scopes.length > 1) ? parallel : 0) do |server, subscope|
|
|
166
223
|
name = server.id
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
$stderr = Parallel::PrefixingIO.new(name, STDERR)
|
|
170
|
-
# rubocop:enable Style/GlobalStdStream
|
|
224
|
+
last_description = name
|
|
225
|
+
|
|
171
226
|
begin
|
|
172
227
|
max_length = 128 - name.length - 3
|
|
173
228
|
short_parent_name = parent_process_name[0..max_length] if max_length >= 0
|
|
174
|
-
new_title = [short_parent_name, name].join(
|
|
229
|
+
new_title = [short_parent_name, name].join(" ")
|
|
175
230
|
Process.setproctitle(new_title)
|
|
176
231
|
Switchman.config[:on_fork_proc]&.call
|
|
177
|
-
with_each_shard(subscope,
|
|
232
|
+
with_each_shard(subscope,
|
|
233
|
+
classes,
|
|
234
|
+
exception:,
|
|
235
|
+
output: output || :decorated) do
|
|
236
|
+
last_description = Shard.current.description
|
|
237
|
+
Parallel::ResultWrapper.new(yield)
|
|
238
|
+
end
|
|
178
239
|
rescue => e
|
|
179
240
|
logger.error e.full_message
|
|
180
|
-
Parallel::QuietExceptionWrapper.new(
|
|
241
|
+
Parallel::QuietExceptionWrapper.new(last_description, ::Parallel::ExceptionWrapper.new(e))
|
|
181
242
|
end
|
|
182
243
|
end.flatten
|
|
183
244
|
|
|
@@ -185,8 +246,9 @@ module Switchman
|
|
|
185
246
|
unless errors.empty?
|
|
186
247
|
raise errors.first.exception if errors.length == 1
|
|
187
248
|
|
|
249
|
+
errors_desc = errors.map(&:name).sort.join(", ")
|
|
188
250
|
raise Errors::ParallelShardExecError,
|
|
189
|
-
"The following database server(s) did not finish processing cleanly: #{
|
|
251
|
+
"The following database server(s) did not finish processing cleanly: #{errors_desc}",
|
|
190
252
|
cause: errors.first.exception
|
|
191
253
|
end
|
|
192
254
|
|
|
@@ -195,14 +257,20 @@ module Switchman
|
|
|
195
257
|
|
|
196
258
|
classes ||= []
|
|
197
259
|
|
|
198
|
-
previous_shard = nil
|
|
199
260
|
result = []
|
|
200
261
|
ex = nil
|
|
262
|
+
old_stdout = $stdout
|
|
263
|
+
old_stderr = $stderr
|
|
201
264
|
scope.each do |shard|
|
|
202
265
|
# shard references a database server that isn't configured in this environment
|
|
203
266
|
next unless shard.database_server
|
|
204
267
|
|
|
205
268
|
shard.activate(*classes) do
|
|
269
|
+
if output
|
|
270
|
+
$stdout = Parallel::TransformingIO.new(output, $stdout)
|
|
271
|
+
$stderr = Parallel::TransformingIO.new(output, $stderr)
|
|
272
|
+
end
|
|
273
|
+
|
|
206
274
|
result.concat Array.wrap(yield)
|
|
207
275
|
rescue
|
|
208
276
|
case exception
|
|
@@ -216,8 +284,10 @@ module Switchman
|
|
|
216
284
|
else
|
|
217
285
|
raise
|
|
218
286
|
end
|
|
287
|
+
ensure
|
|
288
|
+
$stdout = old_stdout
|
|
289
|
+
$stderr = old_stderr
|
|
219
290
|
end
|
|
220
|
-
previous_shard = shard
|
|
221
291
|
end
|
|
222
292
|
raise ex if ex
|
|
223
293
|
|
|
@@ -241,7 +311,7 @@ module Switchman
|
|
|
241
311
|
else
|
|
242
312
|
shard = partition_object.shard
|
|
243
313
|
end
|
|
244
|
-
when Integer,
|
|
314
|
+
when Integer, /\A\d+\Z/, /\A(\d+)~(\d+)\Z/
|
|
245
315
|
local_id, shard = Shard.local_id_for(partition_object)
|
|
246
316
|
local_id ||= partition_object
|
|
247
317
|
object = local_id unless partition_proc
|
|
@@ -283,14 +353,14 @@ module Switchman
|
|
|
283
353
|
case any_id
|
|
284
354
|
when ::ActiveRecord::Base
|
|
285
355
|
any_id.id
|
|
286
|
-
when
|
|
356
|
+
when /\A(\d+)~(-?\d+)\Z/
|
|
287
357
|
local_id = $2.to_i
|
|
288
358
|
signed_id_operation(local_id) do |id|
|
|
289
359
|
return nil if id > IDS_PER_SHARD
|
|
290
360
|
|
|
291
361
|
($1.to_i * IDS_PER_SHARD) + id
|
|
292
362
|
end
|
|
293
|
-
when Integer,
|
|
363
|
+
when Integer, /\A-?\d+\Z/
|
|
294
364
|
any_id.to_i
|
|
295
365
|
end
|
|
296
366
|
end
|
|
@@ -374,7 +444,7 @@ module Switchman
|
|
|
374
444
|
end
|
|
375
445
|
|
|
376
446
|
def configure_connects_to
|
|
377
|
-
full_connects_to_hash = DatabaseServer.
|
|
447
|
+
full_connects_to_hash = DatabaseServer.to_h { |db| [db.id.to_sym, db.connects_to_hash] }
|
|
378
448
|
sharded_models.each do |klass|
|
|
379
449
|
connects_to_hash = full_connects_to_hash.deep_dup
|
|
380
450
|
if klass == UnshardedRecord
|
|
@@ -387,13 +457,16 @@ module Switchman
|
|
|
387
457
|
connects_to_hash.each do |(db_name, role_hash)|
|
|
388
458
|
role_hash.each_key do |role|
|
|
389
459
|
role_hash.delete(role) if klass.connection_handler.retrieve_connection_pool(
|
|
390
|
-
klass.connection_specification_name, role
|
|
460
|
+
klass.connection_specification_name, role:, shard: db_name
|
|
391
461
|
)
|
|
392
462
|
end
|
|
393
463
|
end
|
|
394
464
|
end
|
|
395
465
|
|
|
466
|
+
# this resets the default shard on rails 7.1+, but we want to preserve it
|
|
467
|
+
shard_was = klass.default_shard
|
|
396
468
|
klass.connects_to shards: connects_to_hash
|
|
469
|
+
klass.default_shard = shard_was
|
|
397
470
|
end
|
|
398
471
|
end
|
|
399
472
|
|
|
@@ -427,8 +500,50 @@ module Switchman
|
|
|
427
500
|
shard = nil unless shard.database_server
|
|
428
501
|
shard
|
|
429
502
|
end
|
|
503
|
+
|
|
504
|
+
# Determines the name of the current process, including arguments, but stripping
|
|
505
|
+
# any shebang from the invoked script, and any additional path info from the
|
|
506
|
+
# executable.
|
|
507
|
+
#
|
|
508
|
+
# @return [String]
|
|
509
|
+
def sanitized_process_title
|
|
510
|
+
# get the effective process name from `ps`; this will include any changes
|
|
511
|
+
# from Process.setproctitle _or_ assigning to $0.
|
|
512
|
+
parent_process_name = `ps -ocommand= -p#{Process.pid}`.strip
|
|
513
|
+
# Effective process titles may be shorter than the actual
|
|
514
|
+
# command; truncate our ARGV[0] so that they are comparable
|
|
515
|
+
# for the next step
|
|
516
|
+
argv0 = if parent_process_name.length < Process.argv0.length
|
|
517
|
+
Process.argv0[0..parent_process_name.length]
|
|
518
|
+
else
|
|
519
|
+
Process.argv0
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# when running via a shebang, the `ps` output will include the shebang
|
|
523
|
+
# (i.e. it will be "ruby bin/rails c"); attempt to strip it off.
|
|
524
|
+
# Note that argv0 in this case will _only_ be `bin/rails` (no shebang,
|
|
525
|
+
# no arguments). We want to preserve the arguments we got from `ps`
|
|
526
|
+
if (index = parent_process_name.index(argv0))
|
|
527
|
+
parent_process_name.slice!(0...index)
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
# remove directories from the main executable to make more room
|
|
531
|
+
# for additional info
|
|
532
|
+
argv = parent_process_name.shellsplit
|
|
533
|
+
argv[0] = File.basename(argv[0])
|
|
534
|
+
argv.shelljoin
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
# @return [Array<String>] the list of database servers that are in the
|
|
538
|
+
# config, but don't have any shards on them
|
|
539
|
+
def non_existent_database_servers
|
|
540
|
+
@non_existent_database_servers ||=
|
|
541
|
+
Shard.distinct.pluck(:database_server_id).compact - DatabaseServer.all.map(&:id)
|
|
542
|
+
end
|
|
430
543
|
end
|
|
431
544
|
|
|
545
|
+
delegate :region, :in_region?, :in_current_region?, to: :database_server
|
|
546
|
+
|
|
432
547
|
def name
|
|
433
548
|
unless instance_variable_defined?(:@name)
|
|
434
549
|
# protect against re-entrancy
|
|
@@ -457,7 +572,7 @@ module Switchman
|
|
|
457
572
|
end
|
|
458
573
|
|
|
459
574
|
def description
|
|
460
|
-
[database_server.id, name].compact.join(
|
|
575
|
+
[database_server.id, name].compact.join(":")
|
|
461
576
|
end
|
|
462
577
|
|
|
463
578
|
# Shards are always on the default shard
|
|
@@ -469,9 +584,13 @@ module Switchman
|
|
|
469
584
|
id
|
|
470
585
|
end
|
|
471
586
|
|
|
472
|
-
def
|
|
587
|
+
def original_id_value
|
|
588
|
+
id
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def activate(*classes, &)
|
|
473
592
|
shards = hashify_classes(classes)
|
|
474
|
-
Shard.activate(shards, &
|
|
593
|
+
Shard.activate(shards, &)
|
|
475
594
|
end
|
|
476
595
|
|
|
477
596
|
# for use from console ONLY
|
|
@@ -491,48 +610,39 @@ module Switchman
|
|
|
491
610
|
end
|
|
492
611
|
|
|
493
612
|
def drop_database
|
|
494
|
-
raise
|
|
613
|
+
raise "Cannot drop the database of the default shard" if default?
|
|
495
614
|
return unless read_attribute(:name)
|
|
496
615
|
|
|
497
616
|
begin
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
drop_statement = sharding_config[adapter]&.[](:drop_statement)
|
|
501
|
-
drop_statement ||= sharding_config[:drop_statement]
|
|
502
|
-
if drop_statement
|
|
503
|
-
drop_statement = Array(drop_statement).dup.
|
|
504
|
-
map { |statement| statement.gsub('%{name}', name) }
|
|
617
|
+
activate do
|
|
618
|
+
self.class.drop_database(name)
|
|
505
619
|
end
|
|
620
|
+
rescue ::ActiveRecord::StatementInvalid => e
|
|
621
|
+
logger.error "Drop failed: #{e}"
|
|
622
|
+
end
|
|
623
|
+
end
|
|
506
624
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
drop_statement ||= "DROP SCHEMA #{name} CASCADE"
|
|
525
|
-
Array(drop_statement).each do |stmt|
|
|
526
|
-
::ActiveRecord::Base.connection.execute(stmt)
|
|
527
|
-
end
|
|
528
|
-
ensure
|
|
529
|
-
conn.raw_connection.set_notice_processor(&old_proc) if old_proc
|
|
530
|
-
end
|
|
531
|
-
end
|
|
625
|
+
#
|
|
626
|
+
# Drops a specific database/schema from the currently active connection
|
|
627
|
+
#
|
|
628
|
+
def self.drop_database(name)
|
|
629
|
+
sharding_config = Switchman.config || {}
|
|
630
|
+
drop_statement = sharding_config["postgresql"]&.[](:drop_statement)
|
|
631
|
+
drop_statement ||= sharding_config[:drop_statement]
|
|
632
|
+
drop_statement = Array(drop_statement).map { |statement| statement.gsub("%{name}", name) } if drop_statement
|
|
633
|
+
|
|
634
|
+
::GuardRail.activate(:deploy) do
|
|
635
|
+
# Shut up, Postgres!
|
|
636
|
+
conn = ::ActiveRecord::Base.connection
|
|
637
|
+
old_proc = conn.raw_connection.set_notice_processor {}
|
|
638
|
+
begin
|
|
639
|
+
drop_statement ||= "DROP SCHEMA #{name} CASCADE"
|
|
640
|
+
Array(drop_statement).each do |stmt|
|
|
641
|
+
::ActiveRecord::Base.connection.execute(stmt)
|
|
532
642
|
end
|
|
643
|
+
ensure
|
|
644
|
+
conn.raw_connection.set_notice_processor(&old_proc) if old_proc
|
|
533
645
|
end
|
|
534
|
-
rescue
|
|
535
|
-
logger.info "Drop failed: #{$!}"
|
|
536
646
|
end
|
|
537
647
|
end
|
|
538
648
|
|
|
@@ -551,7 +661,7 @@ module Switchman
|
|
|
551
661
|
end
|
|
552
662
|
|
|
553
663
|
def destroy
|
|
554
|
-
raise(
|
|
664
|
+
raise("Cannot destroy the default shard") if default?
|
|
555
665
|
|
|
556
666
|
super
|
|
557
667
|
end
|
|
@@ -560,8 +670,8 @@ module Switchman
|
|
|
560
670
|
|
|
561
671
|
def clear_cache
|
|
562
672
|
Shard.default.activate do
|
|
563
|
-
Switchman.cache.delete([
|
|
564
|
-
Switchman.cache.delete(
|
|
673
|
+
Switchman.cache.delete(["shard", id].join("/"))
|
|
674
|
+
Switchman.cache.delete("default_shard") if default?
|
|
565
675
|
end
|
|
566
676
|
self.class.clear_cache
|
|
567
677
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Switchman
|
|
4
4
|
class ShardedInstrumenter < ::SimpleDelegator
|
|
5
5
|
def initialize(instrumenter, shard_host)
|
|
6
|
-
super
|
|
6
|
+
super(instrumenter)
|
|
7
7
|
@shard_host = shard_host
|
|
8
8
|
end
|
|
9
9
|
|
|
@@ -13,13 +13,19 @@ module Switchman
|
|
|
13
13
|
# when we might be doing a query while defining attribute methods,
|
|
14
14
|
# so just avoid logging then
|
|
15
15
|
if shard.is_a?(Shard) && Shard.instance_variable_get(:@attribute_methods_generated)
|
|
16
|
+
env = if ::Rails.version < "8.0"
|
|
17
|
+
@shard_host.pool.connection_class&.current_role
|
|
18
|
+
else
|
|
19
|
+
@shard_host.pool.connection_descriptor&.name&.constantize&.current_role
|
|
20
|
+
end
|
|
21
|
+
|
|
16
22
|
payload[:shard] = {
|
|
17
23
|
database_server_id: shard.database_server.id,
|
|
18
24
|
id: shard.id,
|
|
19
|
-
env:
|
|
25
|
+
env:
|
|
20
26
|
}
|
|
21
27
|
end
|
|
22
|
-
super
|
|
28
|
+
super
|
|
23
29
|
end
|
|
24
30
|
end
|
|
25
31
|
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Switchman
|
|
4
|
+
class SharedSchemaCache
|
|
5
|
+
def self.get_schema_cache(connection)
|
|
6
|
+
@schema_cache ||= ::ActiveRecord::ConnectionAdapters::SchemaCache.new(connection)
|
|
7
|
+
@schema_cache.connection = connection
|
|
8
|
+
@schema_cache
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -13,6 +13,10 @@ module Switchman
|
|
|
13
13
|
Thread.current[:switchman_error_handler] = true
|
|
14
14
|
|
|
15
15
|
@active_shards ||= Shard.active_shards
|
|
16
|
+
rescue
|
|
17
|
+
# intentionally empty - don't allow calculating the active_shards to prevent
|
|
18
|
+
# creating the StandardError for any reason. this prevents various random issues
|
|
19
|
+
# when a StandardError is created within a finalizer
|
|
16
20
|
ensure
|
|
17
21
|
Thread.current[:switchman_error_handler] = nil
|
|
18
22
|
end
|
|
@@ -19,7 +19,7 @@ module Switchman
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
server1 = Shard.default.database_server
|
|
22
|
-
server2 = DatabaseServer.create(Shard.default.database_server.config.merge(server2: true))
|
|
22
|
+
server2 = DatabaseServer.create(Shard.default.database_server.config.merge(server2: true, schema_dump: false))
|
|
23
23
|
|
|
24
24
|
if server1 == Shard.default.database_server && server1.config[:shard1] && server1.config[:shard2]
|
|
25
25
|
# look for the shards in the db already
|
|
@@ -63,9 +63,9 @@ module Switchman
|
|
|
63
63
|
|
|
64
64
|
def find_existing_test_shard(server, name)
|
|
65
65
|
if server == Shard.default.database_server
|
|
66
|
-
server.shards.where(name:
|
|
66
|
+
server.shards.where(name:).first
|
|
67
67
|
else
|
|
68
|
-
shard = Shard.where(
|
|
68
|
+
shard = Shard.where("database_server_id IS NOT NULL AND name=?", name).first
|
|
69
69
|
# if somehow databases got created in a different order, change the shard to match
|
|
70
70
|
shard.database_server = server if shard
|
|
71
71
|
shard
|
data/lib/switchman/version.rb
CHANGED
data/lib/switchman.rb
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require "guard_rail"
|
|
4
|
+
require "zeitwerk"
|
|
5
5
|
|
|
6
6
|
class SwitchmanInflector < Zeitwerk::GemInflector
|
|
7
7
|
def camelize(basename, abspath)
|
|
8
8
|
if basename =~ /\Apostgresql_(.*)/
|
|
9
|
-
|
|
9
|
+
"PostgreSQL" + super($1, abspath)
|
|
10
10
|
else
|
|
11
11
|
super
|
|
12
12
|
end
|
|
@@ -18,21 +18,33 @@ loader.inflector = SwitchmanInflector.new(__FILE__)
|
|
|
18
18
|
loader.setup
|
|
19
19
|
|
|
20
20
|
module Switchman
|
|
21
|
-
|
|
22
|
-
# TODO: load from yaml
|
|
23
|
-
@config ||= {}
|
|
24
|
-
end
|
|
21
|
+
Deprecation = ::ActiveSupport::Deprecation.new("4.0", "Switchman")
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
end
|
|
23
|
+
class << self
|
|
24
|
+
attr_writer :cache
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
def config
|
|
27
|
+
# TODO: load from yaml
|
|
28
|
+
@config ||= {}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def cache
|
|
32
|
+
(@cache.respond_to?(:call) ? @cache.call : @cache) || ::Rails.cache
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def region
|
|
36
|
+
config[:region]
|
|
37
|
+
end
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
def foreign_key_check(name, type, limit: nil)
|
|
40
|
+
return unless name.to_s.end_with?("_id") && type.to_s == "integer" && limit.to_i < 8
|
|
41
|
+
|
|
42
|
+
puts <<~TEXT.squish
|
|
43
|
+
WARNING: All foreign keys need to be 8-byte integers.
|
|
44
|
+
#{name} looks like a foreign key.
|
|
45
|
+
If so, please add the option: `:limit => 8`
|
|
46
|
+
TEXT
|
|
47
|
+
end
|
|
36
48
|
end
|
|
37
49
|
|
|
38
50
|
class OrderOnMultiShardQuery < RuntimeError; end
|