switchman 3.3.1 → 4.1.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 +15 -14
- 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 +5 -3
- data/lib/switchman/active_record/associations.rb +91 -51
- data/lib/switchman/active_record/attribute_methods.rb +88 -43
- data/lib/switchman/active_record/base.rb +113 -40
- data/lib/switchman/active_record/calculations.rb +98 -51
- data/lib/switchman/active_record/connection_handler.rb +18 -0
- data/lib/switchman/active_record/connection_pool.rb +56 -6
- data/lib/switchman/active_record/database_configurations.rb +37 -15
- data/lib/switchman/active_record/finder_methods.rb +47 -17
- data/lib/switchman/active_record/log_subscriber.rb +11 -5
- data/lib/switchman/active_record/migration.rb +51 -3
- data/lib/switchman/active_record/pending_migration_connection.rb +17 -0
- data/lib/switchman/active_record/persistence.rb +30 -0
- 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 +57 -20
- data/lib/switchman/active_record/query_methods.rb +148 -44
- data/lib/switchman/active_record/reflection.rb +9 -2
- data/lib/switchman/active_record/relation.rb +79 -15
- data/lib/switchman/active_record/spawn_methods.rb +3 -7
- data/lib/switchman/active_record/statement_cache.rb +2 -2
- 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 +75 -25
- data/lib/switchman/active_support/cache.rb +9 -4
- data/lib/switchman/arel.rb +34 -18
- data/lib/switchman/call_super.rb +2 -8
- data/lib/switchman/database_server.rb +72 -34
- data/lib/switchman/default_shard.rb +14 -3
- data/lib/switchman/engine.rb +38 -22
- data/lib/switchman/environment.rb +2 -2
- data/lib/switchman/errors.rb +13 -0
- data/lib/switchman/guard_rail/relation.rb +1 -2
- data/lib/switchman/parallel.rb +6 -6
- data/lib/switchman/r_spec_helper.rb +12 -11
- data/lib/switchman/shard.rb +185 -71
- data/lib/switchman/sharded_instrumenter.rb +3 -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 +50 -46
|
@@ -13,9 +13,8 @@ module Switchman
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
%w[update_all delete_all].each do |method|
|
|
16
|
-
arg_params = RUBY_VERSION <= '2.8' ? '*args' : '*args, **kwargs'
|
|
17
16
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
18
|
-
def #{method}(
|
|
17
|
+
def #{method}(*args, **kwargs)
|
|
19
18
|
db = Shard.current(connection_class_for_self).database_server
|
|
20
19
|
db.unguard { super }
|
|
21
20
|
end
|
data/lib/switchman/parallel.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "parallel"
|
|
4
4
|
|
|
5
5
|
module Switchman
|
|
6
6
|
module Parallel
|
|
@@ -50,19 +50,19 @@ module Switchman
|
|
|
50
50
|
end
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
class
|
|
53
|
+
class TransformingIO
|
|
54
54
|
delegate_missing_to :@original_io
|
|
55
55
|
|
|
56
|
-
def initialize(
|
|
57
|
-
@
|
|
56
|
+
def initialize(transformer, original_io)
|
|
57
|
+
@transformer = transformer
|
|
58
58
|
@original_io = original_io
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def puts(*args)
|
|
62
|
-
args.flatten.each { |arg| @original_io.puts
|
|
62
|
+
args.flatten.each { |arg| @original_io.puts @transformer.call(arg) }
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
Parallel::UndumpableException.prepend(Switchman::Parallel::UndumpableException)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "switchman/test_helper"
|
|
4
4
|
|
|
5
5
|
module Switchman
|
|
6
6
|
# including this module in your specs will give you several shards to
|
|
@@ -34,9 +34,9 @@ module Switchman
|
|
|
34
34
|
groups = group.class.descendant_filtered_examples.map(&:example_group).uniq
|
|
35
35
|
next unless groups.any? { |descendant_group| RSpecHelper.included_in?(descendant_group) }
|
|
36
36
|
|
|
37
|
-
puts
|
|
37
|
+
puts "Setting up sharding for all specs..."
|
|
38
38
|
Shard.delete_all
|
|
39
|
-
Switchman.cache.delete(
|
|
39
|
+
Switchman.cache.delete("default_shard")
|
|
40
40
|
|
|
41
41
|
@@shard1, @@shard2 = TestHelper.recreate_persistent_test_shards
|
|
42
42
|
@@default_shard = Shard.default
|
|
@@ -48,7 +48,7 @@ module Switchman
|
|
|
48
48
|
@@shard1 = @@shard1.create_new_shard
|
|
49
49
|
@@shard2 = @@shard2.create_new_shard
|
|
50
50
|
rescue => e
|
|
51
|
-
warn
|
|
51
|
+
warn "Sharding setup FAILED!:"
|
|
52
52
|
while e
|
|
53
53
|
warn "\n#{e}\n"
|
|
54
54
|
warn e.backtrace
|
|
@@ -66,9 +66,9 @@ module Switchman
|
|
|
66
66
|
# we'll re-persist in the group's `before :all`; we don't want them to exist
|
|
67
67
|
# in the db before then
|
|
68
68
|
Shard.delete_all
|
|
69
|
-
Switchman.cache.delete(
|
|
69
|
+
Switchman.cache.delete("default_shard")
|
|
70
70
|
Shard.default(reload: true)
|
|
71
|
-
puts
|
|
71
|
+
puts "Done!"
|
|
72
72
|
|
|
73
73
|
main_pid = Process.pid
|
|
74
74
|
at_exit do
|
|
@@ -76,7 +76,7 @@ module Switchman
|
|
|
76
76
|
|
|
77
77
|
# preserve rspec's exit status
|
|
78
78
|
status = $!.is_a?(::SystemExit) ? $!.status : nil
|
|
79
|
-
puts
|
|
79
|
+
puts "Tearing down sharding for all specs"
|
|
80
80
|
@@shard1.database_server.destroy unless @@shard1.database_server == Shard.default.database_server
|
|
81
81
|
unless @@keep_the_shards
|
|
82
82
|
@@shard1.drop_database
|
|
@@ -95,7 +95,7 @@ module Switchman
|
|
|
95
95
|
dup = @@default_shard.dup
|
|
96
96
|
dup.id = @@default_shard.id
|
|
97
97
|
dup.save!
|
|
98
|
-
Switchman.cache.delete(
|
|
98
|
+
Switchman.cache.delete("default_shard")
|
|
99
99
|
Shard.default(reload: true)
|
|
100
100
|
dup = @@shard1.dup
|
|
101
101
|
dup.id = @@shard1.id
|
|
@@ -107,7 +107,7 @@ module Switchman
|
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
klass.before do
|
|
110
|
-
raise
|
|
110
|
+
raise "Sharding did not set up correctly" if @@sharding_failed
|
|
111
111
|
|
|
112
112
|
Shard.clear_cache
|
|
113
113
|
if use_transactional_tests
|
|
@@ -121,18 +121,19 @@ module Switchman
|
|
|
121
121
|
next if @@sharding_failed
|
|
122
122
|
|
|
123
123
|
# clean up after specs
|
|
124
|
-
DatabaseServer.
|
|
124
|
+
DatabaseServer.each do |ds|
|
|
125
125
|
if ds.fake? && ds != @shard2.database_server
|
|
126
126
|
ds.shards.delete_all unless use_transactional_tests
|
|
127
127
|
ds.destroy
|
|
128
128
|
end
|
|
129
|
+
ds.remove_instance_variable(:@primary_shard_id) if ds.instance_variable_defined?(:@primary_shard_id)
|
|
129
130
|
end
|
|
130
131
|
end
|
|
131
132
|
|
|
132
133
|
klass.after(:all) do
|
|
133
134
|
# Don't truncate because that can create some fun cross-connection lock contention
|
|
134
135
|
Shard.delete_all
|
|
135
|
-
Switchman.cache.delete(
|
|
136
|
+
Switchman.cache.delete("default_shard")
|
|
136
137
|
Shard.default(reload: true)
|
|
137
138
|
end
|
|
138
139
|
end
|
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,17 +183,26 @@ 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
207
|
return if database_servers.count.zero?
|
|
151
208
|
|
|
@@ -159,25 +216,33 @@ 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
|
-
::
|
|
219
|
+
if ::Rails.version < "7.1"
|
|
220
|
+
::ActiveRecord::Base.clear_all_connections!(nil)
|
|
221
|
+
else
|
|
222
|
+
::ActiveRecord::Base.connection_handler.clear_all_connections!(:all)
|
|
223
|
+
end
|
|
163
224
|
|
|
164
|
-
parent_process_name =
|
|
165
|
-
ret = ::Parallel.map(scopes, in_processes: scopes.length > 1 ? parallel : 0) do |server, subscope|
|
|
225
|
+
parent_process_name = sanitized_process_title
|
|
226
|
+
ret = ::Parallel.map(scopes, in_processes: (scopes.length > 1) ? parallel : 0) do |server, subscope|
|
|
166
227
|
name = server.id
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
$stderr = Parallel::PrefixingIO.new(name, STDERR)
|
|
170
|
-
# rubocop:enable Style/GlobalStdStream
|
|
228
|
+
last_description = name
|
|
229
|
+
|
|
171
230
|
begin
|
|
172
231
|
max_length = 128 - name.length - 3
|
|
173
232
|
short_parent_name = parent_process_name[0..max_length] if max_length >= 0
|
|
174
|
-
new_title = [short_parent_name, name].join(
|
|
233
|
+
new_title = [short_parent_name, name].join(" ")
|
|
175
234
|
Process.setproctitle(new_title)
|
|
176
235
|
Switchman.config[:on_fork_proc]&.call
|
|
177
|
-
with_each_shard(subscope,
|
|
236
|
+
with_each_shard(subscope,
|
|
237
|
+
classes,
|
|
238
|
+
exception:,
|
|
239
|
+
output: output || :decorated) do
|
|
240
|
+
last_description = Shard.current.description
|
|
241
|
+
Parallel::ResultWrapper.new(yield)
|
|
242
|
+
end
|
|
178
243
|
rescue => e
|
|
179
244
|
logger.error e.full_message
|
|
180
|
-
Parallel::QuietExceptionWrapper.new(
|
|
245
|
+
Parallel::QuietExceptionWrapper.new(last_description, ::Parallel::ExceptionWrapper.new(e))
|
|
181
246
|
end
|
|
182
247
|
end.flatten
|
|
183
248
|
|
|
@@ -185,8 +250,9 @@ module Switchman
|
|
|
185
250
|
unless errors.empty?
|
|
186
251
|
raise errors.first.exception if errors.length == 1
|
|
187
252
|
|
|
253
|
+
errors_desc = errors.map(&:name).sort.join(", ")
|
|
188
254
|
raise Errors::ParallelShardExecError,
|
|
189
|
-
"The following database server(s) did not finish processing cleanly: #{
|
|
255
|
+
"The following database server(s) did not finish processing cleanly: #{errors_desc}",
|
|
190
256
|
cause: errors.first.exception
|
|
191
257
|
end
|
|
192
258
|
|
|
@@ -195,14 +261,20 @@ module Switchman
|
|
|
195
261
|
|
|
196
262
|
classes ||= []
|
|
197
263
|
|
|
198
|
-
previous_shard = nil
|
|
199
264
|
result = []
|
|
200
265
|
ex = nil
|
|
266
|
+
old_stdout = $stdout
|
|
267
|
+
old_stderr = $stderr
|
|
201
268
|
scope.each do |shard|
|
|
202
269
|
# shard references a database server that isn't configured in this environment
|
|
203
270
|
next unless shard.database_server
|
|
204
271
|
|
|
205
272
|
shard.activate(*classes) do
|
|
273
|
+
if output
|
|
274
|
+
$stdout = Parallel::TransformingIO.new(output, $stdout)
|
|
275
|
+
$stderr = Parallel::TransformingIO.new(output, $stderr)
|
|
276
|
+
end
|
|
277
|
+
|
|
206
278
|
result.concat Array.wrap(yield)
|
|
207
279
|
rescue
|
|
208
280
|
case exception
|
|
@@ -216,8 +288,10 @@ module Switchman
|
|
|
216
288
|
else
|
|
217
289
|
raise
|
|
218
290
|
end
|
|
291
|
+
ensure
|
|
292
|
+
$stdout = old_stdout
|
|
293
|
+
$stderr = old_stderr
|
|
219
294
|
end
|
|
220
|
-
previous_shard = shard
|
|
221
295
|
end
|
|
222
296
|
raise ex if ex
|
|
223
297
|
|
|
@@ -241,7 +315,7 @@ module Switchman
|
|
|
241
315
|
else
|
|
242
316
|
shard = partition_object.shard
|
|
243
317
|
end
|
|
244
|
-
when Integer,
|
|
318
|
+
when Integer, /\A\d+\Z/, /\A(\d+)~(\d+)\Z/
|
|
245
319
|
local_id, shard = Shard.local_id_for(partition_object)
|
|
246
320
|
local_id ||= partition_object
|
|
247
321
|
object = local_id unless partition_proc
|
|
@@ -283,14 +357,14 @@ module Switchman
|
|
|
283
357
|
case any_id
|
|
284
358
|
when ::ActiveRecord::Base
|
|
285
359
|
any_id.id
|
|
286
|
-
when
|
|
360
|
+
when /\A(\d+)~(-?\d+)\Z/
|
|
287
361
|
local_id = $2.to_i
|
|
288
362
|
signed_id_operation(local_id) do |id|
|
|
289
363
|
return nil if id > IDS_PER_SHARD
|
|
290
364
|
|
|
291
365
|
($1.to_i * IDS_PER_SHARD) + id
|
|
292
366
|
end
|
|
293
|
-
when Integer,
|
|
367
|
+
when Integer, /\A-?\d+\Z/
|
|
294
368
|
any_id.to_i
|
|
295
369
|
end
|
|
296
370
|
end
|
|
@@ -374,7 +448,7 @@ module Switchman
|
|
|
374
448
|
end
|
|
375
449
|
|
|
376
450
|
def configure_connects_to
|
|
377
|
-
full_connects_to_hash = DatabaseServer.
|
|
451
|
+
full_connects_to_hash = DatabaseServer.to_h { |db| [db.id.to_sym, db.connects_to_hash] }
|
|
378
452
|
sharded_models.each do |klass|
|
|
379
453
|
connects_to_hash = full_connects_to_hash.deep_dup
|
|
380
454
|
if klass == UnshardedRecord
|
|
@@ -387,13 +461,16 @@ module Switchman
|
|
|
387
461
|
connects_to_hash.each do |(db_name, role_hash)|
|
|
388
462
|
role_hash.each_key do |role|
|
|
389
463
|
role_hash.delete(role) if klass.connection_handler.retrieve_connection_pool(
|
|
390
|
-
klass.connection_specification_name, role
|
|
464
|
+
klass.connection_specification_name, role:, shard: db_name
|
|
391
465
|
)
|
|
392
466
|
end
|
|
393
467
|
end
|
|
394
468
|
end
|
|
395
469
|
|
|
470
|
+
# this resets the default shard on rails 7.1+, but we want to preserve it
|
|
471
|
+
shard_was = klass.default_shard
|
|
396
472
|
klass.connects_to shards: connects_to_hash
|
|
473
|
+
klass.default_shard = shard_was
|
|
397
474
|
end
|
|
398
475
|
end
|
|
399
476
|
|
|
@@ -427,8 +504,50 @@ module Switchman
|
|
|
427
504
|
shard = nil unless shard.database_server
|
|
428
505
|
shard
|
|
429
506
|
end
|
|
507
|
+
|
|
508
|
+
# Determines the name of the current process, including arguments, but stripping
|
|
509
|
+
# any shebang from the invoked script, and any additional path info from the
|
|
510
|
+
# executable.
|
|
511
|
+
#
|
|
512
|
+
# @return [String]
|
|
513
|
+
def sanitized_process_title
|
|
514
|
+
# get the effective process name from `ps`; this will include any changes
|
|
515
|
+
# from Process.setproctitle _or_ assigning to $0.
|
|
516
|
+
parent_process_name = `ps -ocommand= -p#{Process.pid}`.strip
|
|
517
|
+
# Effective process titles may be shorter than the actual
|
|
518
|
+
# command; truncate our ARGV[0] so that they are comparable
|
|
519
|
+
# for the next step
|
|
520
|
+
argv0 = if parent_process_name.length < Process.argv0.length
|
|
521
|
+
Process.argv0[0..parent_process_name.length]
|
|
522
|
+
else
|
|
523
|
+
Process.argv0
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
# when running via a shebang, the `ps` output will include the shebang
|
|
527
|
+
# (i.e. it will be "ruby bin/rails c"); attempt to strip it off.
|
|
528
|
+
# Note that argv0 in this case will _only_ be `bin/rails` (no shebang,
|
|
529
|
+
# no arguments). We want to preserve the arguments we got from `ps`
|
|
530
|
+
if (index = parent_process_name.index(argv0))
|
|
531
|
+
parent_process_name.slice!(0...index)
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# remove directories from the main executable to make more room
|
|
535
|
+
# for additional info
|
|
536
|
+
argv = parent_process_name.shellsplit
|
|
537
|
+
argv[0] = File.basename(argv[0])
|
|
538
|
+
argv.shelljoin
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
# @return [Array<String>] the list of database servers that are in the
|
|
542
|
+
# config, but don't have any shards on them
|
|
543
|
+
def non_existent_database_servers
|
|
544
|
+
@non_existent_database_servers ||=
|
|
545
|
+
Shard.distinct.pluck(:database_server_id).compact - DatabaseServer.all.map(&:id)
|
|
546
|
+
end
|
|
430
547
|
end
|
|
431
548
|
|
|
549
|
+
delegate :region, :in_region?, :in_current_region?, to: :database_server
|
|
550
|
+
|
|
432
551
|
def name
|
|
433
552
|
unless instance_variable_defined?(:@name)
|
|
434
553
|
# protect against re-entrancy
|
|
@@ -457,7 +576,7 @@ module Switchman
|
|
|
457
576
|
end
|
|
458
577
|
|
|
459
578
|
def description
|
|
460
|
-
[database_server.id, name].compact.join(
|
|
579
|
+
[database_server.id, name].compact.join(":")
|
|
461
580
|
end
|
|
462
581
|
|
|
463
582
|
# Shards are always on the default shard
|
|
@@ -469,9 +588,13 @@ module Switchman
|
|
|
469
588
|
id
|
|
470
589
|
end
|
|
471
590
|
|
|
472
|
-
def
|
|
591
|
+
def original_id_value
|
|
592
|
+
id
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def activate(*classes, &)
|
|
473
596
|
shards = hashify_classes(classes)
|
|
474
|
-
Shard.activate(shards, &
|
|
597
|
+
Shard.activate(shards, &)
|
|
475
598
|
end
|
|
476
599
|
|
|
477
600
|
# for use from console ONLY
|
|
@@ -491,48 +614,39 @@ module Switchman
|
|
|
491
614
|
end
|
|
492
615
|
|
|
493
616
|
def drop_database
|
|
494
|
-
raise
|
|
617
|
+
raise "Cannot drop the database of the default shard" if default?
|
|
495
618
|
return unless read_attribute(:name)
|
|
496
619
|
|
|
497
620
|
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) }
|
|
621
|
+
activate do
|
|
622
|
+
self.class.drop_database(name)
|
|
505
623
|
end
|
|
624
|
+
rescue ::ActiveRecord::StatementInvalid => e
|
|
625
|
+
logger.error "Drop failed: #{e}"
|
|
626
|
+
end
|
|
627
|
+
end
|
|
506
628
|
|
|
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
|
|
629
|
+
#
|
|
630
|
+
# Drops a specific database/schema from the currently active connection
|
|
631
|
+
#
|
|
632
|
+
def self.drop_database(name)
|
|
633
|
+
sharding_config = Switchman.config || {}
|
|
634
|
+
drop_statement = sharding_config["postgresql"]&.[](:drop_statement)
|
|
635
|
+
drop_statement ||= sharding_config[:drop_statement]
|
|
636
|
+
drop_statement = Array(drop_statement).map { |statement| statement.gsub("%{name}", name) } if drop_statement
|
|
637
|
+
|
|
638
|
+
::GuardRail.activate(:deploy) do
|
|
639
|
+
# Shut up, Postgres!
|
|
640
|
+
conn = ::ActiveRecord::Base.connection
|
|
641
|
+
old_proc = conn.raw_connection.set_notice_processor {}
|
|
642
|
+
begin
|
|
643
|
+
drop_statement ||= "DROP SCHEMA #{name} CASCADE"
|
|
644
|
+
Array(drop_statement).each do |stmt|
|
|
645
|
+
::ActiveRecord::Base.connection.execute(stmt)
|
|
532
646
|
end
|
|
647
|
+
ensure
|
|
648
|
+
conn.raw_connection.set_notice_processor(&old_proc) if old_proc
|
|
533
649
|
end
|
|
534
|
-
rescue
|
|
535
|
-
logger.info "Drop failed: #{$!}"
|
|
536
650
|
end
|
|
537
651
|
end
|
|
538
652
|
|
|
@@ -551,7 +665,7 @@ module Switchman
|
|
|
551
665
|
end
|
|
552
666
|
|
|
553
667
|
def destroy
|
|
554
|
-
raise(
|
|
668
|
+
raise("Cannot destroy the default shard") if default?
|
|
555
669
|
|
|
556
670
|
super
|
|
557
671
|
end
|
|
@@ -560,8 +674,8 @@ module Switchman
|
|
|
560
674
|
|
|
561
675
|
def clear_cache
|
|
562
676
|
Shard.default.activate do
|
|
563
|
-
Switchman.cache.delete([
|
|
564
|
-
Switchman.cache.delete(
|
|
677
|
+
Switchman.cache.delete(["shard", id].join("/"))
|
|
678
|
+
Switchman.cache.delete("default_shard") if default?
|
|
565
679
|
end
|
|
566
680
|
self.class.clear_cache
|
|
567
681
|
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
|
|
|
@@ -16,10 +16,10 @@ module Switchman
|
|
|
16
16
|
payload[:shard] = {
|
|
17
17
|
database_server_id: shard.database_server.id,
|
|
18
18
|
id: shard.id,
|
|
19
|
-
env:
|
|
19
|
+
env: @shard_host.pool.connection_class&.current_role
|
|
20
20
|
}
|
|
21
21
|
end
|
|
22
|
-
super
|
|
22
|
+
super
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
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