switchman 3.0.2 → 4.2.5
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 +11 -18
- data/lib/switchman/active_record/associations.rb +315 -0
- data/lib/switchman/active_record/attribute_methods.rb +191 -79
- data/lib/switchman/active_record/base.rb +204 -50
- data/lib/switchman/active_record/calculations.rb +92 -49
- data/lib/switchman/active_record/connection_handler.rb +18 -0
- data/lib/switchman/active_record/connection_pool.rb +47 -34
- data/lib/switchman/active_record/database_configurations.rb +32 -6
- 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 -14
- 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 +39 -20
- 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 +251 -140
- data/lib/switchman/active_record/reflection.rb +10 -3
- data/lib/switchman/active_record/relation.rb +110 -35
- 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 +89 -0
- data/lib/switchman/active_support/cache.rb +25 -4
- data/lib/switchman/arel.rb +20 -7
- data/lib/switchman/call_super.rb +2 -2
- data/lib/switchman/database_server.rb +123 -83
- data/lib/switchman/default_shard.rb +14 -5
- data/lib/switchman/engine.rb +85 -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 +229 -246
- data/lib/switchman/sharded_instrumenter.rb +9 -3
- data/lib/switchman/shared_schema_cache.rb +11 -0
- data/lib/switchman/standard_error.rb +15 -12
- data/lib/switchman/test_helper.rb +3 -3
- 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 +34 -176
- data/lib/switchman/active_record/association.rb +0 -206
- data/lib/switchman/open4.rb +0 -80
|
@@ -1,23 +1,61 @@
|
|
|
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.
|
|
11
6
|
IDS_PER_SHARD = 10_000_000_000_000
|
|
12
7
|
|
|
8
|
+
# rubocop:disable Style/SymbolProc -- transforming to a lambda produces "no receiver given"
|
|
13
9
|
# only allow one default
|
|
14
10
|
validates_uniqueness_of :default, if: ->(s) { s.default? }
|
|
11
|
+
# rubocop:enable Style/SymbolProc
|
|
15
12
|
|
|
16
13
|
after_save :clear_cache
|
|
17
14
|
after_destroy :clear_cache
|
|
18
15
|
|
|
19
16
|
scope :primary, -> { where(name: nil).order(:database_server_id, :id).distinct_on(:database_server_id) }
|
|
20
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
|
+
|
|
21
59
|
class << self
|
|
22
60
|
def sharded_models
|
|
23
61
|
@sharded_models ||= [::ActiveRecord::Base, UnshardedRecord].freeze
|
|
@@ -39,15 +77,17 @@ module Switchman
|
|
|
39
77
|
# the first time we need a dummy dummy for re-entrancy to avoid looping on ourselves
|
|
40
78
|
@default ||= default
|
|
41
79
|
|
|
42
|
-
# Now find the actual record, if it exists
|
|
80
|
+
# Now find the actual record, if it exists
|
|
43
81
|
@default = begin
|
|
44
|
-
find_cached(
|
|
82
|
+
find_cached("default_shard") { Shard.where(default: true).take } || default
|
|
45
83
|
rescue
|
|
46
84
|
default
|
|
47
85
|
end
|
|
48
86
|
|
|
49
87
|
# make sure this is not erroneously cached
|
|
50
|
-
|
|
88
|
+
if @default.database_server.instance_variable_defined?(:@primary_shard)
|
|
89
|
+
@default.database_server.remove_instance_variable(:@primary_shard)
|
|
90
|
+
end
|
|
51
91
|
|
|
52
92
|
# and finally, check for cached references to the default shard on the existing connection
|
|
53
93
|
sharded_models.each do |klass|
|
|
@@ -59,50 +99,61 @@ module Switchman
|
|
|
59
99
|
|
|
60
100
|
def current(klass = ::ActiveRecord::Base)
|
|
61
101
|
klass ||= ::ActiveRecord::Base
|
|
62
|
-
klass.
|
|
102
|
+
klass.current_switchman_shard
|
|
63
103
|
end
|
|
64
104
|
|
|
65
105
|
def activate(shards)
|
|
66
106
|
activated_classes = activate!(shards)
|
|
67
107
|
yield
|
|
68
108
|
ensure
|
|
69
|
-
activated_classes
|
|
70
|
-
klass.connection_pool.shard_stack.pop
|
|
109
|
+
activated_classes&.each do |klass|
|
|
71
110
|
klass.connected_to_stack.pop
|
|
72
111
|
end
|
|
73
112
|
end
|
|
74
113
|
|
|
75
114
|
def activate!(shards)
|
|
76
|
-
activated_classes =
|
|
115
|
+
activated_classes = nil
|
|
77
116
|
shards.each do |klass, shard|
|
|
78
117
|
next if klass == UnshardedRecord
|
|
79
118
|
|
|
80
119
|
next unless klass.current_shard != shard.database_server.id.to_sym ||
|
|
81
|
-
klass.
|
|
120
|
+
klass.current_switchman_shard != shard
|
|
82
121
|
|
|
83
|
-
activated_classes << klass
|
|
84
|
-
klass.connected_to_stack << { shard: shard.database_server.id.to_sym,
|
|
85
|
-
|
|
122
|
+
(activated_classes ||= []) << klass
|
|
123
|
+
klass.connected_to_stack << { shard: shard.database_server.id.to_sym,
|
|
124
|
+
klasses: [klass],
|
|
125
|
+
switchman_shard: shard }
|
|
86
126
|
end
|
|
87
127
|
activated_classes
|
|
88
128
|
end
|
|
89
129
|
|
|
130
|
+
def active_shards
|
|
131
|
+
sharded_models.filter_map do |klass|
|
|
132
|
+
[klass, current(klass)]
|
|
133
|
+
end.to_h
|
|
134
|
+
end
|
|
135
|
+
|
|
90
136
|
def lookup(id)
|
|
91
137
|
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 ==
|
|
138
|
+
return current if id_i == current.id || id == "self"
|
|
139
|
+
return default if id_i == default.id || id.nil? || id == "default"
|
|
94
140
|
|
|
95
141
|
id = id_i
|
|
96
142
|
raise ArgumentError if id.zero?
|
|
97
143
|
|
|
98
144
|
unless cached_shards.key?(id)
|
|
99
145
|
cached_shards[id] = Shard.default.activate do
|
|
100
|
-
find_cached([
|
|
146
|
+
find_cached(["shard", id]) { find_by(id:) }
|
|
101
147
|
end
|
|
102
148
|
end
|
|
103
149
|
cached_shards[id]
|
|
104
150
|
end
|
|
105
151
|
|
|
152
|
+
def preload_cache
|
|
153
|
+
cached_shards.reverse_merge!(active_shards.values.index_by(&:id))
|
|
154
|
+
cached_shards.reverse_merge!(all.index_by(&:id))
|
|
155
|
+
end
|
|
156
|
+
|
|
106
157
|
def clear_cache
|
|
107
158
|
cached_shards.clear
|
|
108
159
|
end
|
|
@@ -111,14 +162,13 @@ module Switchman
|
|
|
111
162
|
#
|
|
112
163
|
# * +shards+ - an array or relation of Shards to iterate over
|
|
113
164
|
# * +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
|
|
165
|
+
# parallel: - true/false to execute in parallel, or an integer of how many
|
|
166
|
+
# sub-processes. Note that parallel invocation currently uses
|
|
167
|
+
# forking.
|
|
119
168
|
# exception: - :ignore, :raise, :defer (wait until the end and raise the first
|
|
120
169
|
# error), or a proc
|
|
121
|
-
|
|
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)
|
|
122
172
|
raise ArgumentError, "wrong number of arguments (#{args.length} for 0...2)" if args.length > 2
|
|
123
173
|
|
|
124
174
|
return Array.wrap(yield) unless default.is_a?(Shard)
|
|
@@ -133,190 +183,94 @@ module Switchman
|
|
|
133
183
|
scope, classes = args
|
|
134
184
|
end
|
|
135
185
|
|
|
136
|
-
|
|
186
|
+
output = if output == :decorated
|
|
187
|
+
->(arg) { "#{Shard.current.description}: #{arg}" }
|
|
188
|
+
elsif output == :simple
|
|
189
|
+
nil
|
|
190
|
+
else
|
|
191
|
+
output
|
|
192
|
+
end
|
|
193
|
+
parallel = [Environment.cpu_count || 2, 2].min if parallel == true
|
|
137
194
|
parallel = 0 if parallel == false || parallel.nil?
|
|
138
195
|
|
|
139
196
|
scope ||= Shard.all
|
|
140
|
-
|
|
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
|
|
141
200
|
|
|
142
|
-
if parallel
|
|
143
|
-
max_procs = determine_max_procs(max_procs, parallel)
|
|
201
|
+
if parallel > 1
|
|
144
202
|
if ::ActiveRecord::Relation === scope
|
|
145
203
|
# 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
|
-
|
|
204
|
+
database_servers = scope.reorder("database_server_id").select(:database_server_id).distinct
|
|
205
|
+
.filter_map(&:database_server).uniq
|
|
148
206
|
# nothing to do
|
|
149
|
-
return if database_servers.
|
|
150
|
-
|
|
151
|
-
parallel = [(max_procs.to_f / database_servers.count).ceil, parallel].min if max_procs
|
|
152
|
-
|
|
153
|
-
scopes = database_servers.map do |server|
|
|
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
|
|
174
|
-
else
|
|
175
|
-
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
|
|
207
|
+
return if database_servers.none?
|
|
185
208
|
|
|
186
|
-
|
|
187
|
-
|
|
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}"
|
|
209
|
+
scopes = database_servers.to_h do |server|
|
|
210
|
+
[server, scope.merge(server.shards)]
|
|
205
211
|
end
|
|
206
|
-
|
|
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
|
+
else
|
|
213
|
+
scopes = scope.group_by(&:database_server)
|
|
212
214
|
end
|
|
213
215
|
|
|
214
216
|
# clear connections prior to forking (no more queries will be executed in the parent,
|
|
215
217
|
# and we want them gone so that we don't accidentally use them post-fork doing something
|
|
216
218
|
# silly like dealloc'ing prepared statements)
|
|
217
|
-
::ActiveRecord::Base.clear_all_connections!
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
219
|
+
::ActiveRecord::Base.connection_handler.clear_all_connections!(:all)
|
|
220
|
+
|
|
221
|
+
parent_process_name = sanitized_process_title
|
|
222
|
+
ret = ::Parallel.map(scopes, in_processes: (scopes.length > 1) ? parallel : 0) do |server, subscope|
|
|
223
|
+
name = server.id
|
|
224
|
+
last_description = name
|
|
225
|
+
|
|
226
|
+
begin
|
|
227
|
+
max_length = 128 - name.length - 3
|
|
228
|
+
short_parent_name = parent_process_name[0..max_length] if max_length >= 0
|
|
229
|
+
new_title = [short_parent_name, name].join(" ")
|
|
230
|
+
Process.setproctitle(new_title)
|
|
231
|
+
Switchman.config[:on_fork_proc]&.call
|
|
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)
|
|
281
238
|
end
|
|
239
|
+
rescue => e
|
|
240
|
+
logger.error e.full_message
|
|
241
|
+
Parallel::QuietExceptionWrapper.new(last_description, ::Parallel::ExceptionWrapper.new(e))
|
|
282
242
|
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
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
# check for an exception; we only re-raise the first one
|
|
292
|
-
exception_pipes.each do |exception_pipe|
|
|
293
|
-
serialized_exception = exception_pipe.first.read
|
|
294
|
-
next if serialized_exception.empty?
|
|
295
|
-
|
|
296
|
-
ex = Marshal.load(serialized_exception) # rubocop:disable Security/MarshalLoad
|
|
297
|
-
raise ex
|
|
298
|
-
ensure
|
|
299
|
-
exception_pipe.first.close
|
|
300
|
-
end
|
|
243
|
+
end.flatten
|
|
301
244
|
|
|
245
|
+
errors = ret.select { |val| val.is_a?(Parallel::QuietExceptionWrapper) }
|
|
302
246
|
unless errors.empty?
|
|
303
|
-
raise
|
|
304
|
-
|
|
247
|
+
raise errors.first.exception if errors.length == 1
|
|
248
|
+
|
|
249
|
+
errors_desc = errors.map(&:name).sort.join(", ")
|
|
250
|
+
raise Errors::ParallelShardExecError,
|
|
251
|
+
"The following database server(s) did not finish processing cleanly: #{errors_desc}",
|
|
252
|
+
cause: errors.first.exception
|
|
305
253
|
end
|
|
306
254
|
|
|
307
|
-
return
|
|
255
|
+
return ret.map(&:result)
|
|
308
256
|
end
|
|
309
257
|
|
|
310
258
|
classes ||= []
|
|
311
259
|
|
|
312
|
-
previous_shard = nil
|
|
313
260
|
result = []
|
|
314
261
|
ex = nil
|
|
262
|
+
old_stdout = $stdout
|
|
263
|
+
old_stderr = $stderr
|
|
315
264
|
scope.each do |shard|
|
|
316
265
|
# shard references a database server that isn't configured in this environment
|
|
317
266
|
next unless shard.database_server
|
|
318
267
|
|
|
319
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
|
+
|
|
320
274
|
result.concat Array.wrap(yield)
|
|
321
275
|
rescue
|
|
322
276
|
case exception
|
|
@@ -330,8 +284,10 @@ module Switchman
|
|
|
330
284
|
else
|
|
331
285
|
raise
|
|
332
286
|
end
|
|
287
|
+
ensure
|
|
288
|
+
$stdout = old_stdout
|
|
289
|
+
$stderr = old_stderr
|
|
333
290
|
end
|
|
334
|
-
previous_shard = shard
|
|
335
291
|
end
|
|
336
292
|
raise ex if ex
|
|
337
293
|
|
|
@@ -355,7 +311,7 @@ module Switchman
|
|
|
355
311
|
else
|
|
356
312
|
shard = partition_object.shard
|
|
357
313
|
end
|
|
358
|
-
when Integer,
|
|
314
|
+
when Integer, /\A\d+\Z/, /\A(\d+)~(\d+)\Z/
|
|
359
315
|
local_id, shard = Shard.local_id_for(partition_object)
|
|
360
316
|
local_id ||= partition_object
|
|
361
317
|
object = local_id unless partition_proc
|
|
@@ -397,14 +353,14 @@ module Switchman
|
|
|
397
353
|
case any_id
|
|
398
354
|
when ::ActiveRecord::Base
|
|
399
355
|
any_id.id
|
|
400
|
-
when
|
|
356
|
+
when /\A(\d+)~(-?\d+)\Z/
|
|
401
357
|
local_id = $2.to_i
|
|
402
358
|
signed_id_operation(local_id) do |id|
|
|
403
359
|
return nil if id > IDS_PER_SHARD
|
|
404
360
|
|
|
405
|
-
$1.to_i * IDS_PER_SHARD + id
|
|
361
|
+
($1.to_i * IDS_PER_SHARD) + id
|
|
406
362
|
end
|
|
407
|
-
when Integer,
|
|
363
|
+
when Integer, /\A-?\d+\Z/
|
|
408
364
|
any_id.to_i
|
|
409
365
|
end
|
|
410
366
|
end
|
|
@@ -480,32 +436,15 @@ module Switchman
|
|
|
480
436
|
shard || source_shard || Shard.current
|
|
481
437
|
end
|
|
482
438
|
|
|
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
439
|
private
|
|
501
440
|
|
|
502
441
|
def add_sharded_model(klass)
|
|
503
442
|
@sharded_models = (sharded_models + [klass]).freeze
|
|
504
|
-
|
|
443
|
+
configure_connects_to
|
|
505
444
|
end
|
|
506
445
|
|
|
507
|
-
def
|
|
508
|
-
full_connects_to_hash = DatabaseServer.
|
|
446
|
+
def configure_connects_to
|
|
447
|
+
full_connects_to_hash = DatabaseServer.to_h { |db| [db.id.to_sym, db.connects_to_hash] }
|
|
509
448
|
sharded_models.each do |klass|
|
|
510
449
|
connects_to_hash = full_connects_to_hash.deep_dup
|
|
511
450
|
if klass == UnshardedRecord
|
|
@@ -518,13 +457,16 @@ module Switchman
|
|
|
518
457
|
connects_to_hash.each do |(db_name, role_hash)|
|
|
519
458
|
role_hash.each_key do |role|
|
|
520
459
|
role_hash.delete(role) if klass.connection_handler.retrieve_connection_pool(
|
|
521
|
-
klass.connection_specification_name, role
|
|
460
|
+
klass.connection_specification_name, role:, shard: db_name
|
|
522
461
|
)
|
|
523
462
|
end
|
|
524
463
|
end
|
|
525
464
|
end
|
|
526
465
|
|
|
466
|
+
# this resets the default shard on rails 7.1+, but we want to preserve it
|
|
467
|
+
shard_was = klass.default_shard
|
|
527
468
|
klass.connects_to shards: connects_to_hash
|
|
469
|
+
klass.default_shard = shard_was
|
|
528
470
|
end
|
|
529
471
|
end
|
|
530
472
|
|
|
@@ -558,8 +500,50 @@ module Switchman
|
|
|
558
500
|
shard = nil unless shard.database_server
|
|
559
501
|
shard
|
|
560
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
|
|
561
543
|
end
|
|
562
544
|
|
|
545
|
+
delegate :region, :in_region?, :in_current_region?, to: :database_server
|
|
546
|
+
|
|
563
547
|
def name
|
|
564
548
|
unless instance_variable_defined?(:@name)
|
|
565
549
|
# protect against re-entrancy
|
|
@@ -588,7 +572,7 @@ module Switchman
|
|
|
588
572
|
end
|
|
589
573
|
|
|
590
574
|
def description
|
|
591
|
-
[database_server.id, name].compact.join(
|
|
575
|
+
[database_server.id, name].compact.join(":")
|
|
592
576
|
end
|
|
593
577
|
|
|
594
578
|
# Shards are always on the default shard
|
|
@@ -596,9 +580,17 @@ module Switchman
|
|
|
596
580
|
Shard.default
|
|
597
581
|
end
|
|
598
582
|
|
|
599
|
-
def
|
|
583
|
+
def original_id
|
|
584
|
+
id
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def original_id_value
|
|
588
|
+
id
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def activate(*classes, &)
|
|
600
592
|
shards = hashify_classes(classes)
|
|
601
|
-
Shard.activate(shards, &
|
|
593
|
+
Shard.activate(shards, &)
|
|
602
594
|
end
|
|
603
595
|
|
|
604
596
|
# for use from console ONLY
|
|
@@ -618,48 +610,39 @@ module Switchman
|
|
|
618
610
|
end
|
|
619
611
|
|
|
620
612
|
def drop_database
|
|
621
|
-
raise
|
|
613
|
+
raise "Cannot drop the database of the default shard" if default?
|
|
622
614
|
return unless read_attribute(:name)
|
|
623
615
|
|
|
624
616
|
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) }
|
|
617
|
+
activate do
|
|
618
|
+
self.class.drop_database(name)
|
|
632
619
|
end
|
|
620
|
+
rescue ::ActiveRecord::StatementInvalid => e
|
|
621
|
+
logger.error "Drop failed: #{e}"
|
|
622
|
+
end
|
|
623
|
+
end
|
|
633
624
|
|
|
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
|
|
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)
|
|
659
642
|
end
|
|
643
|
+
ensure
|
|
644
|
+
conn.raw_connection.set_notice_processor(&old_proc) if old_proc
|
|
660
645
|
end
|
|
661
|
-
rescue
|
|
662
|
-
logger.info "Drop failed: #{$!}"
|
|
663
646
|
end
|
|
664
647
|
end
|
|
665
648
|
|
|
@@ -668,7 +651,7 @@ module Switchman
|
|
|
668
651
|
return nil unless local_id
|
|
669
652
|
|
|
670
653
|
self.class.signed_id_operation(local_id) do |abs_id|
|
|
671
|
-
abs_id + id * IDS_PER_SHARD
|
|
654
|
+
abs_id + (id * IDS_PER_SHARD)
|
|
672
655
|
end
|
|
673
656
|
end
|
|
674
657
|
|
|
@@ -678,7 +661,7 @@ module Switchman
|
|
|
678
661
|
end
|
|
679
662
|
|
|
680
663
|
def destroy
|
|
681
|
-
raise(
|
|
664
|
+
raise("Cannot destroy the default shard") if default?
|
|
682
665
|
|
|
683
666
|
super
|
|
684
667
|
end
|
|
@@ -687,8 +670,8 @@ module Switchman
|
|
|
687
670
|
|
|
688
671
|
def clear_cache
|
|
689
672
|
Shard.default.activate do
|
|
690
|
-
Switchman.cache.delete([
|
|
691
|
-
Switchman.cache.delete(
|
|
673
|
+
Switchman.cache.delete(["shard", id].join("/"))
|
|
674
|
+
Switchman.cache.delete("default_shard") if default?
|
|
692
675
|
end
|
|
693
676
|
self.class.clear_cache
|
|
694
677
|
end
|