switchman 3.0.14 → 3.5.20
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/action_controller/caching.rb +2 -2
- data/lib/switchman/active_record/abstract_adapter.rb +6 -6
- data/lib/switchman/active_record/associations.rb +365 -0
- data/lib/switchman/active_record/attribute_methods.rb +188 -99
- data/lib/switchman/active_record/base.rb +185 -40
- data/lib/switchman/active_record/calculations.rb +64 -40
- data/lib/switchman/active_record/connection_handler.rb +18 -0
- data/lib/switchman/active_record/connection_pool.rb +24 -5
- data/lib/switchman/active_record/database_configurations.rb +37 -13
- data/lib/switchman/active_record/finder_methods.rb +46 -16
- data/lib/switchman/active_record/log_subscriber.rb +11 -5
- data/lib/switchman/active_record/migration.rb +52 -8
- data/lib/switchman/active_record/model_schema.rb +1 -1
- data/lib/switchman/active_record/persistence.rb +31 -3
- data/lib/switchman/active_record/postgresql_adapter.rb +11 -10
- data/lib/switchman/active_record/predicate_builder.rb +2 -2
- data/lib/switchman/active_record/query_cache.rb +49 -20
- data/lib/switchman/active_record/query_methods.rb +187 -136
- data/lib/switchman/active_record/reflection.rb +1 -1
- data/lib/switchman/active_record/relation.rb +33 -26
- data/lib/switchman/active_record/spawn_methods.rb +2 -2
- data/lib/switchman/active_record/statement_cache.rb +11 -7
- 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 +26 -16
- data/lib/switchman/active_support/cache.rb +20 -1
- data/lib/switchman/arel.rb +34 -18
- data/lib/switchman/call_super.rb +8 -2
- data/lib/switchman/database_server.rb +91 -45
- data/lib/switchman/default_shard.rb +14 -5
- data/lib/switchman/engine.rb +79 -126
- data/lib/switchman/environment.rb +2 -2
- data/lib/switchman/errors.rb +17 -2
- data/lib/switchman/guard_rail/relation.rb +8 -10
- data/lib/switchman/guard_rail.rb +5 -0
- data/lib/switchman/parallel.rb +68 -0
- data/lib/switchman/r_spec_helper.rb +14 -11
- data/lib/switchman/rails.rb +2 -5
- data/{app/models → lib}/switchman/shard.rb +186 -189
- data/lib/switchman/sharded_instrumenter.rb +5 -1
- data/lib/switchman/shared_schema_cache.rb +11 -0
- data/lib/switchman/standard_error.rb +6 -5
- data/lib/switchman/test_helper.rb +2 -2
- data/{app/models → lib}/switchman/unsharded_record.rb +1 -1
- data/lib/switchman/version.rb +1 -1
- data/lib/switchman.rb +44 -12
- data/lib/tasks/switchman.rake +74 -53
- metadata +42 -53
- data/lib/switchman/active_record/association.rb +0 -206
- data/lib/switchman/open4.rb +0 -80
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'switchman/database_server'
|
|
4
|
-
require 'switchman/default_shard'
|
|
5
|
-
require 'switchman/environment'
|
|
6
|
-
require 'switchman/errors'
|
|
7
|
-
|
|
8
3
|
module Switchman
|
|
9
4
|
class Shard < UnshardedRecord
|
|
10
5
|
# ten trillion possible ids per shard. yup.
|
|
@@ -18,6 +13,47 @@ module Switchman
|
|
|
18
13
|
|
|
19
14
|
scope :primary, -> { where(name: nil).order(:database_server_id, :id).distinct_on(:database_server_id) }
|
|
20
15
|
|
|
16
|
+
scope :in_region, (lambda do |region, include_regionless: true|
|
|
17
|
+
next in_current_region if region.nil?
|
|
18
|
+
|
|
19
|
+
dbs_by_region = DatabaseServer.group_by(&:region)
|
|
20
|
+
db_count_in_this_region = dbs_by_region[region]&.length.to_i
|
|
21
|
+
db_count_in_this_region += dbs_by_region[nil]&.length.to_i if include_regionless
|
|
22
|
+
non_existent_database_servers = Shard.send(:non_existent_database_servers)
|
|
23
|
+
db_count_in_other_regions = DatabaseServer.all.length -
|
|
24
|
+
db_count_in_this_region +
|
|
25
|
+
non_existent_database_servers.length
|
|
26
|
+
|
|
27
|
+
dbs_in_this_region = dbs_by_region[region]&.map(&:id) || []
|
|
28
|
+
dbs_in_this_region += dbs_by_region[nil]&.map(&:id) || [] if include_regionless
|
|
29
|
+
|
|
30
|
+
if db_count_in_this_region <= db_count_in_other_regions
|
|
31
|
+
if dbs_in_this_region.include?(Shard.default.database_server.id)
|
|
32
|
+
where("database_server_id IN (?) OR database_server_id IS NULL", dbs_in_this_region)
|
|
33
|
+
else
|
|
34
|
+
where(database_server_id: dbs_in_this_region)
|
|
35
|
+
end
|
|
36
|
+
elsif db_count_in_other_regions.zero?
|
|
37
|
+
all
|
|
38
|
+
else
|
|
39
|
+
dbs_not_in_this_region = DatabaseServer.map(&:id) - dbs_in_this_region + non_existent_database_servers
|
|
40
|
+
if dbs_in_this_region.include?(Shard.default.database_server.id)
|
|
41
|
+
where("database_server_id NOT IN (?) OR database_server_id IS NULL", dbs_not_in_this_region)
|
|
42
|
+
else
|
|
43
|
+
where.not(database_server_id: dbs_not_in_this_region)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end)
|
|
47
|
+
|
|
48
|
+
scope :in_current_region, (lambda do |include_regionless: true|
|
|
49
|
+
# sharding isn't set up? maybe we're in tests, or a somehow degraded environment
|
|
50
|
+
# either way there's only one shard, and we always want to see it
|
|
51
|
+
return [default] unless default.is_a?(Switchman::Shard)
|
|
52
|
+
return all if !Switchman.region || DatabaseServer.none?(&:region)
|
|
53
|
+
|
|
54
|
+
in_region(Switchman.region, include_regionless: include_regionless)
|
|
55
|
+
end)
|
|
56
|
+
|
|
21
57
|
class << self
|
|
22
58
|
def sharded_models
|
|
23
59
|
@sharded_models ||= [::ActiveRecord::Base, UnshardedRecord].freeze
|
|
@@ -41,16 +77,15 @@ module Switchman
|
|
|
41
77
|
|
|
42
78
|
# Now find the actual record, if it exists
|
|
43
79
|
@default = begin
|
|
44
|
-
find_cached(
|
|
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
|
-
# Otherwise, rescue the fake default if the table doesn't exist
|
|
80
|
+
find_cached("default_shard") { Shard.where(default: true).take } || default
|
|
47
81
|
rescue
|
|
48
|
-
|
|
82
|
+
default
|
|
49
83
|
end
|
|
50
|
-
return default unless @default
|
|
51
84
|
|
|
52
85
|
# make sure this is not erroneously cached
|
|
53
|
-
|
|
86
|
+
if @default.database_server.instance_variable_defined?(:@primary_shard)
|
|
87
|
+
@default.database_server.remove_instance_variable(:@primary_shard)
|
|
88
|
+
end
|
|
54
89
|
|
|
55
90
|
# and finally, check for cached references to the default shard on the existing connection
|
|
56
91
|
sharded_models.each do |klass|
|
|
@@ -83,28 +118,30 @@ module Switchman
|
|
|
83
118
|
klass.current_switchman_shard != shard
|
|
84
119
|
|
|
85
120
|
(activated_classes ||= []) << klass
|
|
86
|
-
klass.connected_to_stack << { shard: shard.database_server.id.to_sym,
|
|
121
|
+
klass.connected_to_stack << { shard: shard.database_server.id.to_sym,
|
|
122
|
+
klasses: [klass],
|
|
123
|
+
switchman_shard: shard }
|
|
87
124
|
end
|
|
88
125
|
activated_classes
|
|
89
126
|
end
|
|
90
127
|
|
|
91
128
|
def active_shards
|
|
92
|
-
sharded_models.
|
|
129
|
+
sharded_models.filter_map do |klass|
|
|
93
130
|
[klass, current(klass)]
|
|
94
|
-
end.
|
|
131
|
+
end.to_h
|
|
95
132
|
end
|
|
96
133
|
|
|
97
134
|
def lookup(id)
|
|
98
135
|
id_i = id.to_i
|
|
99
|
-
return current if id_i == current.id || id ==
|
|
100
|
-
return default if id_i == default.id || id.nil? || id ==
|
|
136
|
+
return current if id_i == current.id || id == "self"
|
|
137
|
+
return default if id_i == default.id || id.nil? || id == "default"
|
|
101
138
|
|
|
102
139
|
id = id_i
|
|
103
140
|
raise ArgumentError if id.zero?
|
|
104
141
|
|
|
105
142
|
unless cached_shards.key?(id)
|
|
106
143
|
cached_shards[id] = Shard.default.activate do
|
|
107
|
-
find_cached([
|
|
144
|
+
find_cached(["shard", id]) { find_by(id: id) }
|
|
108
145
|
end
|
|
109
146
|
end
|
|
110
147
|
cached_shards[id]
|
|
@@ -125,11 +162,11 @@ module Switchman
|
|
|
125
162
|
# * +classes+ - an array of classes to activate
|
|
126
163
|
# parallel: - true/false to execute in parallel, or an integer of how many
|
|
127
164
|
# sub-processes. Note that parallel invocation currently uses
|
|
128
|
-
# forking
|
|
129
|
-
# results back
|
|
165
|
+
# forking.
|
|
130
166
|
# exception: - :ignore, :raise, :defer (wait until the end and raise the first
|
|
131
167
|
# error), or a proc
|
|
132
|
-
|
|
168
|
+
# output: - :simple, :decorated (with database_server_id:shard_name)
|
|
169
|
+
def with_each_shard(*args, parallel: false, exception: :raise, output: :simple)
|
|
133
170
|
raise ArgumentError, "wrong number of arguments (#{args.length} for 0...2)" if args.length > 2
|
|
134
171
|
|
|
135
172
|
return Array.wrap(yield) unless default.is_a?(Shard)
|
|
@@ -148,160 +185,84 @@ module Switchman
|
|
|
148
185
|
parallel = 0 if parallel == false || parallel.nil?
|
|
149
186
|
|
|
150
187
|
scope ||= Shard.all
|
|
151
|
-
|
|
188
|
+
if ::ActiveRecord::Relation === scope && scope.order_values.empty?
|
|
189
|
+
scope = scope.order(::Arel.sql("database_server_id IS NOT NULL, database_server_id, id"))
|
|
190
|
+
end
|
|
152
191
|
|
|
153
192
|
if parallel > 1
|
|
154
193
|
if ::ActiveRecord::Relation === scope
|
|
155
194
|
# still need a post-uniq, cause the default database server could be NULL or Rails.env in the db
|
|
156
|
-
database_servers = scope.reorder(
|
|
157
|
-
|
|
195
|
+
database_servers = scope.reorder("database_server_id").select(:database_server_id).distinct
|
|
196
|
+
.filter_map(&:database_server).uniq
|
|
158
197
|
# nothing to do
|
|
159
198
|
return if database_servers.count.zero?
|
|
160
199
|
|
|
161
200
|
scopes = database_servers.to_h do |server|
|
|
162
|
-
[server, server.shards
|
|
201
|
+
[server, scope.merge(server.shards)]
|
|
163
202
|
end
|
|
164
203
|
else
|
|
165
204
|
scopes = scope.group_by(&:database_server)
|
|
166
205
|
end
|
|
167
206
|
|
|
168
|
-
exception_pipes = []
|
|
169
|
-
pids = []
|
|
170
|
-
out_fds = []
|
|
171
|
-
err_fds = []
|
|
172
|
-
pid_to_name_map = {}
|
|
173
|
-
fd_to_name_map = {}
|
|
174
|
-
errors = []
|
|
175
|
-
|
|
176
|
-
wait_for_output = lambda do
|
|
177
|
-
ready, = IO.select(out_fds + err_fds)
|
|
178
|
-
ready.each do |fd|
|
|
179
|
-
if fd.eof?
|
|
180
|
-
fd.close
|
|
181
|
-
out_fds.delete(fd)
|
|
182
|
-
err_fds.delete(fd)
|
|
183
|
-
next
|
|
184
|
-
end
|
|
185
|
-
line = fd.readline
|
|
186
|
-
puts "#{fd_to_name_map[fd]}: #{line}"
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
# only one process; don't bother forking
|
|
191
|
-
return with_each_shard(scopes.first.last, classes, exception: exception, &block) if scopes.length == 1
|
|
192
|
-
|
|
193
207
|
# clear connections prior to forking (no more queries will be executed in the parent,
|
|
194
208
|
# and we want them gone so that we don't accidentally use them post-fork doing something
|
|
195
209
|
# silly like dealloc'ing prepared statements)
|
|
196
|
-
::
|
|
210
|
+
if ::Rails.version < "7.1"
|
|
211
|
+
::ActiveRecord::Base.clear_all_connections!(nil)
|
|
212
|
+
else
|
|
213
|
+
::ActiveRecord::Base.connection_handler.clear_all_connections!(:all)
|
|
214
|
+
end
|
|
197
215
|
|
|
198
|
-
|
|
216
|
+
parent_process_name = sanitized_process_title
|
|
217
|
+
ret = ::Parallel.map(scopes, in_processes: (scopes.length > 1) ? parallel : 0) do |server, subscope|
|
|
199
218
|
name = server.id
|
|
219
|
+
last_description = name
|
|
200
220
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
# set a pretty name for the process title, up to 128 characters
|
|
207
|
-
# (we don't actually know the limit, depending on how the process
|
|
208
|
-
# was started)
|
|
209
|
-
# first, simplify the binary name by stripping directories,
|
|
210
|
-
# then truncate arguments as necessary
|
|
211
|
-
bin = File.basename($0) # Process.argv0 doesn't work on Ruby 2.5 (https://bugs.ruby-lang.org/issues/15887)
|
|
212
|
-
max_length = 128 - bin.length - name.length - 3
|
|
213
|
-
args = ARGV.join(' ')
|
|
214
|
-
args = args[0..max_length] if max_length >= 0
|
|
215
|
-
new_title = [bin, args, name].join(' ')
|
|
221
|
+
begin
|
|
222
|
+
max_length = 128 - name.length - 3
|
|
223
|
+
short_parent_name = parent_process_name[0..max_length] if max_length >= 0
|
|
224
|
+
new_title = [short_parent_name, name].join(" ")
|
|
216
225
|
Process.setproctitle(new_title)
|
|
217
|
-
|
|
218
|
-
with_each_shard(subscope, classes, exception: exception,
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
begin
|
|
222
|
-
dumped = Marshal.dump(e)
|
|
223
|
-
dumped = nil if dumped.length > 64 * 1024
|
|
224
|
-
rescue
|
|
225
|
-
dumped = nil
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
if dumped.nil?
|
|
229
|
-
# couldn't dump the exception; create a copy with just
|
|
230
|
-
# the message and the backtrace
|
|
231
|
-
e2 = e.class.new(e.message)
|
|
232
|
-
backtrace = e.backtrace
|
|
233
|
-
# truncate excessively long backtraces
|
|
234
|
-
backtrace = backtrace[0...25] + ['...'] + backtrace[-25..] if backtrace.length > 50
|
|
235
|
-
e2.set_backtrace(backtrace)
|
|
236
|
-
e2.instance_variable_set(:@active_shards, e.instance_variable_get(:@active_shards))
|
|
237
|
-
dumped = Marshal.dump(e2)
|
|
238
|
-
end
|
|
239
|
-
exception_pipe.last.set_encoding(dumped.encoding)
|
|
240
|
-
exception_pipe.last.write(dumped)
|
|
241
|
-
exception_pipe.last.flush
|
|
242
|
-
exception_pipe.last.close
|
|
243
|
-
exit! 1
|
|
244
|
-
end)
|
|
245
|
-
exception_pipe.last.close
|
|
246
|
-
pids << pid
|
|
247
|
-
io_in.close # don't care about writing to stdin
|
|
248
|
-
out_fds << io_out
|
|
249
|
-
err_fds << io_err
|
|
250
|
-
pid_to_name_map[pid] = name
|
|
251
|
-
fd_to_name_map[io_out] = name
|
|
252
|
-
fd_to_name_map[io_err] = name
|
|
253
|
-
|
|
254
|
-
while pids.count >= parallel
|
|
255
|
-
while out_fds.count >= parallel
|
|
256
|
-
# wait for output if we've hit the parallel limit
|
|
257
|
-
wait_for_output.call
|
|
226
|
+
Switchman.config[:on_fork_proc]&.call
|
|
227
|
+
with_each_shard(subscope, classes, exception: exception, output: :decorated) do
|
|
228
|
+
last_description = Shard.current.description
|
|
229
|
+
Parallel::ResultWrapper.new(yield)
|
|
258
230
|
end
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
errors << pid_to_name_map[found_pid] if status.exitstatus != 0
|
|
231
|
+
rescue => e
|
|
232
|
+
logger.error e.full_message
|
|
233
|
+
Parallel::QuietExceptionWrapper.new(last_description, ::Parallel::ExceptionWrapper.new(e))
|
|
263
234
|
end
|
|
264
|
-
|
|
265
|
-
found_pid, status = Process.wait2
|
|
266
|
-
pids.delete(found_pid)
|
|
267
|
-
errors << pid_to_name_map[found_pid] if status.exitstatus != 0
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
wait_for_output.call while out_fds.any? || err_fds.any?
|
|
271
|
-
pids.each do |pid|
|
|
272
|
-
_, status = Process.waitpid2(pid)
|
|
273
|
-
errors << pid_to_name_map[pid] if status.exitstatus != 0
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
# check for an exception; we only re-raise the first one
|
|
277
|
-
exception_pipes.each do |exception_pipe|
|
|
278
|
-
serialized_exception = exception_pipe.first.read
|
|
279
|
-
next if serialized_exception.empty?
|
|
280
|
-
|
|
281
|
-
ex = Marshal.load(serialized_exception) # rubocop:disable Security/MarshalLoad
|
|
282
|
-
raise ex
|
|
283
|
-
ensure
|
|
284
|
-
exception_pipe.first.close
|
|
285
|
-
end
|
|
235
|
+
end.flatten
|
|
286
236
|
|
|
237
|
+
errors = ret.select { |val| val.is_a?(Parallel::QuietExceptionWrapper) }
|
|
287
238
|
unless errors.empty?
|
|
288
|
-
raise
|
|
289
|
-
|
|
239
|
+
raise errors.first.exception if errors.length == 1
|
|
240
|
+
|
|
241
|
+
errors_desc = errors.map(&:name).sort.join(", ")
|
|
242
|
+
raise Errors::ParallelShardExecError,
|
|
243
|
+
"The following database server(s) did not finish processing cleanly: #{errors_desc}",
|
|
244
|
+
cause: errors.first.exception
|
|
290
245
|
end
|
|
291
246
|
|
|
292
|
-
return
|
|
247
|
+
return ret.map(&:result)
|
|
293
248
|
end
|
|
294
249
|
|
|
295
250
|
classes ||= []
|
|
296
251
|
|
|
297
|
-
previous_shard = nil
|
|
298
252
|
result = []
|
|
299
253
|
ex = nil
|
|
254
|
+
old_stdout = $stdout
|
|
255
|
+
old_stderr = $stderr
|
|
300
256
|
scope.each do |shard|
|
|
301
257
|
# shard references a database server that isn't configured in this environment
|
|
302
258
|
next unless shard.database_server
|
|
303
259
|
|
|
304
260
|
shard.activate(*classes) do
|
|
261
|
+
if output == :decorated
|
|
262
|
+
$stdout = Parallel::PrefixingIO.new(shard.description, $stdout)
|
|
263
|
+
$stderr = Parallel::PrefixingIO.new(shard.description, $stderr)
|
|
264
|
+
end
|
|
265
|
+
|
|
305
266
|
result.concat Array.wrap(yield)
|
|
306
267
|
rescue
|
|
307
268
|
case exception
|
|
@@ -315,8 +276,10 @@ module Switchman
|
|
|
315
276
|
else
|
|
316
277
|
raise
|
|
317
278
|
end
|
|
279
|
+
ensure
|
|
280
|
+
$stdout = old_stdout
|
|
281
|
+
$stderr = old_stderr
|
|
318
282
|
end
|
|
319
|
-
previous_shard = shard
|
|
320
283
|
end
|
|
321
284
|
raise ex if ex
|
|
322
285
|
|
|
@@ -340,7 +303,7 @@ module Switchman
|
|
|
340
303
|
else
|
|
341
304
|
shard = partition_object.shard
|
|
342
305
|
end
|
|
343
|
-
when Integer,
|
|
306
|
+
when Integer, /\A\d+\Z/, /\A(\d+)~(\d+)\Z/
|
|
344
307
|
local_id, shard = Shard.local_id_for(partition_object)
|
|
345
308
|
local_id ||= partition_object
|
|
346
309
|
object = local_id unless partition_proc
|
|
@@ -382,14 +345,14 @@ module Switchman
|
|
|
382
345
|
case any_id
|
|
383
346
|
when ::ActiveRecord::Base
|
|
384
347
|
any_id.id
|
|
385
|
-
when
|
|
348
|
+
when /\A(\d+)~(-?\d+)\Z/
|
|
386
349
|
local_id = $2.to_i
|
|
387
350
|
signed_id_operation(local_id) do |id|
|
|
388
351
|
return nil if id > IDS_PER_SHARD
|
|
389
352
|
|
|
390
353
|
($1.to_i * IDS_PER_SHARD) + id
|
|
391
354
|
end
|
|
392
|
-
when Integer,
|
|
355
|
+
when Integer, /\A-?\d+\Z/
|
|
393
356
|
any_id.to_i
|
|
394
357
|
end
|
|
395
358
|
end
|
|
@@ -467,17 +430,13 @@ module Switchman
|
|
|
467
430
|
|
|
468
431
|
private
|
|
469
432
|
|
|
470
|
-
def sharding_initialized
|
|
471
|
-
@sharding_initialized ||= false
|
|
472
|
-
end
|
|
473
|
-
|
|
474
433
|
def add_sharded_model(klass)
|
|
475
434
|
@sharded_models = (sharded_models + [klass]).freeze
|
|
476
|
-
|
|
435
|
+
configure_connects_to
|
|
477
436
|
end
|
|
478
437
|
|
|
479
|
-
def
|
|
480
|
-
full_connects_to_hash = DatabaseServer.
|
|
438
|
+
def configure_connects_to
|
|
439
|
+
full_connects_to_hash = DatabaseServer.to_h { |db| [db.id.to_sym, db.connects_to_hash] }
|
|
481
440
|
sharded_models.each do |klass|
|
|
482
441
|
connects_to_hash = full_connects_to_hash.deep_dup
|
|
483
442
|
if klass == UnshardedRecord
|
|
@@ -496,10 +455,11 @@ module Switchman
|
|
|
496
455
|
end
|
|
497
456
|
end
|
|
498
457
|
|
|
458
|
+
# this resets the default shard on rails 7.1+, but we want to preserve it
|
|
459
|
+
shard_was = klass.default_shard
|
|
499
460
|
klass.connects_to shards: connects_to_hash
|
|
461
|
+
klass.default_shard = shard_was
|
|
500
462
|
end
|
|
501
|
-
|
|
502
|
-
@sharding_initialized = true
|
|
503
463
|
end
|
|
504
464
|
|
|
505
465
|
# in-process caching
|
|
@@ -532,8 +492,50 @@ module Switchman
|
|
|
532
492
|
shard = nil unless shard.database_server
|
|
533
493
|
shard
|
|
534
494
|
end
|
|
495
|
+
|
|
496
|
+
# Determines the name of the current process, including arguments, but stripping
|
|
497
|
+
# any shebang from the invoked script, and any additional path info from the
|
|
498
|
+
# executable.
|
|
499
|
+
#
|
|
500
|
+
# @return [String]
|
|
501
|
+
def sanitized_process_title
|
|
502
|
+
# get the effective process name from `ps`; this will include any changes
|
|
503
|
+
# from Process.setproctitle _or_ assigning to $0.
|
|
504
|
+
parent_process_name = `ps -ocommand= -p#{Process.pid}`.strip
|
|
505
|
+
# Effective process titles may be shorter than the actual
|
|
506
|
+
# command; truncate our ARGV[0] so that they are comparable
|
|
507
|
+
# for the next step
|
|
508
|
+
argv0 = if parent_process_name.length < Process.argv0.length
|
|
509
|
+
Process.argv0[0..parent_process_name.length]
|
|
510
|
+
else
|
|
511
|
+
Process.argv0
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
# when running via a shebang, the `ps` output will include the shebang
|
|
515
|
+
# (i.e. it will be "ruby bin/rails c"); attempt to strip it off.
|
|
516
|
+
# Note that argv0 in this case will _only_ be `bin/rails` (no shebang,
|
|
517
|
+
# no arguments). We want to preserve the arguments we got from `ps`
|
|
518
|
+
if (index = parent_process_name.index(argv0))
|
|
519
|
+
parent_process_name.slice!(0...index)
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# remove directories from the main executable to make more room
|
|
523
|
+
# for additional info
|
|
524
|
+
argv = parent_process_name.shellsplit
|
|
525
|
+
argv[0] = File.basename(argv[0])
|
|
526
|
+
argv.shelljoin
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
# @return [Array<String>] the list of database servers that are in the
|
|
530
|
+
# config, but don't have any shards on them
|
|
531
|
+
def non_existent_database_servers
|
|
532
|
+
@non_existent_database_servers ||=
|
|
533
|
+
Shard.distinct.pluck(:database_server_id).compact - DatabaseServer.all.map(&:id)
|
|
534
|
+
end
|
|
535
535
|
end
|
|
536
536
|
|
|
537
|
+
delegate :region, :in_region?, :in_current_region?, to: :database_server
|
|
538
|
+
|
|
537
539
|
def name
|
|
538
540
|
unless instance_variable_defined?(:@name)
|
|
539
541
|
# protect against re-entrancy
|
|
@@ -562,7 +564,7 @@ module Switchman
|
|
|
562
564
|
end
|
|
563
565
|
|
|
564
566
|
def description
|
|
565
|
-
[database_server.id, name].compact.join(
|
|
567
|
+
[database_server.id, name].compact.join(":")
|
|
566
568
|
end
|
|
567
569
|
|
|
568
570
|
# Shards are always on the default shard
|
|
@@ -574,6 +576,10 @@ module Switchman
|
|
|
574
576
|
id
|
|
575
577
|
end
|
|
576
578
|
|
|
579
|
+
def original_id_value
|
|
580
|
+
id
|
|
581
|
+
end
|
|
582
|
+
|
|
577
583
|
def activate(*classes, &block)
|
|
578
584
|
shards = hashify_classes(classes)
|
|
579
585
|
Shard.activate(shards, &block)
|
|
@@ -596,48 +602,39 @@ module Switchman
|
|
|
596
602
|
end
|
|
597
603
|
|
|
598
604
|
def drop_database
|
|
599
|
-
raise
|
|
605
|
+
raise "Cannot drop the database of the default shard" if default?
|
|
600
606
|
return unless read_attribute(:name)
|
|
601
607
|
|
|
602
608
|
begin
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
drop_statement = sharding_config[adapter]&.[](:drop_statement)
|
|
606
|
-
drop_statement ||= sharding_config[:drop_statement]
|
|
607
|
-
if drop_statement
|
|
608
|
-
drop_statement = Array(drop_statement).dup.
|
|
609
|
-
map { |statement| statement.gsub('%{name}', name) }
|
|
609
|
+
activate do
|
|
610
|
+
self.class.drop_database(name)
|
|
610
611
|
end
|
|
612
|
+
rescue ::ActiveRecord::StatementInvalid => e
|
|
613
|
+
logger.error "Drop failed: #{e}"
|
|
614
|
+
end
|
|
615
|
+
end
|
|
611
616
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
drop_statement ||= "DROP SCHEMA #{name} CASCADE"
|
|
630
|
-
Array(drop_statement).each do |stmt|
|
|
631
|
-
::ActiveRecord::Base.connection.execute(stmt)
|
|
632
|
-
end
|
|
633
|
-
ensure
|
|
634
|
-
conn.raw_connection.set_notice_processor(&old_proc) if old_proc
|
|
635
|
-
end
|
|
636
|
-
end
|
|
617
|
+
#
|
|
618
|
+
# Drops a specific database/schema from the currently active connection
|
|
619
|
+
#
|
|
620
|
+
def self.drop_database(name)
|
|
621
|
+
sharding_config = Switchman.config || {}
|
|
622
|
+
drop_statement = sharding_config["postgresql"]&.[](:drop_statement)
|
|
623
|
+
drop_statement ||= sharding_config[:drop_statement]
|
|
624
|
+
drop_statement = Array(drop_statement).map { |statement| statement.gsub("%{name}", name) } if drop_statement
|
|
625
|
+
|
|
626
|
+
::GuardRail.activate(:deploy) do
|
|
627
|
+
# Shut up, Postgres!
|
|
628
|
+
conn = ::ActiveRecord::Base.connection
|
|
629
|
+
old_proc = conn.raw_connection.set_notice_processor {}
|
|
630
|
+
begin
|
|
631
|
+
drop_statement ||= "DROP SCHEMA #{name} CASCADE"
|
|
632
|
+
Array(drop_statement).each do |stmt|
|
|
633
|
+
::ActiveRecord::Base.connection.execute(stmt)
|
|
637
634
|
end
|
|
635
|
+
ensure
|
|
636
|
+
conn.raw_connection.set_notice_processor(&old_proc) if old_proc
|
|
638
637
|
end
|
|
639
|
-
rescue
|
|
640
|
-
logger.info "Drop failed: #{$!}"
|
|
641
638
|
end
|
|
642
639
|
end
|
|
643
640
|
|
|
@@ -656,7 +653,7 @@ module Switchman
|
|
|
656
653
|
end
|
|
657
654
|
|
|
658
655
|
def destroy
|
|
659
|
-
raise(
|
|
656
|
+
raise("Cannot destroy the default shard") if default?
|
|
660
657
|
|
|
661
658
|
super
|
|
662
659
|
end
|
|
@@ -665,8 +662,8 @@ module Switchman
|
|
|
665
662
|
|
|
666
663
|
def clear_cache
|
|
667
664
|
Shard.default.activate do
|
|
668
|
-
Switchman.cache.delete([
|
|
669
|
-
Switchman.cache.delete(
|
|
665
|
+
Switchman.cache.delete(["shard", id].join("/"))
|
|
666
|
+
Switchman.cache.delete("default_shard") if default?
|
|
670
667
|
end
|
|
671
668
|
self.class.clear_cache
|
|
672
669
|
end
|
|
@@ -16,7 +16,11 @@ module Switchman
|
|
|
16
16
|
payload[:shard] = {
|
|
17
17
|
database_server_id: shard.database_server.id,
|
|
18
18
|
id: shard.id,
|
|
19
|
-
env:
|
|
19
|
+
env: if ::Rails.version < "7.0"
|
|
20
|
+
@shard_host.pool.connection_klass&.current_role
|
|
21
|
+
else
|
|
22
|
+
@shard_host.pool.connection_class&.current_role
|
|
23
|
+
end
|
|
20
24
|
}
|
|
21
25
|
end
|
|
22
26
|
super name, payload
|
|
@@ -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
|
|
@@ -6,16 +6,17 @@ module Switchman
|
|
|
6
6
|
super
|
|
7
7
|
# These seem to get themselves into a bad state if we try to lookup shards while processing
|
|
8
8
|
return if is_a?(IO::EAGAINWaitReadable)
|
|
9
|
+
|
|
9
10
|
return if Thread.current[:switchman_error_handler]
|
|
10
11
|
|
|
11
12
|
begin
|
|
12
13
|
Thread.current[:switchman_error_handler] = true
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
19
20
|
ensure
|
|
20
21
|
Thread.current[:switchman_error_handler] = nil
|
|
21
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
|
|
@@ -65,7 +65,7 @@ module Switchman
|
|
|
65
65
|
if server == Shard.default.database_server
|
|
66
66
|
server.shards.where(name: 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