switchman 3.3.6 → 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 +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 +10 -6
- data/lib/switchman/active_record/associations.rb +72 -49
- data/lib/switchman/active_record/attribute_methods.rb +89 -44
- data/lib/switchman/active_record/base.rb +109 -40
- data/lib/switchman/active_record/calculations.rb +90 -54
- data/lib/switchman/active_record/connection_handler.rb +18 -0
- data/lib/switchman/active_record/connection_pool.rb +41 -23
- data/lib/switchman/active_record/database_configurations.rb +23 -13
- data/lib/switchman/active_record/finder_methods.rb +20 -14
- data/lib/switchman/active_record/log_subscriber.rb +3 -6
- data/lib/switchman/active_record/migration.rb +35 -12
- 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 +26 -17
- 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 +87 -17
- data/lib/switchman/active_record/spawn_methods.rb +3 -7
- data/lib/switchman/active_record/statement_cache.rb +4 -4
- data/lib/switchman/active_record/table_definition.rb +1 -1
- data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
- data/lib/switchman/active_record/test_fixtures.rb +71 -25
- data/lib/switchman/active_support/cache.rb +9 -4
- data/lib/switchman/arel.rb +16 -25
- data/lib/switchman/call_super.rb +2 -8
- data/lib/switchman/database_server.rb +67 -48
- data/lib/switchman/default_shard.rb +14 -3
- data/lib/switchman/engine.rb +35 -23
- 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 +168 -68
- data/lib/switchman/sharded_instrumenter.rb +9 -3
- 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 +28 -173
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,7 @@ 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
|
-
# output: - :simple, :decorated (with database_server_id:shard_name)
|
|
170
|
+
# output: - :simple, :decorated (with database_server_id:shard_name), custom lambda transformer
|
|
124
171
|
def with_each_shard(*args, parallel: false, exception: :raise, output: :simple)
|
|
125
172
|
raise ArgumentError, "wrong number of arguments (#{args.length} for 0...2)" if args.length > 2
|
|
126
173
|
|
|
@@ -136,19 +183,28 @@ module Switchman
|
|
|
136
183
|
scope, classes = args
|
|
137
184
|
end
|
|
138
185
|
|
|
186
|
+
output = if output == :decorated
|
|
187
|
+
->(arg) { "#{Shard.current.description}: #{arg}" }
|
|
188
|
+
elsif output == :simple
|
|
189
|
+
nil
|
|
190
|
+
else
|
|
191
|
+
output
|
|
192
|
+
end
|
|
139
193
|
parallel = [Environment.cpu_count || 2, 2].min if parallel == true
|
|
140
194
|
parallel = 0 if parallel == false || parallel.nil?
|
|
141
195
|
|
|
142
196
|
scope ||= Shard.all
|
|
143
|
-
|
|
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
|
|
144
200
|
|
|
145
201
|
if parallel > 1
|
|
146
202
|
if ::ActiveRecord::Relation === scope
|
|
147
203
|
# still need a post-uniq, cause the default database server could be NULL or Rails.env in the db
|
|
148
|
-
database_servers = scope.reorder(
|
|
149
|
-
|
|
204
|
+
database_servers = scope.reorder("database_server_id").select(:database_server_id).distinct
|
|
205
|
+
.filter_map(&:database_server).uniq
|
|
150
206
|
# nothing to do
|
|
151
|
-
return if database_servers.
|
|
207
|
+
return if database_servers.none?
|
|
152
208
|
|
|
153
209
|
scopes = database_servers.to_h do |server|
|
|
154
210
|
[server, scope.merge(server.shards)]
|
|
@@ -160,20 +216,23 @@ module Switchman
|
|
|
160
216
|
# clear connections prior to forking (no more queries will be executed in the parent,
|
|
161
217
|
# and we want them gone so that we don't accidentally use them post-fork doing something
|
|
162
218
|
# silly like dealloc'ing prepared statements)
|
|
163
|
-
::ActiveRecord::Base.clear_all_connections!
|
|
219
|
+
::ActiveRecord::Base.connection_handler.clear_all_connections!(:all)
|
|
164
220
|
|
|
165
|
-
parent_process_name =
|
|
166
|
-
ret = ::Parallel.map(scopes, in_processes: scopes.length > 1 ? parallel : 0) do |server, subscope|
|
|
221
|
+
parent_process_name = sanitized_process_title
|
|
222
|
+
ret = ::Parallel.map(scopes, in_processes: (scopes.length > 1) ? parallel : 0) do |server, subscope|
|
|
167
223
|
name = server.id
|
|
168
224
|
last_description = name
|
|
169
225
|
|
|
170
226
|
begin
|
|
171
227
|
max_length = 128 - name.length - 3
|
|
172
228
|
short_parent_name = parent_process_name[0..max_length] if max_length >= 0
|
|
173
|
-
new_title = [short_parent_name, name].join(
|
|
229
|
+
new_title = [short_parent_name, name].join(" ")
|
|
174
230
|
Process.setproctitle(new_title)
|
|
175
231
|
Switchman.config[:on_fork_proc]&.call
|
|
176
|
-
with_each_shard(subscope,
|
|
232
|
+
with_each_shard(subscope,
|
|
233
|
+
classes,
|
|
234
|
+
exception:,
|
|
235
|
+
output: output || :decorated) do
|
|
177
236
|
last_description = Shard.current.description
|
|
178
237
|
Parallel::ResultWrapper.new(yield)
|
|
179
238
|
end
|
|
@@ -187,8 +246,9 @@ module Switchman
|
|
|
187
246
|
unless errors.empty?
|
|
188
247
|
raise errors.first.exception if errors.length == 1
|
|
189
248
|
|
|
249
|
+
errors_desc = errors.map(&:name).sort.join(", ")
|
|
190
250
|
raise Errors::ParallelShardExecError,
|
|
191
|
-
"The following database server(s) did not finish processing cleanly: #{
|
|
251
|
+
"The following database server(s) did not finish processing cleanly: #{errors_desc}",
|
|
192
252
|
cause: errors.first.exception
|
|
193
253
|
end
|
|
194
254
|
|
|
@@ -206,9 +266,9 @@ module Switchman
|
|
|
206
266
|
next unless shard.database_server
|
|
207
267
|
|
|
208
268
|
shard.activate(*classes) do
|
|
209
|
-
if output
|
|
210
|
-
$stdout = Parallel::
|
|
211
|
-
$stderr = Parallel::
|
|
269
|
+
if output
|
|
270
|
+
$stdout = Parallel::TransformingIO.new(output, $stdout)
|
|
271
|
+
$stderr = Parallel::TransformingIO.new(output, $stderr)
|
|
212
272
|
end
|
|
213
273
|
|
|
214
274
|
result.concat Array.wrap(yield)
|
|
@@ -251,7 +311,7 @@ module Switchman
|
|
|
251
311
|
else
|
|
252
312
|
shard = partition_object.shard
|
|
253
313
|
end
|
|
254
|
-
when Integer,
|
|
314
|
+
when Integer, /\A\d+\Z/, /\A(\d+)~(\d+)\Z/
|
|
255
315
|
local_id, shard = Shard.local_id_for(partition_object)
|
|
256
316
|
local_id ||= partition_object
|
|
257
317
|
object = local_id unless partition_proc
|
|
@@ -293,14 +353,14 @@ module Switchman
|
|
|
293
353
|
case any_id
|
|
294
354
|
when ::ActiveRecord::Base
|
|
295
355
|
any_id.id
|
|
296
|
-
when
|
|
356
|
+
when /\A(\d+)~(-?\d+)\Z/
|
|
297
357
|
local_id = $2.to_i
|
|
298
358
|
signed_id_operation(local_id) do |id|
|
|
299
359
|
return nil if id > IDS_PER_SHARD
|
|
300
360
|
|
|
301
361
|
($1.to_i * IDS_PER_SHARD) + id
|
|
302
362
|
end
|
|
303
|
-
when Integer,
|
|
363
|
+
when Integer, /\A-?\d+\Z/
|
|
304
364
|
any_id.to_i
|
|
305
365
|
end
|
|
306
366
|
end
|
|
@@ -384,7 +444,7 @@ module Switchman
|
|
|
384
444
|
end
|
|
385
445
|
|
|
386
446
|
def configure_connects_to
|
|
387
|
-
full_connects_to_hash = DatabaseServer.
|
|
447
|
+
full_connects_to_hash = DatabaseServer.to_h { |db| [db.id.to_sym, db.connects_to_hash] }
|
|
388
448
|
sharded_models.each do |klass|
|
|
389
449
|
connects_to_hash = full_connects_to_hash.deep_dup
|
|
390
450
|
if klass == UnshardedRecord
|
|
@@ -397,13 +457,16 @@ module Switchman
|
|
|
397
457
|
connects_to_hash.each do |(db_name, role_hash)|
|
|
398
458
|
role_hash.each_key do |role|
|
|
399
459
|
role_hash.delete(role) if klass.connection_handler.retrieve_connection_pool(
|
|
400
|
-
klass.connection_specification_name, role
|
|
460
|
+
klass.connection_specification_name, role:, shard: db_name
|
|
401
461
|
)
|
|
402
462
|
end
|
|
403
463
|
end
|
|
404
464
|
end
|
|
405
465
|
|
|
466
|
+
# this resets the default shard on rails 7.1+, but we want to preserve it
|
|
467
|
+
shard_was = klass.default_shard
|
|
406
468
|
klass.connects_to shards: connects_to_hash
|
|
469
|
+
klass.default_shard = shard_was
|
|
407
470
|
end
|
|
408
471
|
end
|
|
409
472
|
|
|
@@ -437,8 +500,50 @@ module Switchman
|
|
|
437
500
|
shard = nil unless shard.database_server
|
|
438
501
|
shard
|
|
439
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
|
|
440
543
|
end
|
|
441
544
|
|
|
545
|
+
delegate :region, :in_region?, :in_current_region?, to: :database_server
|
|
546
|
+
|
|
442
547
|
def name
|
|
443
548
|
unless instance_variable_defined?(:@name)
|
|
444
549
|
# protect against re-entrancy
|
|
@@ -467,7 +572,7 @@ module Switchman
|
|
|
467
572
|
end
|
|
468
573
|
|
|
469
574
|
def description
|
|
470
|
-
[database_server.id, name].compact.join(
|
|
575
|
+
[database_server.id, name].compact.join(":")
|
|
471
576
|
end
|
|
472
577
|
|
|
473
578
|
# Shards are always on the default shard
|
|
@@ -479,9 +584,13 @@ module Switchman
|
|
|
479
584
|
id
|
|
480
585
|
end
|
|
481
586
|
|
|
482
|
-
def
|
|
587
|
+
def original_id_value
|
|
588
|
+
id
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def activate(*classes, &)
|
|
483
592
|
shards = hashify_classes(classes)
|
|
484
|
-
Shard.activate(shards, &
|
|
593
|
+
Shard.activate(shards, &)
|
|
485
594
|
end
|
|
486
595
|
|
|
487
596
|
# for use from console ONLY
|
|
@@ -501,48 +610,39 @@ module Switchman
|
|
|
501
610
|
end
|
|
502
611
|
|
|
503
612
|
def drop_database
|
|
504
|
-
raise
|
|
613
|
+
raise "Cannot drop the database of the default shard" if default?
|
|
505
614
|
return unless read_attribute(:name)
|
|
506
615
|
|
|
507
616
|
begin
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
drop_statement = sharding_config[adapter]&.[](:drop_statement)
|
|
511
|
-
drop_statement ||= sharding_config[:drop_statement]
|
|
512
|
-
if drop_statement
|
|
513
|
-
drop_statement = Array(drop_statement).dup.
|
|
514
|
-
map { |statement| statement.gsub('%{name}', name) }
|
|
617
|
+
activate do
|
|
618
|
+
self.class.drop_database(name)
|
|
515
619
|
end
|
|
620
|
+
rescue ::ActiveRecord::StatementInvalid => e
|
|
621
|
+
logger.error "Drop failed: #{e}"
|
|
622
|
+
end
|
|
623
|
+
end
|
|
516
624
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
drop_statement ||= "DROP SCHEMA #{name} CASCADE"
|
|
535
|
-
Array(drop_statement).each do |stmt|
|
|
536
|
-
::ActiveRecord::Base.connection.execute(stmt)
|
|
537
|
-
end
|
|
538
|
-
ensure
|
|
539
|
-
conn.raw_connection.set_notice_processor(&old_proc) if old_proc
|
|
540
|
-
end
|
|
541
|
-
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)
|
|
542
642
|
end
|
|
643
|
+
ensure
|
|
644
|
+
conn.raw_connection.set_notice_processor(&old_proc) if old_proc
|
|
543
645
|
end
|
|
544
|
-
rescue
|
|
545
|
-
logger.info "Drop failed: #{$!}"
|
|
546
646
|
end
|
|
547
647
|
end
|
|
548
648
|
|
|
@@ -561,7 +661,7 @@ module Switchman
|
|
|
561
661
|
end
|
|
562
662
|
|
|
563
663
|
def destroy
|
|
564
|
-
raise(
|
|
664
|
+
raise("Cannot destroy the default shard") if default?
|
|
565
665
|
|
|
566
666
|
super
|
|
567
667
|
end
|
|
@@ -570,8 +670,8 @@ module Switchman
|
|
|
570
670
|
|
|
571
671
|
def clear_cache
|
|
572
672
|
Shard.default.activate do
|
|
573
|
-
Switchman.cache.delete([
|
|
574
|
-
Switchman.cache.delete(
|
|
673
|
+
Switchman.cache.delete(["shard", id].join("/"))
|
|
674
|
+
Switchman.cache.delete("default_shard") if default?
|
|
575
675
|
end
|
|
576
676
|
self.class.clear_cache
|
|
577
677
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Switchman
|
|
4
4
|
class ShardedInstrumenter < ::SimpleDelegator
|
|
5
5
|
def initialize(instrumenter, shard_host)
|
|
6
|
-
super
|
|
6
|
+
super(instrumenter)
|
|
7
7
|
@shard_host = shard_host
|
|
8
8
|
end
|
|
9
9
|
|
|
@@ -13,13 +13,19 @@ module Switchman
|
|
|
13
13
|
# when we might be doing a query while defining attribute methods,
|
|
14
14
|
# so just avoid logging then
|
|
15
15
|
if shard.is_a?(Shard) && Shard.instance_variable_get(:@attribute_methods_generated)
|
|
16
|
+
env = if ::Rails.version < "8.0"
|
|
17
|
+
@shard_host.pool.connection_class&.current_role
|
|
18
|
+
else
|
|
19
|
+
@shard_host.pool.connection_descriptor&.name&.constantize&.current_role
|
|
20
|
+
end
|
|
21
|
+
|
|
16
22
|
payload[:shard] = {
|
|
17
23
|
database_server_id: shard.database_server.id,
|
|
18
24
|
id: shard.id,
|
|
19
|
-
env:
|
|
25
|
+
env:
|
|
20
26
|
}
|
|
21
27
|
end
|
|
22
|
-
super
|
|
28
|
+
super
|
|
23
29
|
end
|
|
24
30
|
end
|
|
25
31
|
end
|
|
@@ -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