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
|
@@ -4,8 +4,8 @@ module Switchman
|
|
|
4
4
|
module ActiveRecord
|
|
5
5
|
module StatementCache
|
|
6
6
|
module ClassMethods
|
|
7
|
-
def create(connection
|
|
8
|
-
relation =
|
|
7
|
+
def create(connection)
|
|
8
|
+
relation = yield ::ActiveRecord::StatementCache::Params.new
|
|
9
9
|
|
|
10
10
|
_query_builder, binds = connection.cacheable_query(self, relation.arel)
|
|
11
11
|
bind_map = ::ActiveRecord::StatementCache::BindMap.new(binds)
|
|
@@ -25,7 +25,7 @@ module Switchman
|
|
|
25
25
|
# we can make some assumptions about the shard source
|
|
26
26
|
# (e.g. infer from the primary key or use the current shard)
|
|
27
27
|
|
|
28
|
-
def execute(*args)
|
|
28
|
+
def execute(*args, &block)
|
|
29
29
|
params, connection = args
|
|
30
30
|
klass = @klass
|
|
31
31
|
target_shard = nil
|
|
@@ -33,14 +33,14 @@ module Switchman
|
|
|
33
33
|
primary_value = params[primary_index]
|
|
34
34
|
target_shard = Shard.local_id_for(primary_value)[1]
|
|
35
35
|
end
|
|
36
|
-
current_shard = Shard.current(klass.
|
|
36
|
+
current_shard = Shard.current(klass.connection_class_for_self)
|
|
37
37
|
target_shard ||= current_shard
|
|
38
38
|
|
|
39
39
|
bind_values = bind_map.bind(params, current_shard, target_shard)
|
|
40
40
|
|
|
41
|
-
target_shard.activate(klass.
|
|
41
|
+
target_shard.activate(klass.connection_class_for_self) do
|
|
42
42
|
sql = qualified_query_builder(target_shard, klass).sql_for(bind_values, connection)
|
|
43
|
-
klass.find_by_sql(sql, bind_values)
|
|
43
|
+
klass.find_by_sql(sql, bind_values, &block)
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
46
|
|
|
@@ -66,7 +66,11 @@ module Switchman
|
|
|
66
66
|
|
|
67
67
|
def primary_value_index
|
|
68
68
|
primary_ba_index = @bound_attributes.index do |ba|
|
|
69
|
-
ba.is_a?(::ActiveRecord::
|
|
69
|
+
if ba.value.is_a?(::ActiveRecord::StatementCache::Substitute)
|
|
70
|
+
ba.is_a?(::ActiveRecord::Relation::QueryAttribute) && ba.value.primary
|
|
71
|
+
else
|
|
72
|
+
false
|
|
73
|
+
end
|
|
70
74
|
end
|
|
71
75
|
@indexes.index(primary_ba_index) if primary_ba_index
|
|
72
76
|
end
|
|
@@ -7,9 +7,14 @@ module Switchman
|
|
|
7
7
|
def drop(*)
|
|
8
8
|
super
|
|
9
9
|
# no really, it's gone
|
|
10
|
-
Switchman.cache.delete(
|
|
10
|
+
Switchman.cache.delete("default_shard")
|
|
11
11
|
Shard.default(reload: true)
|
|
12
12
|
end
|
|
13
|
+
|
|
14
|
+
def raise_for_multi_db(*)
|
|
15
|
+
# ignore; Switchman doesn't use namespaced tasks for multiple shards; it uses
|
|
16
|
+
# environment variables to filter which shards you want to target
|
|
17
|
+
end
|
|
13
18
|
end
|
|
14
19
|
end
|
|
15
20
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Switchman
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
module TestFixtures
|
|
6
|
+
FORBIDDEN_DB_ENVS = %i[development production].freeze
|
|
7
|
+
def setup_fixtures(config = ::ActiveRecord::Base)
|
|
8
|
+
super
|
|
9
|
+
|
|
10
|
+
return unless run_in_transaction?
|
|
11
|
+
|
|
12
|
+
# Replace the one that activerecord natively uses with a switchman-optimized one
|
|
13
|
+
::ActiveSupport::Notifications.unsubscribe(@connection_subscriber)
|
|
14
|
+
# Code adapted from the code in rails proper
|
|
15
|
+
@connection_subscriber =
|
|
16
|
+
::ActiveSupport::Notifications.subscribe("!connection.active_record") do |_, _, _, _, payload|
|
|
17
|
+
spec_name = if ::Rails.version < "7.1"
|
|
18
|
+
payload[:spec_name] if payload.key?(:spec_name)
|
|
19
|
+
elsif payload.key?(:connection_name)
|
|
20
|
+
payload[:connection_name]
|
|
21
|
+
end
|
|
22
|
+
shard = payload[:shard] if payload.key?(:shard)
|
|
23
|
+
|
|
24
|
+
if spec_name && !FORBIDDEN_DB_ENVS.include?(shard)
|
|
25
|
+
begin
|
|
26
|
+
connection = ::ActiveRecord::Base.connection_handler.retrieve_connection(spec_name, shard: shard)
|
|
27
|
+
connection.connect! if ::Rails.version >= "7.1" # eagerly validate the connection
|
|
28
|
+
rescue ::ActiveRecord::ConnectionNotEstablished, ::ActiveRecord::NoDatabaseError
|
|
29
|
+
connection = nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if connection
|
|
33
|
+
setup_shared_connection_pool
|
|
34
|
+
unless @fixture_connections.include?(connection)
|
|
35
|
+
connection.begin_transaction joinable: false, _lazy: false
|
|
36
|
+
connection.pool.lock_thread = true if lock_threads
|
|
37
|
+
@fixture_connections << connection
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def enlist_fixture_connections
|
|
45
|
+
setup_shared_connection_pool
|
|
46
|
+
|
|
47
|
+
::ActiveRecord::Base.connection_handler.connection_pool_list(:primary).reject do |cp|
|
|
48
|
+
FORBIDDEN_DB_ENVS.include?(cp.db_config.env_name.to_sym)
|
|
49
|
+
end.map(&:connection)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -4,11 +4,32 @@ module Switchman
|
|
|
4
4
|
module ActiveSupport
|
|
5
5
|
module Cache
|
|
6
6
|
module ClassMethods
|
|
7
|
+
def lookup_stores(cache_store_config)
|
|
8
|
+
result = {}
|
|
9
|
+
cache_store_config.each do |key, value|
|
|
10
|
+
next if value.is_a?(String)
|
|
11
|
+
|
|
12
|
+
result[key] = ::ActiveSupport::Cache.lookup_store(value)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
cache_store_config.each do |key, value| # rubocop:disable Style/CombinableLoops
|
|
16
|
+
next unless value.is_a?(String)
|
|
17
|
+
|
|
18
|
+
result[key] = result[value]
|
|
19
|
+
end
|
|
20
|
+
result
|
|
21
|
+
end
|
|
22
|
+
|
|
7
23
|
def lookup_store(*store_options)
|
|
8
24
|
store = super
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
|
|
25
|
+
# must use the string name, otherwise it will try to auto-load the constant
|
|
26
|
+
# and we don't want to require redis in this file (since it's not a hard dependency)
|
|
27
|
+
# rubocop:disable Style/ClassEqualityComparison
|
|
28
|
+
if store.class.name == "ActiveSupport::Cache::RedisCacheStore" &&
|
|
29
|
+
!(::ActiveSupport::Cache::RedisCacheStore <= RedisCacheStore)
|
|
30
|
+
::ActiveSupport::Cache::RedisCacheStore.prepend(RedisCacheStore)
|
|
31
|
+
end
|
|
32
|
+
# rubocop:enable Style/ClassEqualityComparison
|
|
12
33
|
store.options[:namespace] ||= -> { Shard.current.default? ? nil : "shard_#{Shard.current.id}" }
|
|
13
34
|
store
|
|
14
35
|
end
|
|
@@ -17,7 +38,7 @@ module Switchman
|
|
|
17
38
|
module RedisCacheStore
|
|
18
39
|
def clear(namespace: nil, **)
|
|
19
40
|
# RedisCacheStore tries to be smart and only clear the cache under your namespace, if you have one set
|
|
20
|
-
# unfortunately, it
|
|
41
|
+
# unfortunately, it doesn't work using redis clustering because of the way redis keys are distributed
|
|
21
42
|
# fortunately, we can assume we control the entire instance, because we set up the namespacing, so just
|
|
22
43
|
# always unset it temporarily for clear calls
|
|
23
44
|
namespace = nil # rubocop:disable Lint/ShadowedArgument
|
data/lib/switchman/arel.rb
CHANGED
|
@@ -11,22 +11,60 @@ module Switchman
|
|
|
11
11
|
module Visitors
|
|
12
12
|
module ToSql
|
|
13
13
|
# rubocop:disable Naming/MethodName
|
|
14
|
+
# rubocop:disable Naming/MethodParameterName
|
|
14
15
|
|
|
15
|
-
def
|
|
16
|
-
|
|
16
|
+
def visit_Arel_Nodes_Cte(o, collector)
|
|
17
|
+
collector << quote_local_table_name(o.name)
|
|
18
|
+
collector << " AS "
|
|
19
|
+
|
|
20
|
+
case o.materialized
|
|
21
|
+
when true
|
|
22
|
+
collector << "MATERIALIZED "
|
|
23
|
+
when false
|
|
24
|
+
collector << "NOT MATERIALIZED "
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
visit o.relation, collector
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def visit_Arel_Nodes_TableAlias(o, collector)
|
|
17
31
|
collector = visit o.relation, collector
|
|
18
|
-
collector <<
|
|
32
|
+
collector << " "
|
|
19
33
|
collector << quote_local_table_name(o.name)
|
|
20
34
|
end
|
|
21
35
|
|
|
22
|
-
def visit_Arel_Attributes_Attribute(
|
|
23
|
-
o = args.first
|
|
36
|
+
def visit_Arel_Attributes_Attribute(o, collector)
|
|
24
37
|
join_name = o.relation.table_alias || o.relation.name
|
|
25
|
-
|
|
26
|
-
|
|
38
|
+
collector << quote_local_table_name(join_name) << "." << quote_column_name(o.name)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if ::Rails.version < "7.1"
|
|
42
|
+
def visit_Arel_Nodes_HomogeneousIn(o, collector)
|
|
43
|
+
collector.preparable = false
|
|
44
|
+
|
|
45
|
+
collector << quote_local_table_name(o.table_name) << "." << quote_column_name(o.column_name)
|
|
46
|
+
|
|
47
|
+
collector << if o.type == :in
|
|
48
|
+
" IN ("
|
|
49
|
+
else
|
|
50
|
+
" NOT IN ("
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
values = o.casted_values
|
|
54
|
+
|
|
55
|
+
if values.empty?
|
|
56
|
+
collector << @connection.quote(nil)
|
|
57
|
+
else
|
|
58
|
+
collector.add_binds(values, o.proc_for_binds, &bind_block)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
collector << ")"
|
|
62
|
+
collector
|
|
63
|
+
end
|
|
27
64
|
end
|
|
28
65
|
|
|
29
66
|
# rubocop:enable Naming/MethodName
|
|
67
|
+
# rubocop:enable Naming/MethodParameterName
|
|
30
68
|
|
|
31
69
|
def quote_local_table_name(name)
|
|
32
70
|
return name if ::Arel::Nodes::SqlLiteral === name
|
data/lib/switchman/call_super.rb
CHANGED
|
@@ -12,8 +12,8 @@ module Switchman
|
|
|
12
12
|
method.super_method
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def call_super(method, above_module,
|
|
16
|
-
super_method_above(method, above_module).call(
|
|
15
|
+
def call_super(method, above_module, ...)
|
|
16
|
+
super_method_above(method, above_module).call(...)
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "securerandom"
|
|
4
4
|
|
|
5
5
|
module Switchman
|
|
6
6
|
class DatabaseServer
|
|
@@ -8,24 +8,25 @@ module Switchman
|
|
|
8
8
|
|
|
9
9
|
class << self
|
|
10
10
|
attr_accessor :creating_new_shard
|
|
11
|
+
attr_reader :all_roles
|
|
12
|
+
|
|
13
|
+
include Enumerable
|
|
14
|
+
|
|
15
|
+
delegate :each, to: :all
|
|
11
16
|
|
|
12
17
|
def all
|
|
13
18
|
database_servers.values
|
|
14
19
|
end
|
|
15
20
|
|
|
16
|
-
def all_roles
|
|
17
|
-
@all_roles ||= all.map(&:roles).flatten.uniq
|
|
18
|
-
end
|
|
19
|
-
|
|
20
21
|
def find(id_or_all)
|
|
21
22
|
return all if id_or_all == :all
|
|
22
|
-
return id_or_all.
|
|
23
|
+
return id_or_all.filter_map { |id| database_servers[id || ::Rails.env] }.uniq if id_or_all.is_a?(Array)
|
|
23
24
|
|
|
24
25
|
database_servers[id_or_all || ::Rails.env]
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
def create(settings = {})
|
|
28
|
-
raise
|
|
29
|
+
raise "database servers should be set up in database.yml" unless ::Rails.env.test?
|
|
29
30
|
|
|
30
31
|
id = settings[:id]
|
|
31
32
|
unless id
|
|
@@ -38,7 +39,7 @@ module Switchman
|
|
|
38
39
|
database_servers[server.id] = server
|
|
39
40
|
::ActiveRecord::Base.configurations.configurations <<
|
|
40
41
|
::ActiveRecord::DatabaseConfigurations::HashConfig.new(::Rails.env, "#{server.id}/primary", settings)
|
|
41
|
-
Shard.send(:
|
|
42
|
+
Shard.send(:configure_connects_to)
|
|
42
43
|
server
|
|
43
44
|
end
|
|
44
45
|
|
|
@@ -49,31 +50,48 @@ module Switchman
|
|
|
49
50
|
servers[rand(servers.length)]
|
|
50
51
|
end
|
|
51
52
|
|
|
53
|
+
def guard_servers
|
|
54
|
+
all.each { |db| db.guard! if db.config[:prefer_secondary] }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def regions
|
|
58
|
+
@regions ||= all.filter_map(&:region).uniq.sort
|
|
59
|
+
end
|
|
60
|
+
|
|
52
61
|
private
|
|
53
62
|
|
|
54
63
|
def reference_role(role)
|
|
55
64
|
return if all_roles.include?(role)
|
|
56
65
|
|
|
57
66
|
@all_roles << role
|
|
58
|
-
Shard.send(:
|
|
67
|
+
Shard.send(:configure_connects_to)
|
|
59
68
|
end
|
|
60
69
|
|
|
61
70
|
def database_servers
|
|
62
|
-
|
|
71
|
+
if !@database_servers || @database_servers.empty?
|
|
63
72
|
@database_servers = {}.with_indifferent_access
|
|
73
|
+
roles = []
|
|
64
74
|
::ActiveRecord::Base.configurations.configurations.each do |config|
|
|
65
|
-
if config.name.include?(
|
|
66
|
-
name, role = config.name.split(
|
|
75
|
+
if config.name.include?("/")
|
|
76
|
+
name, role = config.name.split("/")
|
|
67
77
|
else
|
|
68
78
|
name, role = config.env_name, config.name
|
|
69
79
|
end
|
|
80
|
+
role = role.to_sym
|
|
70
81
|
|
|
71
|
-
|
|
82
|
+
roles << role
|
|
83
|
+
if role == :primary
|
|
72
84
|
@database_servers[name] = DatabaseServer.new(config.env_name, config.configuration_hash)
|
|
73
85
|
else
|
|
74
86
|
@database_servers[name].roles << role
|
|
75
87
|
end
|
|
76
88
|
end
|
|
89
|
+
# Do this after so that all database servers for all roles are established and we won't prematurely
|
|
90
|
+
# configure a connection for the wrong role
|
|
91
|
+
@all_roles = roles.uniq
|
|
92
|
+
return @database_servers if @database_servers.empty?
|
|
93
|
+
|
|
94
|
+
Shard.send(:configure_connects_to)
|
|
77
95
|
end
|
|
78
96
|
@database_servers
|
|
79
97
|
end
|
|
@@ -89,21 +107,22 @@ module Switchman
|
|
|
89
107
|
end
|
|
90
108
|
|
|
91
109
|
def connects_to_hash
|
|
92
|
-
self.class.all_roles.
|
|
110
|
+
self.class.all_roles.to_h do |role|
|
|
93
111
|
config_role = role
|
|
94
112
|
config_role = :primary unless roles.include?(role)
|
|
95
113
|
config_name = :"#{id}/#{config_role}"
|
|
96
114
|
config_name = :primary if id == ::Rails.env && config_role == :primary
|
|
97
115
|
[role.to_sym, config_name]
|
|
98
|
-
end
|
|
116
|
+
end
|
|
99
117
|
end
|
|
100
118
|
|
|
101
119
|
def destroy
|
|
102
120
|
self.class.send(:database_servers).delete(id) if id
|
|
103
121
|
Shard.sharded_models.each do |klass|
|
|
104
122
|
self.class.all_roles.each do |role|
|
|
105
|
-
klass.connection_handler.remove_connection_pool(klass.connection_specification_name,
|
|
106
|
-
|
|
123
|
+
klass.connection_handler.remove_connection_pool(klass.connection_specification_name,
|
|
124
|
+
role: role,
|
|
125
|
+
shard: id.to_sym)
|
|
107
126
|
end
|
|
108
127
|
end
|
|
109
128
|
end
|
|
@@ -129,32 +148,57 @@ module Switchman
|
|
|
129
148
|
end
|
|
130
149
|
end
|
|
131
150
|
|
|
132
|
-
def
|
|
133
|
-
|
|
151
|
+
def region
|
|
152
|
+
config[:region]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# @param region [String, Array<String>] the region(s) to check against
|
|
156
|
+
# @return true if the database server doesn't have a region, or it
|
|
157
|
+
# matches the specified region
|
|
158
|
+
def in_region?(region)
|
|
159
|
+
!self.region || (region.is_a?(Array) ? region.include?(self.region) : self.region == region)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# @return true if the database server doesn't have a region, Switchman is
|
|
163
|
+
# not configured with a region, or the database server's region matches
|
|
164
|
+
# Switchman's current region
|
|
165
|
+
def in_current_region?
|
|
166
|
+
unless instance_variable_defined?(:@in_current_region)
|
|
167
|
+
@in_current_region = !region ||
|
|
168
|
+
!Switchman.region ||
|
|
169
|
+
region == Switchman.region
|
|
170
|
+
end
|
|
171
|
+
@in_current_region
|
|
134
172
|
end
|
|
135
173
|
|
|
136
174
|
# locks this db to a specific environment, except for
|
|
137
175
|
# when doing writes (then it falls back to the current
|
|
138
176
|
# value of GuardRail.environment)
|
|
139
177
|
def guard!(environment = :secondary)
|
|
140
|
-
|
|
178
|
+
DatabaseServer.send(:reference_role, environment)
|
|
179
|
+
::ActiveRecord::Base.connected_to_stack << { shard_roles: { id.to_sym => environment },
|
|
180
|
+
klasses: [::ActiveRecord::Base] }
|
|
141
181
|
end
|
|
142
182
|
|
|
143
183
|
def unguard!
|
|
144
|
-
|
|
184
|
+
::ActiveRecord::Base.connected_to_stack << { shard_roles: { id.to_sym => :_switchman_inherit },
|
|
185
|
+
klasses: [::ActiveRecord::Base] }
|
|
145
186
|
end
|
|
146
187
|
|
|
147
188
|
def unguard
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
189
|
+
return yield unless ::ActiveRecord::Base.role_overriden?(id.to_sym)
|
|
190
|
+
|
|
191
|
+
begin
|
|
192
|
+
unguard!
|
|
193
|
+
yield
|
|
194
|
+
ensure
|
|
195
|
+
::ActiveRecord::Base.connected_to_stack.pop
|
|
196
|
+
end
|
|
153
197
|
end
|
|
154
198
|
|
|
155
199
|
def shards
|
|
156
200
|
if id == ::Rails.env
|
|
157
|
-
Shard.where(
|
|
201
|
+
Shard.where("database_server_id IS NULL OR database_server_id=?", id)
|
|
158
202
|
else
|
|
159
203
|
Shard.where(database_server_id: id)
|
|
160
204
|
end
|
|
@@ -179,65 +223,65 @@ module Switchman
|
|
|
179
223
|
end
|
|
180
224
|
|
|
181
225
|
id ||= begin
|
|
182
|
-
id_seq = Shard.connection.quote(Shard.connection.quote_table_name(
|
|
226
|
+
id_seq = Shard.connection.quote(Shard.connection.quote_table_name("switchman_shards_id_seq"))
|
|
183
227
|
next_id = Shard.connection.select_value("SELECT nextval(#{id_seq})")
|
|
184
228
|
next_id.to_i
|
|
185
229
|
end
|
|
186
230
|
|
|
187
231
|
name ||= "#{config[:database]}_shard_#{id}"
|
|
188
232
|
|
|
233
|
+
schema_already_existed = false
|
|
234
|
+
shard = nil
|
|
189
235
|
Shard.connection.transaction do
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
raise 'This schema already exists; cannot overwrite'
|
|
203
|
-
end
|
|
204
|
-
Array(create_statement.call).each do |stmt|
|
|
205
|
-
::ActiveRecord::Base.connection.execute(stmt)
|
|
206
|
-
end
|
|
236
|
+
self.class.creating_new_shard = true
|
|
237
|
+
DatabaseServer.send(:reference_role, :deploy)
|
|
238
|
+
::ActiveRecord::Base.connected_to(shard: self.id.to_sym, role: :deploy) do
|
|
239
|
+
shard = Shard.create!(id: id,
|
|
240
|
+
name: name,
|
|
241
|
+
database_server_id: self.id)
|
|
242
|
+
if create_statement
|
|
243
|
+
if ::ActiveRecord::Base.connection.select_value(
|
|
244
|
+
"SELECT 1 FROM pg_namespace WHERE nspname=#{::ActiveRecord::Base.connection.quote(name)}"
|
|
245
|
+
)
|
|
246
|
+
schema_already_existed = true
|
|
247
|
+
raise "This schema already exists; cannot overwrite"
|
|
207
248
|
end
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
end
|
|
249
|
+
Array(create_statement.call).each do |stmt|
|
|
250
|
+
::ActiveRecord::Base.connection.execute(stmt)
|
|
211
251
|
end
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
252
|
+
end
|
|
253
|
+
if config[:adapter] == "postgresql"
|
|
254
|
+
old_proc = ::ActiveRecord::Base.connection.raw_connection.set_notice_processor {}
|
|
255
|
+
end
|
|
256
|
+
old_verbose = ::ActiveRecord::Migration.verbose
|
|
257
|
+
::ActiveRecord::Migration.verbose = false
|
|
218
258
|
|
|
219
|
-
|
|
259
|
+
unless schema == false
|
|
260
|
+
shard.activate do
|
|
261
|
+
::ActiveRecord::Base.connection.transaction(requires_new: true) do
|
|
262
|
+
if ::Rails.version < "7.1"
|
|
220
263
|
::ActiveRecord::Base.connection.migration_context.migrate
|
|
264
|
+
else
|
|
265
|
+
::ActiveRecord::MigrationContext.new(::ActiveRecord::Migrator.migrations_paths).migrate
|
|
221
266
|
end
|
|
222
|
-
reset_column_information
|
|
223
|
-
::ActiveRecord::Base.descendants.reject do |m|
|
|
224
|
-
m <= UnshardedRecord || !m.table_exists?
|
|
225
|
-
end.each(&:define_attribute_methods)
|
|
226
267
|
end
|
|
268
|
+
|
|
269
|
+
::ActiveRecord::Base.descendants.reject do |m|
|
|
270
|
+
m <= UnshardedRecord || !m.table_exists?
|
|
271
|
+
end.each(&:define_attribute_methods)
|
|
227
272
|
end
|
|
228
|
-
ensure
|
|
229
|
-
::ActiveRecord::Migration.verbose = old_verbose
|
|
230
|
-
::ActiveRecord::Base.connection.raw_connection.set_notice_processor(&old_proc) if old_proc
|
|
231
273
|
end
|
|
232
|
-
shard
|
|
233
|
-
rescue
|
|
234
|
-
shard.destroy
|
|
235
|
-
shard.drop_database rescue nil unless schema_already_existed
|
|
236
|
-
reset_column_information unless schema == false rescue nil
|
|
237
|
-
raise
|
|
238
274
|
ensure
|
|
239
|
-
|
|
275
|
+
::ActiveRecord::Migration.verbose = old_verbose
|
|
276
|
+
::ActiveRecord::Base.connection.raw_connection.set_notice_processor(&old_proc) if old_proc
|
|
240
277
|
end
|
|
278
|
+
shard
|
|
279
|
+
rescue
|
|
280
|
+
shard&.destroy
|
|
281
|
+
shard&.drop_database rescue nil unless schema_already_existed
|
|
282
|
+
raise
|
|
283
|
+
ensure
|
|
284
|
+
self.class.creating_new_shard = false
|
|
241
285
|
end
|
|
242
286
|
end
|
|
243
287
|
|
|
@@ -259,18 +303,18 @@ module Switchman
|
|
|
259
303
|
end
|
|
260
304
|
|
|
261
305
|
def primary_shard
|
|
262
|
-
unless
|
|
263
|
-
# if sharding isn't fully set up yet, we may not be able to query the shards table
|
|
264
|
-
@primary_shard = Shard.default if Shard.default.database_server == self
|
|
265
|
-
@primary_shard ||= shards.where(name: nil).first
|
|
266
|
-
end
|
|
267
|
-
@primary_shard
|
|
268
|
-
end
|
|
306
|
+
return nil unless primary_shard_id
|
|
269
307
|
|
|
270
|
-
|
|
308
|
+
Shard.lookup(primary_shard_id)
|
|
309
|
+
end
|
|
271
310
|
|
|
272
|
-
def
|
|
273
|
-
|
|
311
|
+
def primary_shard_id
|
|
312
|
+
unless instance_variable_defined?(:@primary_shard_id)
|
|
313
|
+
# if sharding isn't fully set up yet, we may not be able to query the shards table
|
|
314
|
+
@primary_shard_id = Shard.default.id if Shard.default.database_server == self
|
|
315
|
+
@primary_shard_id ||= shards.where(name: nil).first&.id
|
|
316
|
+
end
|
|
317
|
+
@primary_shard_id
|
|
274
318
|
end
|
|
275
319
|
end
|
|
276
320
|
end
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'switchman/database_server'
|
|
4
|
-
|
|
5
3
|
module Switchman
|
|
6
4
|
class DefaultShard
|
|
7
5
|
def id
|
|
8
|
-
|
|
6
|
+
"default"
|
|
9
7
|
end
|
|
10
|
-
|
|
8
|
+
alias_method :cache_key, :id
|
|
9
|
+
|
|
11
10
|
def activate(*_classes)
|
|
12
11
|
yield
|
|
13
12
|
end
|
|
@@ -59,8 +58,18 @@ module Switchman
|
|
|
59
58
|
self
|
|
60
59
|
end
|
|
61
60
|
|
|
61
|
+
def region; end
|
|
62
|
+
|
|
63
|
+
def in_region?(_region)
|
|
64
|
+
true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def in_current_region?
|
|
68
|
+
true
|
|
69
|
+
end
|
|
70
|
+
|
|
62
71
|
def _dump(_depth)
|
|
63
|
-
|
|
72
|
+
""
|
|
64
73
|
end
|
|
65
74
|
|
|
66
75
|
def self._load(_str)
|