switchman 3.0.5 → 4.0.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 +16 -15
- data/db/migrate/20180828183945_add_default_shard_index.rb +2 -2
- data/db/migrate/20180828192111_add_timestamps_to_shards.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 -15
- data/lib/switchman/active_record/associations.rb +331 -0
- data/lib/switchman/active_record/attribute_methods.rb +182 -77
- data/lib/switchman/active_record/base.rb +249 -46
- data/lib/switchman/active_record/calculations.rb +98 -44
- data/lib/switchman/active_record/connection_handler.rb +18 -0
- data/lib/switchman/active_record/connection_pool.rb +27 -28
- data/lib/switchman/active_record/database_configurations.rb +44 -6
- 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 -5
- 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 +37 -2
- data/lib/switchman/active_record/postgresql_adapter.rb +12 -11
- 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 +202 -136
- data/lib/switchman/active_record/reflection.rb +1 -1
- data/lib/switchman/active_record/relation.rb +40 -28
- 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 +53 -0
- data/lib/switchman/active_support/cache.rb +25 -4
- data/lib/switchman/arel.rb +45 -7
- data/lib/switchman/call_super.rb +2 -2
- data/lib/switchman/database_server.rb +123 -79
- data/lib/switchman/default_shard.rb +14 -5
- data/lib/switchman/engine.rb +79 -131
- data/lib/switchman/environment.rb +2 -2
- data/lib/switchman/errors.rb +17 -2
- data/lib/switchman/guard_rail/relation.rb +7 -10
- data/lib/switchman/guard_rail.rb +5 -0
- data/lib/switchman/parallel.rb +68 -0
- data/lib/switchman/r_spec_helper.rb +17 -28
- data/lib/switchman/rails.rb +1 -4
- data/{app/models → lib}/switchman/shard.rb +226 -241
- data/lib/switchman/sharded_instrumenter.rb +3 -3
- data/lib/switchman/shared_schema_cache.rb +11 -0
- data/lib/switchman/standard_error.rb +15 -12
- 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 +101 -54
- metadata +50 -58
- 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
|
|
@@ -39,15 +75,17 @@ module Switchman
|
|
|
39
75
|
# the first time we need a dummy dummy for re-entrancy to avoid looping on ourselves
|
|
40
76
|
@default ||= default
|
|
41
77
|
|
|
42
|
-
# Now find the actual record, if it exists
|
|
78
|
+
# Now find the actual record, if it exists
|
|
43
79
|
@default = begin
|
|
44
|
-
find_cached(
|
|
80
|
+
find_cached("default_shard") { Shard.where(default: true).take } || default
|
|
45
81
|
rescue
|
|
46
82
|
default
|
|
47
83
|
end
|
|
48
84
|
|
|
49
85
|
# make sure this is not erroneously cached
|
|
50
|
-
|
|
86
|
+
if @default.database_server.instance_variable_defined?(:@primary_shard)
|
|
87
|
+
@default.database_server.remove_instance_variable(:@primary_shard)
|
|
88
|
+
end
|
|
51
89
|
|
|
52
90
|
# and finally, check for cached references to the default shard on the existing connection
|
|
53
91
|
sharded_models.each do |klass|
|
|
@@ -59,50 +97,61 @@ module Switchman
|
|
|
59
97
|
|
|
60
98
|
def current(klass = ::ActiveRecord::Base)
|
|
61
99
|
klass ||= ::ActiveRecord::Base
|
|
62
|
-
klass.
|
|
100
|
+
klass.current_switchman_shard
|
|
63
101
|
end
|
|
64
102
|
|
|
65
103
|
def activate(shards)
|
|
66
104
|
activated_classes = activate!(shards)
|
|
67
105
|
yield
|
|
68
106
|
ensure
|
|
69
|
-
activated_classes
|
|
70
|
-
klass.connection_pool.shard_stack.pop
|
|
107
|
+
activated_classes&.each do |klass|
|
|
71
108
|
klass.connected_to_stack.pop
|
|
72
109
|
end
|
|
73
110
|
end
|
|
74
111
|
|
|
75
112
|
def activate!(shards)
|
|
76
|
-
activated_classes =
|
|
113
|
+
activated_classes = nil
|
|
77
114
|
shards.each do |klass, shard|
|
|
78
115
|
next if klass == UnshardedRecord
|
|
79
116
|
|
|
80
117
|
next unless klass.current_shard != shard.database_server.id.to_sym ||
|
|
81
|
-
klass.
|
|
118
|
+
klass.current_switchman_shard != shard
|
|
82
119
|
|
|
83
|
-
activated_classes << klass
|
|
84
|
-
klass.connected_to_stack << { shard: shard.database_server.id.to_sym,
|
|
85
|
-
|
|
120
|
+
(activated_classes ||= []) << klass
|
|
121
|
+
klass.connected_to_stack << { shard: shard.database_server.id.to_sym,
|
|
122
|
+
klasses: [klass],
|
|
123
|
+
switchman_shard: shard }
|
|
86
124
|
end
|
|
87
125
|
activated_classes
|
|
88
126
|
end
|
|
89
127
|
|
|
128
|
+
def active_shards
|
|
129
|
+
sharded_models.filter_map do |klass|
|
|
130
|
+
[klass, current(klass)]
|
|
131
|
+
end.to_h
|
|
132
|
+
end
|
|
133
|
+
|
|
90
134
|
def lookup(id)
|
|
91
135
|
id_i = id.to_i
|
|
92
|
-
return current if id_i == current.id || id ==
|
|
93
|
-
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"
|
|
94
138
|
|
|
95
139
|
id = id_i
|
|
96
140
|
raise ArgumentError if id.zero?
|
|
97
141
|
|
|
98
142
|
unless cached_shards.key?(id)
|
|
99
143
|
cached_shards[id] = Shard.default.activate do
|
|
100
|
-
find_cached([
|
|
144
|
+
find_cached(["shard", id]) { find_by(id: id) }
|
|
101
145
|
end
|
|
102
146
|
end
|
|
103
147
|
cached_shards[id]
|
|
104
148
|
end
|
|
105
149
|
|
|
150
|
+
def preload_cache
|
|
151
|
+
cached_shards.reverse_merge!(active_shards.values.index_by(&:id))
|
|
152
|
+
cached_shards.reverse_merge!(all.index_by(&:id))
|
|
153
|
+
end
|
|
154
|
+
|
|
106
155
|
def clear_cache
|
|
107
156
|
cached_shards.clear
|
|
108
157
|
end
|
|
@@ -111,14 +160,13 @@ module Switchman
|
|
|
111
160
|
#
|
|
112
161
|
# * +shards+ - an array or relation of Shards to iterate over
|
|
113
162
|
# * +classes+ - an array of classes to activate
|
|
114
|
-
# parallel: - true/false to execute in parallel, or
|
|
115
|
-
# sub-processes
|
|
116
|
-
#
|
|
117
|
-
# because errors are not raised, and you cannot get results back
|
|
118
|
-
# max_procs: - only run this many parallel processes at a time
|
|
163
|
+
# parallel: - true/false to execute in parallel, or an integer of how many
|
|
164
|
+
# sub-processes. Note that parallel invocation currently uses
|
|
165
|
+
# forking.
|
|
119
166
|
# exception: - :ignore, :raise, :defer (wait until the end and raise the first
|
|
120
167
|
# error), or a proc
|
|
121
|
-
|
|
168
|
+
# output: - :simple, :decorated (with database_server_id:shard_name), custom lambda transformer
|
|
169
|
+
def with_each_shard(*args, parallel: false, exception: :raise, output: :simple)
|
|
122
170
|
raise ArgumentError, "wrong number of arguments (#{args.length} for 0...2)" if args.length > 2
|
|
123
171
|
|
|
124
172
|
return Array.wrap(yield) unless default.is_a?(Shard)
|
|
@@ -133,190 +181,98 @@ module Switchman
|
|
|
133
181
|
scope, classes = args
|
|
134
182
|
end
|
|
135
183
|
|
|
136
|
-
|
|
184
|
+
output = if output == :decorated
|
|
185
|
+
->(arg) { "#{Shard.current.description}: #{arg}" }
|
|
186
|
+
elsif output == :simple
|
|
187
|
+
nil
|
|
188
|
+
else
|
|
189
|
+
output
|
|
190
|
+
end
|
|
191
|
+
parallel = [Environment.cpu_count || 2, 2].min if parallel == true
|
|
137
192
|
parallel = 0 if parallel == false || parallel.nil?
|
|
138
193
|
|
|
139
194
|
scope ||= Shard.all
|
|
140
|
-
|
|
195
|
+
if ::ActiveRecord::Relation === scope && scope.order_values.empty?
|
|
196
|
+
scope = scope.order(::Arel.sql("database_server_id IS NOT NULL, database_server_id, id"))
|
|
197
|
+
end
|
|
141
198
|
|
|
142
|
-
if parallel
|
|
143
|
-
max_procs = determine_max_procs(max_procs, parallel)
|
|
199
|
+
if parallel > 1
|
|
144
200
|
if ::ActiveRecord::Relation === scope
|
|
145
201
|
# still need a post-uniq, cause the default database server could be NULL or Rails.env in the db
|
|
146
|
-
database_servers = scope.reorder(
|
|
147
|
-
|
|
202
|
+
database_servers = scope.reorder("database_server_id").select(:database_server_id).distinct
|
|
203
|
+
.filter_map(&:database_server).uniq
|
|
148
204
|
# nothing to do
|
|
149
205
|
return if database_servers.count.zero?
|
|
150
206
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
server_scope = server.shards.merge(scope)
|
|
155
|
-
if parallel == 1
|
|
156
|
-
subscopes = [server_scope]
|
|
157
|
-
else
|
|
158
|
-
subscopes = []
|
|
159
|
-
total = server_scope.count
|
|
160
|
-
ranges = []
|
|
161
|
-
server_scope.find_ids_in_ranges(batch_size: (total.to_f / parallel).ceil) do |min, max|
|
|
162
|
-
ranges << [min, max]
|
|
163
|
-
end
|
|
164
|
-
# create a half-open range on the last one
|
|
165
|
-
ranges.last[1] = nil
|
|
166
|
-
ranges.each do |min, max|
|
|
167
|
-
subscope = server_scope.where('id>=?', min)
|
|
168
|
-
subscope = subscope.where('id<=?', max) if max
|
|
169
|
-
subscopes << subscope
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
[server, subscopes]
|
|
173
|
-
end.to_h
|
|
207
|
+
scopes = database_servers.to_h do |server|
|
|
208
|
+
[server, scope.merge(server.shards)]
|
|
209
|
+
end
|
|
174
210
|
else
|
|
175
211
|
scopes = scope.group_by(&:database_server)
|
|
176
|
-
if parallel > 1
|
|
177
|
-
parallel = [(max_procs.to_f / scopes.count).ceil, parallel].min if max_procs
|
|
178
|
-
scopes = scopes.map do |(server, shards)|
|
|
179
|
-
[server, shards.in_groups(parallel, false).compact]
|
|
180
|
-
end.to_h
|
|
181
|
-
else
|
|
182
|
-
scopes = scopes.map { |(server, shards)| [server, [shards]] }.to_h
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
exception_pipes = []
|
|
187
|
-
pids = []
|
|
188
|
-
out_fds = []
|
|
189
|
-
err_fds = []
|
|
190
|
-
pid_to_name_map = {}
|
|
191
|
-
fd_to_name_map = {}
|
|
192
|
-
errors = []
|
|
193
|
-
|
|
194
|
-
wait_for_output = lambda do
|
|
195
|
-
ready, = IO.select(out_fds + err_fds)
|
|
196
|
-
ready.each do |fd|
|
|
197
|
-
if fd.eof?
|
|
198
|
-
fd.close
|
|
199
|
-
out_fds.delete(fd)
|
|
200
|
-
err_fds.delete(fd)
|
|
201
|
-
next
|
|
202
|
-
end
|
|
203
|
-
line = fd.readline
|
|
204
|
-
puts "#{fd_to_name_map[fd]}: #{line}"
|
|
205
|
-
end
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
# only one process; don't bother forking
|
|
209
|
-
if scopes.length == 1 && parallel == 1
|
|
210
|
-
return with_each_shard(scopes.first.last.first, classes, exception: exception,
|
|
211
|
-
&block)
|
|
212
212
|
end
|
|
213
213
|
|
|
214
214
|
# clear connections prior to forking (no more queries will be executed in the parent,
|
|
215
215
|
# and we want them gone so that we don't accidentally use them post-fork doing something
|
|
216
216
|
# silly like dealloc'ing prepared statements)
|
|
217
|
-
::
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
name = if subscopes.length > 1
|
|
222
|
-
"#{server.id} #{idx + 1}"
|
|
223
|
-
else
|
|
224
|
-
server.id
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
exception_pipe = IO.pipe
|
|
228
|
-
exception_pipes << exception_pipe
|
|
229
|
-
pid, io_in, io_out, io_err = Open4.pfork4(lambda do
|
|
230
|
-
Switchman.config[:on_fork_proc]&.call
|
|
231
|
-
|
|
232
|
-
# set a pretty name for the process title, up to 128 characters
|
|
233
|
-
# (we don't actually know the limit, depending on how the process
|
|
234
|
-
# was started)
|
|
235
|
-
# first, simplify the binary name by stripping directories,
|
|
236
|
-
# then truncate arguments as necessary
|
|
237
|
-
bin = File.basename($0) # Process.argv0 doesn't work on Ruby 2.5 (https://bugs.ruby-lang.org/issues/15887)
|
|
238
|
-
max_length = 128 - bin.length - name.length - 3
|
|
239
|
-
args = ARGV.join(' ')
|
|
240
|
-
args = args[0..max_length] if max_length >= 0
|
|
241
|
-
new_title = [bin, args, name].join(' ')
|
|
242
|
-
Process.setproctitle(new_title)
|
|
243
|
-
|
|
244
|
-
with_each_shard(subscope, classes, exception: exception, &block)
|
|
245
|
-
exception_pipe.last.close
|
|
246
|
-
rescue => e
|
|
247
|
-
begin
|
|
248
|
-
dumped = Marshal.dump(e)
|
|
249
|
-
rescue
|
|
250
|
-
# couldn't dump the exception; create a copy with just
|
|
251
|
-
# the message and the backtrace
|
|
252
|
-
e2 = e.class.new(e.message)
|
|
253
|
-
e2.set_backtrace(e.backtrace)
|
|
254
|
-
e2.instance_variable_set(:@active_shards, e.instance_variable_get(:@active_shards))
|
|
255
|
-
dumped = Marshal.dump(e2)
|
|
256
|
-
end
|
|
257
|
-
exception_pipe.last.set_encoding(dumped.encoding)
|
|
258
|
-
exception_pipe.last.write(dumped)
|
|
259
|
-
exception_pipe.last.flush
|
|
260
|
-
exception_pipe.last.close
|
|
261
|
-
exit! 1
|
|
262
|
-
end)
|
|
263
|
-
exception_pipe.last.close
|
|
264
|
-
pids << pid
|
|
265
|
-
io_in.close # don't care about writing to stdin
|
|
266
|
-
out_fds << io_out
|
|
267
|
-
err_fds << io_err
|
|
268
|
-
pid_to_name_map[pid] = name
|
|
269
|
-
fd_to_name_map[io_out] = name
|
|
270
|
-
fd_to_name_map[io_err] = name
|
|
271
|
-
|
|
272
|
-
while max_procs && pids.count >= max_procs
|
|
273
|
-
while max_procs && out_fds.count >= max_procs
|
|
274
|
-
# wait for output if we've hit the max_procs limit
|
|
275
|
-
wait_for_output.call
|
|
276
|
-
end
|
|
277
|
-
# we've gotten all the output from one fd so wait for its child process to exit
|
|
278
|
-
found_pid, status = Process.wait2
|
|
279
|
-
pids.delete(found_pid)
|
|
280
|
-
errors << pid_to_name_map[found_pid] if status.exitstatus != 0
|
|
281
|
-
end
|
|
282
|
-
end
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
wait_for_output.call while out_fds.any? || err_fds.any?
|
|
286
|
-
pids.each do |pid|
|
|
287
|
-
_, status = Process.waitpid2(pid)
|
|
288
|
-
errors << pid_to_name_map[pid] if status.exitstatus != 0
|
|
217
|
+
if ::Rails.version < "7.1"
|
|
218
|
+
::ActiveRecord::Base.clear_all_connections!(nil)
|
|
219
|
+
else
|
|
220
|
+
::ActiveRecord::Base.connection_handler.clear_all_connections!(:all)
|
|
289
221
|
end
|
|
290
222
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
223
|
+
parent_process_name = sanitized_process_title
|
|
224
|
+
ret = ::Parallel.map(scopes, in_processes: (scopes.length > 1) ? parallel : 0) do |server, subscope|
|
|
225
|
+
name = server.id
|
|
226
|
+
last_description = name
|
|
227
|
+
|
|
228
|
+
begin
|
|
229
|
+
max_length = 128 - name.length - 3
|
|
230
|
+
short_parent_name = parent_process_name[0..max_length] if max_length >= 0
|
|
231
|
+
new_title = [short_parent_name, name].join(" ")
|
|
232
|
+
Process.setproctitle(new_title)
|
|
233
|
+
Switchman.config[:on_fork_proc]&.call
|
|
234
|
+
with_each_shard(subscope,
|
|
235
|
+
classes,
|
|
236
|
+
exception: exception,
|
|
237
|
+
output: output || :decorated) do
|
|
238
|
+
last_description = Shard.current.description
|
|
239
|
+
Parallel::ResultWrapper.new(yield)
|
|
240
|
+
end
|
|
241
|
+
rescue => e
|
|
242
|
+
logger.error e.full_message
|
|
243
|
+
Parallel::QuietExceptionWrapper.new(last_description, ::Parallel::ExceptionWrapper.new(e))
|
|
244
|
+
end
|
|
245
|
+
end.flatten
|
|
301
246
|
|
|
247
|
+
errors = ret.select { |val| val.is_a?(Parallel::QuietExceptionWrapper) }
|
|
302
248
|
unless errors.empty?
|
|
303
|
-
raise
|
|
304
|
-
|
|
249
|
+
raise errors.first.exception if errors.length == 1
|
|
250
|
+
|
|
251
|
+
errors_desc = errors.map(&:name).sort.join(", ")
|
|
252
|
+
raise Errors::ParallelShardExecError,
|
|
253
|
+
"The following database server(s) did not finish processing cleanly: #{errors_desc}",
|
|
254
|
+
cause: errors.first.exception
|
|
305
255
|
end
|
|
306
256
|
|
|
307
|
-
return
|
|
257
|
+
return ret.map(&:result)
|
|
308
258
|
end
|
|
309
259
|
|
|
310
260
|
classes ||= []
|
|
311
261
|
|
|
312
|
-
previous_shard = nil
|
|
313
262
|
result = []
|
|
314
263
|
ex = nil
|
|
264
|
+
old_stdout = $stdout
|
|
265
|
+
old_stderr = $stderr
|
|
315
266
|
scope.each do |shard|
|
|
316
267
|
# shard references a database server that isn't configured in this environment
|
|
317
268
|
next unless shard.database_server
|
|
318
269
|
|
|
319
270
|
shard.activate(*classes) do
|
|
271
|
+
if output
|
|
272
|
+
$stdout = Parallel::TransformingIO.new(output, $stdout)
|
|
273
|
+
$stderr = Parallel::TransformingIO.new(output, $stderr)
|
|
274
|
+
end
|
|
275
|
+
|
|
320
276
|
result.concat Array.wrap(yield)
|
|
321
277
|
rescue
|
|
322
278
|
case exception
|
|
@@ -330,8 +286,10 @@ module Switchman
|
|
|
330
286
|
else
|
|
331
287
|
raise
|
|
332
288
|
end
|
|
289
|
+
ensure
|
|
290
|
+
$stdout = old_stdout
|
|
291
|
+
$stderr = old_stderr
|
|
333
292
|
end
|
|
334
|
-
previous_shard = shard
|
|
335
293
|
end
|
|
336
294
|
raise ex if ex
|
|
337
295
|
|
|
@@ -355,7 +313,7 @@ module Switchman
|
|
|
355
313
|
else
|
|
356
314
|
shard = partition_object.shard
|
|
357
315
|
end
|
|
358
|
-
when Integer,
|
|
316
|
+
when Integer, /\A\d+\Z/, /\A(\d+)~(\d+)\Z/
|
|
359
317
|
local_id, shard = Shard.local_id_for(partition_object)
|
|
360
318
|
local_id ||= partition_object
|
|
361
319
|
object = local_id unless partition_proc
|
|
@@ -397,14 +355,14 @@ module Switchman
|
|
|
397
355
|
case any_id
|
|
398
356
|
when ::ActiveRecord::Base
|
|
399
357
|
any_id.id
|
|
400
|
-
when
|
|
358
|
+
when /\A(\d+)~(-?\d+)\Z/
|
|
401
359
|
local_id = $2.to_i
|
|
402
360
|
signed_id_operation(local_id) do |id|
|
|
403
361
|
return nil if id > IDS_PER_SHARD
|
|
404
362
|
|
|
405
|
-
$1.to_i * IDS_PER_SHARD + id
|
|
363
|
+
($1.to_i * IDS_PER_SHARD) + id
|
|
406
364
|
end
|
|
407
|
-
when Integer,
|
|
365
|
+
when Integer, /\A-?\d+\Z/
|
|
408
366
|
any_id.to_i
|
|
409
367
|
end
|
|
410
368
|
end
|
|
@@ -480,32 +438,15 @@ module Switchman
|
|
|
480
438
|
shard || source_shard || Shard.current
|
|
481
439
|
end
|
|
482
440
|
|
|
483
|
-
# given the provided option, determines whether we need to (and whether
|
|
484
|
-
# it's possible) to determine a reasonable default.
|
|
485
|
-
def determine_max_procs(max_procs_input, parallel_input = 2)
|
|
486
|
-
max_procs = nil
|
|
487
|
-
if max_procs_input
|
|
488
|
-
max_procs = max_procs_input.to_i
|
|
489
|
-
max_procs = nil if max_procs.zero?
|
|
490
|
-
else
|
|
491
|
-
return 1 if parallel_input.nil? || parallel_input < 1
|
|
492
|
-
|
|
493
|
-
cpus = Environment.cpu_count
|
|
494
|
-
max_procs = cpus * parallel_input if cpus&.positive?
|
|
495
|
-
end
|
|
496
|
-
|
|
497
|
-
max_procs
|
|
498
|
-
end
|
|
499
|
-
|
|
500
441
|
private
|
|
501
442
|
|
|
502
443
|
def add_sharded_model(klass)
|
|
503
444
|
@sharded_models = (sharded_models + [klass]).freeze
|
|
504
|
-
|
|
445
|
+
configure_connects_to
|
|
505
446
|
end
|
|
506
447
|
|
|
507
|
-
def
|
|
508
|
-
full_connects_to_hash = DatabaseServer.
|
|
448
|
+
def configure_connects_to
|
|
449
|
+
full_connects_to_hash = DatabaseServer.to_h { |db| [db.id.to_sym, db.connects_to_hash] }
|
|
509
450
|
sharded_models.each do |klass|
|
|
510
451
|
connects_to_hash = full_connects_to_hash.deep_dup
|
|
511
452
|
if klass == UnshardedRecord
|
|
@@ -524,7 +465,10 @@ module Switchman
|
|
|
524
465
|
end
|
|
525
466
|
end
|
|
526
467
|
|
|
468
|
+
# this resets the default shard on rails 7.1+, but we want to preserve it
|
|
469
|
+
shard_was = klass.default_shard
|
|
527
470
|
klass.connects_to shards: connects_to_hash
|
|
471
|
+
klass.default_shard = shard_was
|
|
528
472
|
end
|
|
529
473
|
end
|
|
530
474
|
|
|
@@ -558,8 +502,50 @@ module Switchman
|
|
|
558
502
|
shard = nil unless shard.database_server
|
|
559
503
|
shard
|
|
560
504
|
end
|
|
505
|
+
|
|
506
|
+
# Determines the name of the current process, including arguments, but stripping
|
|
507
|
+
# any shebang from the invoked script, and any additional path info from the
|
|
508
|
+
# executable.
|
|
509
|
+
#
|
|
510
|
+
# @return [String]
|
|
511
|
+
def sanitized_process_title
|
|
512
|
+
# get the effective process name from `ps`; this will include any changes
|
|
513
|
+
# from Process.setproctitle _or_ assigning to $0.
|
|
514
|
+
parent_process_name = `ps -ocommand= -p#{Process.pid}`.strip
|
|
515
|
+
# Effective process titles may be shorter than the actual
|
|
516
|
+
# command; truncate our ARGV[0] so that they are comparable
|
|
517
|
+
# for the next step
|
|
518
|
+
argv0 = if parent_process_name.length < Process.argv0.length
|
|
519
|
+
Process.argv0[0..parent_process_name.length]
|
|
520
|
+
else
|
|
521
|
+
Process.argv0
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# when running via a shebang, the `ps` output will include the shebang
|
|
525
|
+
# (i.e. it will be "ruby bin/rails c"); attempt to strip it off.
|
|
526
|
+
# Note that argv0 in this case will _only_ be `bin/rails` (no shebang,
|
|
527
|
+
# no arguments). We want to preserve the arguments we got from `ps`
|
|
528
|
+
if (index = parent_process_name.index(argv0))
|
|
529
|
+
parent_process_name.slice!(0...index)
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# remove directories from the main executable to make more room
|
|
533
|
+
# for additional info
|
|
534
|
+
argv = parent_process_name.shellsplit
|
|
535
|
+
argv[0] = File.basename(argv[0])
|
|
536
|
+
argv.shelljoin
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
# @return [Array<String>] the list of database servers that are in the
|
|
540
|
+
# config, but don't have any shards on them
|
|
541
|
+
def non_existent_database_servers
|
|
542
|
+
@non_existent_database_servers ||=
|
|
543
|
+
Shard.distinct.pluck(:database_server_id).compact - DatabaseServer.all.map(&:id)
|
|
544
|
+
end
|
|
561
545
|
end
|
|
562
546
|
|
|
547
|
+
delegate :region, :in_region?, :in_current_region?, to: :database_server
|
|
548
|
+
|
|
563
549
|
def name
|
|
564
550
|
unless instance_variable_defined?(:@name)
|
|
565
551
|
# protect against re-entrancy
|
|
@@ -588,7 +574,7 @@ module Switchman
|
|
|
588
574
|
end
|
|
589
575
|
|
|
590
576
|
def description
|
|
591
|
-
[database_server.id, name].compact.join(
|
|
577
|
+
[database_server.id, name].compact.join(":")
|
|
592
578
|
end
|
|
593
579
|
|
|
594
580
|
# Shards are always on the default shard
|
|
@@ -596,6 +582,14 @@ module Switchman
|
|
|
596
582
|
Shard.default
|
|
597
583
|
end
|
|
598
584
|
|
|
585
|
+
def original_id
|
|
586
|
+
id
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
def original_id_value
|
|
590
|
+
id
|
|
591
|
+
end
|
|
592
|
+
|
|
599
593
|
def activate(*classes, &block)
|
|
600
594
|
shards = hashify_classes(classes)
|
|
601
595
|
Shard.activate(shards, &block)
|
|
@@ -618,48 +612,39 @@ module Switchman
|
|
|
618
612
|
end
|
|
619
613
|
|
|
620
614
|
def drop_database
|
|
621
|
-
raise
|
|
615
|
+
raise "Cannot drop the database of the default shard" if default?
|
|
622
616
|
return unless read_attribute(:name)
|
|
623
617
|
|
|
624
618
|
begin
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
drop_statement = sharding_config[adapter]&.[](:drop_statement)
|
|
628
|
-
drop_statement ||= sharding_config[:drop_statement]
|
|
629
|
-
if drop_statement
|
|
630
|
-
drop_statement = Array(drop_statement).dup.
|
|
631
|
-
map { |statement| statement.gsub('%{name}', name) }
|
|
619
|
+
activate do
|
|
620
|
+
self.class.drop_database(name)
|
|
632
621
|
end
|
|
622
|
+
rescue ::ActiveRecord::StatementInvalid => e
|
|
623
|
+
logger.error "Drop failed: #{e}"
|
|
624
|
+
end
|
|
625
|
+
end
|
|
633
626
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
drop_statement ||= "DROP SCHEMA #{name} CASCADE"
|
|
652
|
-
Array(drop_statement).each do |stmt|
|
|
653
|
-
::ActiveRecord::Base.connection.execute(stmt)
|
|
654
|
-
end
|
|
655
|
-
ensure
|
|
656
|
-
conn.raw_connection.set_notice_processor(&old_proc) if old_proc
|
|
657
|
-
end
|
|
658
|
-
end
|
|
627
|
+
#
|
|
628
|
+
# Drops a specific database/schema from the currently active connection
|
|
629
|
+
#
|
|
630
|
+
def self.drop_database(name)
|
|
631
|
+
sharding_config = Switchman.config || {}
|
|
632
|
+
drop_statement = sharding_config["postgresql"]&.[](:drop_statement)
|
|
633
|
+
drop_statement ||= sharding_config[:drop_statement]
|
|
634
|
+
drop_statement = Array(drop_statement).map { |statement| statement.gsub("%{name}", name) } if drop_statement
|
|
635
|
+
|
|
636
|
+
::GuardRail.activate(:deploy) do
|
|
637
|
+
# Shut up, Postgres!
|
|
638
|
+
conn = ::ActiveRecord::Base.connection
|
|
639
|
+
old_proc = conn.raw_connection.set_notice_processor {}
|
|
640
|
+
begin
|
|
641
|
+
drop_statement ||= "DROP SCHEMA #{name} CASCADE"
|
|
642
|
+
Array(drop_statement).each do |stmt|
|
|
643
|
+
::ActiveRecord::Base.connection.execute(stmt)
|
|
659
644
|
end
|
|
645
|
+
ensure
|
|
646
|
+
conn.raw_connection.set_notice_processor(&old_proc) if old_proc
|
|
660
647
|
end
|
|
661
|
-
rescue
|
|
662
|
-
logger.info "Drop failed: #{$!}"
|
|
663
648
|
end
|
|
664
649
|
end
|
|
665
650
|
|
|
@@ -668,7 +653,7 @@ module Switchman
|
|
|
668
653
|
return nil unless local_id
|
|
669
654
|
|
|
670
655
|
self.class.signed_id_operation(local_id) do |abs_id|
|
|
671
|
-
abs_id + id * IDS_PER_SHARD
|
|
656
|
+
abs_id + (id * IDS_PER_SHARD)
|
|
672
657
|
end
|
|
673
658
|
end
|
|
674
659
|
|
|
@@ -678,7 +663,7 @@ module Switchman
|
|
|
678
663
|
end
|
|
679
664
|
|
|
680
665
|
def destroy
|
|
681
|
-
raise(
|
|
666
|
+
raise("Cannot destroy the default shard") if default?
|
|
682
667
|
|
|
683
668
|
super
|
|
684
669
|
end
|
|
@@ -687,8 +672,8 @@ module Switchman
|
|
|
687
672
|
|
|
688
673
|
def clear_cache
|
|
689
674
|
Shard.default.activate do
|
|
690
|
-
Switchman.cache.delete([
|
|
691
|
-
Switchman.cache.delete(
|
|
675
|
+
Switchman.cache.delete(["shard", id].join("/"))
|
|
676
|
+
Switchman.cache.delete("default_shard") if default?
|
|
692
677
|
end
|
|
693
678
|
self.class.clear_cache
|
|
694
679
|
end
|