switchman 1.5.21 → 2.1.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 +5 -5
- data/app/models/switchman/shard.rb +757 -11
- data/db/migrate/20130328212039_create_switchman_shards.rb +3 -1
- data/db/migrate/20130328224244_create_default_shard.rb +4 -2
- data/db/migrate/20161206323434_add_back_default_string_limits_switchman.rb +13 -0
- data/db/migrate/20180828183945_add_default_shard_index.rb +15 -0
- data/db/migrate/20180828192111_add_timestamps_to_shards.rb +17 -0
- data/db/migrate/20190114212900_add_unique_name_indexes.rb +9 -0
- data/lib/switchman/action_controller/caching.rb +2 -0
- data/lib/switchman/active_record/abstract_adapter.rb +14 -4
- data/lib/switchman/active_record/association.rb +64 -37
- data/lib/switchman/active_record/attribute_methods.rb +54 -22
- data/lib/switchman/active_record/base.rb +76 -31
- data/lib/switchman/active_record/batches.rb +3 -1
- data/lib/switchman/active_record/calculations.rb +17 -22
- data/lib/switchman/active_record/connection_handler.rb +88 -78
- data/lib/switchman/active_record/connection_pool.rb +28 -23
- data/lib/switchman/active_record/finder_methods.rb +37 -28
- data/lib/switchman/active_record/log_subscriber.rb +14 -19
- data/lib/switchman/active_record/migration.rb +80 -0
- data/lib/switchman/active_record/model_schema.rb +3 -1
- data/lib/switchman/active_record/persistence.rb +9 -1
- data/lib/switchman/active_record/postgresql_adapter.rb +170 -126
- data/lib/switchman/active_record/predicate_builder.rb +3 -1
- data/lib/switchman/active_record/query_cache.rb +22 -87
- data/lib/switchman/active_record/query_methods.rb +139 -125
- data/lib/switchman/active_record/reflection.rb +42 -14
- data/lib/switchman/active_record/relation.rb +108 -33
- data/lib/switchman/active_record/spawn_methods.rb +2 -0
- data/lib/switchman/active_record/statement_cache.rb +44 -52
- data/lib/switchman/active_record/table_definition.rb +4 -2
- data/lib/switchman/active_record/type_caster.rb +2 -0
- data/lib/switchman/active_record/where_clause_factory.rb +5 -2
- data/lib/switchman/active_support/cache.rb +18 -0
- data/lib/switchman/arel.rb +8 -25
- data/lib/switchman/call_super.rb +19 -0
- data/lib/switchman/connection_pool_proxy.rb +70 -24
- data/lib/switchman/database_server.rb +69 -59
- data/lib/switchman/default_shard.rb +3 -0
- data/lib/switchman/engine.rb +44 -41
- data/lib/switchman/environment.rb +2 -0
- data/lib/switchman/errors.rb +2 -0
- data/lib/switchman/{shackles → guard_rail}/relation.rb +7 -5
- data/lib/switchman/{shackles.rb → guard_rail.rb} +6 -4
- data/lib/switchman/open4.rb +2 -0
- data/lib/switchman/r_spec_helper.rb +14 -8
- data/lib/switchman/rails.rb +2 -0
- data/lib/switchman/schema_cache.rb +17 -0
- data/lib/switchman/sharded_instrumenter.rb +4 -2
- data/lib/switchman/standard_error.rb +4 -2
- data/lib/switchman/test_helper.rb +7 -10
- data/lib/switchman/version.rb +3 -1
- data/lib/switchman.rb +5 -1
- data/lib/tasks/switchman.rake +53 -72
- metadata +84 -38
- data/app/models/switchman/shard_internal.rb +0 -692
|
@@ -1,16 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Switchman
|
|
2
4
|
module ActiveRecord
|
|
3
5
|
module Calculations
|
|
4
|
-
CALL_SUPER = Object.new.freeze
|
|
5
|
-
private_constant :CALL_SUPER
|
|
6
6
|
|
|
7
7
|
def pluck(*column_names)
|
|
8
|
-
return super(*column_names[1..-1]) if column_names.first.equal?(CALL_SUPER)
|
|
9
8
|
target_shard = Shard.current(klass.shard_category)
|
|
10
9
|
shard_count = 0
|
|
11
10
|
result = self.activate do |relation, shard|
|
|
12
11
|
shard_count += 1
|
|
13
|
-
results = relation.pluck
|
|
12
|
+
results = relation.call_super(:pluck, Calculations, *column_names)
|
|
14
13
|
if column_names.length > 1
|
|
15
14
|
column_names.each_with_index do |column_name, idx|
|
|
16
15
|
if klass.sharded_column?(column_name)
|
|
@@ -30,13 +29,12 @@ module Switchman
|
|
|
30
29
|
result
|
|
31
30
|
end
|
|
32
31
|
|
|
33
|
-
def execute_simple_calculation(operation, column_name, distinct
|
|
34
|
-
return super(operation, column_name, distinct) if super_method
|
|
32
|
+
def execute_simple_calculation(operation, column_name, distinct)
|
|
35
33
|
operation = operation.to_s.downcase
|
|
36
34
|
if operation == "average"
|
|
37
35
|
result = calculate_simple_average(column_name, distinct)
|
|
38
36
|
else
|
|
39
|
-
result = self.activate{ |relation| relation.
|
|
37
|
+
result = self.activate{ |relation| relation.call_super(:execute_simple_calculation, Calculations, operation, column_name, distinct) }
|
|
40
38
|
if result.is_a?(Array)
|
|
41
39
|
case operation
|
|
42
40
|
when "count", "sum"
|
|
@@ -53,7 +51,7 @@ module Switchman
|
|
|
53
51
|
|
|
54
52
|
def calculate_simple_average(column_name, distinct)
|
|
55
53
|
# See activerecord#execute_simple_calculation
|
|
56
|
-
relation =
|
|
54
|
+
relation = except(:order)
|
|
57
55
|
column = aggregate_column(column_name)
|
|
58
56
|
relation.select_values = [operation_over_aggregate_column(column, "average", distinct).as("average"),
|
|
59
57
|
operation_over_aggregate_column(column, "count", distinct).as("count")]
|
|
@@ -61,12 +59,12 @@ module Switchman
|
|
|
61
59
|
initial_results = relation.activate{ |rel| klass.connection.select_all(rel) }
|
|
62
60
|
if initial_results.is_a?(Array)
|
|
63
61
|
initial_results.each do |r|
|
|
64
|
-
r["average"] = type_cast_calculated_value(r["average"],
|
|
65
|
-
r["count"] = type_cast_calculated_value(r["count"],
|
|
62
|
+
r["average"] = type_cast_calculated_value(r["average"], type_for(column_name), "average")
|
|
63
|
+
r["count"] = type_cast_calculated_value(r["count"], type_for(column_name), "count")
|
|
66
64
|
end
|
|
67
65
|
result = initial_results.map{|r| r["average"] * r["count"]}.sum / initial_results.map{|r| r["count"]}.sum
|
|
68
66
|
else
|
|
69
|
-
result = type_cast_calculated_value(initial_results.first["average"],
|
|
67
|
+
result = type_cast_calculated_value(initial_results.first["average"], type_for(column_name), "average")
|
|
70
68
|
end
|
|
71
69
|
result
|
|
72
70
|
end
|
|
@@ -76,7 +74,7 @@ module Switchman
|
|
|
76
74
|
opts = grouped_calculation_options(operation.to_s.downcase, column_name, distinct)
|
|
77
75
|
|
|
78
76
|
relation = build_grouped_calculation_relation(opts)
|
|
79
|
-
target_shard = Shard.current(:
|
|
77
|
+
target_shard = Shard.current(:primary)
|
|
80
78
|
|
|
81
79
|
rows = relation.activate do |rel, shard|
|
|
82
80
|
calculated_data = klass.connection.select_all(rel)
|
|
@@ -89,14 +87,14 @@ module Switchman
|
|
|
89
87
|
|
|
90
88
|
calculated_data.map do |row|
|
|
91
89
|
row[opts[:aggregate_alias]] = type_cast_calculated_value(
|
|
92
|
-
row[opts[:aggregate_alias]],
|
|
90
|
+
row[opts[:aggregate_alias]], type_for(opts[:column_name]), opts[:operation])
|
|
93
91
|
row['count'] = row['count'].to_i if opts[:operation] == 'average'
|
|
94
92
|
|
|
95
|
-
opts[:group_columns].each do |aliaz,
|
|
93
|
+
opts[:group_columns].each do |aliaz, type, column_name|
|
|
96
94
|
if opts[:associated] && (aliaz == opts[:group_aliases].first)
|
|
97
95
|
row[aliaz] = key_records[Shard.relative_id_for(row[aliaz], shard, target_shard)]
|
|
98
96
|
elsif column_name && @klass.sharded_column?(column_name)
|
|
99
|
-
row[aliaz] = Shard.relative_id_for(type_cast_calculated_value(row[aliaz],
|
|
97
|
+
row[aliaz] = Shard.relative_id_for(type_cast_calculated_value(row[aliaz], type), shard, target_shard)
|
|
100
98
|
end
|
|
101
99
|
end
|
|
102
100
|
row
|
|
@@ -112,10 +110,6 @@ module Switchman
|
|
|
112
110
|
field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last
|
|
113
111
|
end
|
|
114
112
|
|
|
115
|
-
def column_or_type_for(field)
|
|
116
|
-
::Rails.version < '4.2' ? column_for(field) : type_for(field)
|
|
117
|
-
end
|
|
118
|
-
|
|
119
113
|
def grouped_calculation_options(operation, column_name, distinct)
|
|
120
114
|
opts = {:operation => operation, :column_name => column_name, :distinct => distinct}
|
|
121
115
|
|
|
@@ -129,9 +123,10 @@ module Switchman
|
|
|
129
123
|
group_fields = group_attrs
|
|
130
124
|
end
|
|
131
125
|
|
|
132
|
-
|
|
126
|
+
# to_s is because Rails 5 returns a string but Rails 6 returns a symbol.
|
|
127
|
+
group_aliases = group_fields.map { |field| column_alias_for(field.downcase.to_s).to_s }
|
|
133
128
|
group_columns = group_aliases.zip(group_fields).map { |aliaz, field|
|
|
134
|
-
[aliaz,
|
|
129
|
+
[aliaz, type_for(field), column_name_for(field)]
|
|
135
130
|
}
|
|
136
131
|
opts.merge!(:association => association, :associated => associated,
|
|
137
132
|
:group_aliases => group_aliases, :group_columns => group_columns,
|
|
@@ -167,7 +162,7 @@ module Switchman
|
|
|
167
162
|
'count', opts[:distinct]).as('count')
|
|
168
163
|
end
|
|
169
164
|
|
|
170
|
-
haves =
|
|
165
|
+
haves = having_clause.send(:predicates)
|
|
171
166
|
select_values += select_values unless haves.empty?
|
|
172
167
|
select_values.concat opts[:group_fields].zip(opts[:group_aliases]).map { |field,aliaz|
|
|
173
168
|
if field.respond_to?(:as)
|
|
@@ -1,29 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'switchman/connection_pool_proxy'
|
|
2
4
|
|
|
3
5
|
module Switchman
|
|
4
6
|
module ActiveRecord
|
|
5
7
|
module ConnectionHandler
|
|
6
|
-
def self.make_sharing_automagic(config)
|
|
7
|
-
key = config[:adapter] == 'postgresql' ? :schema_search_path : :database
|
|
8
|
-
|
|
8
|
+
def self.make_sharing_automagic(config, shard = Shard.current)
|
|
9
9
|
# only load the shard name from the db if we have to
|
|
10
|
-
if
|
|
10
|
+
if !config[:shard_name]
|
|
11
11
|
# we may not be able to connect to this shard yet, cause it might be an empty database server
|
|
12
|
-
|
|
12
|
+
shard = shard.call if shard.is_a?(Proc)
|
|
13
|
+
shard_name = shard.name rescue nil
|
|
13
14
|
return unless shard_name
|
|
14
15
|
|
|
15
16
|
config[:shard_name] ||= shard_name
|
|
16
17
|
end
|
|
17
|
-
|
|
18
|
-
if !config[key] || config[key] == shard_name
|
|
19
|
-
# this may truncate the schema_search_path if it was not specified in database.yml
|
|
20
|
-
# but that's what our old behavior was anyway, so I guess it's okay
|
|
21
|
-
config[key] = '%{shard_name}'
|
|
22
|
-
end
|
|
23
18
|
end
|
|
24
19
|
|
|
25
|
-
def establish_connection(
|
|
26
|
-
|
|
20
|
+
def establish_connection(spec)
|
|
21
|
+
# Just skip establishing a sharded connection if sharding isn't loaded; we'll do it again later
|
|
22
|
+
# This only can happen when loading ActiveRecord::Base; after everything is loaded Shard will
|
|
23
|
+
# be defined and this will actually establish a connection
|
|
24
|
+
return unless defined?(Shard)
|
|
25
|
+
pool = super
|
|
27
26
|
|
|
28
27
|
# this is the first place that the adapter would have been required; but now we
|
|
29
28
|
# need this addition ASAP since it will be called when loading the default shard below
|
|
@@ -32,45 +31,48 @@ module Switchman
|
|
|
32
31
|
::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(ActiveRecord::PostgreSQLAdapter)
|
|
33
32
|
end
|
|
34
33
|
|
|
35
|
-
# AR3 uses the name, AR4 uses the model
|
|
36
|
-
model = case owner
|
|
37
|
-
when String
|
|
38
|
-
owner.constantize
|
|
39
|
-
when Class
|
|
40
|
-
owner
|
|
41
|
-
else
|
|
42
|
-
raise "unknown owner #{owner}"
|
|
43
|
-
end
|
|
44
|
-
pool = owner_to_pool[owner.name]
|
|
45
|
-
|
|
46
34
|
first_time = !Shard.instance_variable_get(:@default)
|
|
47
35
|
if first_time
|
|
48
36
|
# Have to cache the default shard before we insert sharding, otherwise the first access
|
|
49
37
|
# to sharding will recurse onto itself trying to access column information
|
|
50
38
|
Shard.default
|
|
51
39
|
|
|
40
|
+
config = pool.spec.config
|
|
52
41
|
# automatically change config to allow for sharing connections with simple config
|
|
53
|
-
ConnectionHandler.make_sharing_automagic(
|
|
42
|
+
ConnectionHandler.make_sharing_automagic(config)
|
|
54
43
|
ConnectionHandler.make_sharing_automagic(Shard.default.database_server.config)
|
|
55
44
|
|
|
56
|
-
::
|
|
45
|
+
if ::Rails.version < '6.0'
|
|
46
|
+
::ActiveRecord::Base.configurations[::Rails.env] = config.stringify_keys
|
|
47
|
+
else
|
|
48
|
+
# Adopted from the deprecated code that currently lives in rails proper
|
|
49
|
+
remaining_configs = ::ActiveRecord::Base.configurations.configurations.reject { |db_config| db_config.env_name == ::Rails.env }
|
|
50
|
+
new_config = ::ActiveRecord::DatabaseConfigurations.new(::Rails.env => config.stringify_keys).configurations
|
|
51
|
+
new_configs = remaining_configs + new_config
|
|
52
|
+
|
|
53
|
+
::ActiveRecord::Base.configurations = new_configs
|
|
54
|
+
end
|
|
55
|
+
else
|
|
56
|
+
# this is probably wrong now
|
|
57
|
+
Shard.default.remove_instance_variable(:@name) if Shard.default.instance_variable_defined?(:@name)
|
|
57
58
|
end
|
|
58
|
-
@shard_connection_pools ||= { [:master, Shard.default.database_server.shareable? ? ::Rails.env : Shard.default] => pool}
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
@shard_connection_pools ||= { [:primary, Shard.default.database_server.shareable? ? ::Rails.env : Shard.default] => pool}
|
|
61
|
+
|
|
62
|
+
category = pool.spec.name.to_sym
|
|
63
|
+
proxy = ConnectionPoolProxy.new(category,
|
|
61
64
|
pool,
|
|
62
65
|
@shard_connection_pools)
|
|
63
|
-
owner_to_pool[
|
|
64
|
-
class_to_pool.clear
|
|
66
|
+
owner_to_pool[pool.spec.name] = proxy
|
|
65
67
|
|
|
66
68
|
if first_time
|
|
67
|
-
if Shard.default.database_server.config[:
|
|
68
|
-
Shard.default.database_server.
|
|
69
|
+
if Shard.default.database_server.config[:prefer_secondary]
|
|
70
|
+
Shard.default.database_server.guard!
|
|
69
71
|
end
|
|
70
72
|
|
|
71
|
-
if Shard.default.is_a?(DefaultShard) && Shard.default.database_server.config[:
|
|
72
|
-
Shard.default.database_server.
|
|
73
|
-
Shard.default(true)
|
|
73
|
+
if Shard.default.is_a?(DefaultShard) && Shard.default.database_server.config[:secondary]
|
|
74
|
+
Shard.default.database_server.guard!
|
|
75
|
+
Shard.default(reload: true)
|
|
74
76
|
end
|
|
75
77
|
end
|
|
76
78
|
|
|
@@ -79,7 +81,7 @@ module Switchman
|
|
|
79
81
|
# DON'T do it if we're not the current connection handler - that means
|
|
80
82
|
# we're in the middle of switching environments, and we don't want to
|
|
81
83
|
# establish a connection with incorrect settings
|
|
82
|
-
if (
|
|
84
|
+
if [:primary, :unsharded].include?(category) && self == ::ActiveRecord::Base.connection_handler && !first_time
|
|
83
85
|
Shard.default(reload: true, with_fallback: true)
|
|
84
86
|
proxy.disconnect!
|
|
85
87
|
end
|
|
@@ -89,12 +91,15 @@ module Switchman
|
|
|
89
91
|
if Shard.default.is_a?(Shard)
|
|
90
92
|
DatabaseServer.all.each do |server|
|
|
91
93
|
next if server == Shard.default.database_server
|
|
92
|
-
|
|
93
|
-
shard
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
|
|
95
|
+
shard = nil
|
|
96
|
+
shard_proc = -> do
|
|
97
|
+
shard ||= server.shards.where(:name => nil).first
|
|
98
|
+
shard ||= Shard.new(:database_server => server)
|
|
99
|
+
shard
|
|
97
100
|
end
|
|
101
|
+
ConnectionHandler.make_sharing_automagic(server.config, shard_proc)
|
|
102
|
+
ConnectionHandler.make_sharing_automagic(proxy.current_pool.spec.config, shard_proc)
|
|
98
103
|
end
|
|
99
104
|
end
|
|
100
105
|
# we may have established some connections above trying to infer the shard's name.
|
|
@@ -106,60 +111,65 @@ module Switchman
|
|
|
106
111
|
proxy
|
|
107
112
|
end
|
|
108
113
|
|
|
109
|
-
def remove_connection(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
end
|
|
114
|
+
def remove_connection(spec_name)
|
|
115
|
+
# also remove pools based on the same spec name that are for shard category purposes
|
|
116
|
+
# can't just use delete_if, because it's a Concurrent::Map, not a Hash
|
|
117
|
+
owner_to_pool.keys.each do |k|
|
|
118
|
+
next if k == spec_name
|
|
115
119
|
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
v = owner_to_pool[k]
|
|
121
|
+
owner_to_pool.delete(k) if v.is_a?(ConnectionPoolProxy) && v.default_pool.spec.name == spec_name
|
|
122
|
+
end
|
|
118
123
|
|
|
119
|
-
|
|
120
|
-
|
|
124
|
+
# unwrap the pool from inside a ConnectionPoolProxy
|
|
125
|
+
pool = owner_to_pool[spec_name]
|
|
126
|
+
owner_to_pool[spec_name] = pool.default_pool if pool.is_a?(ConnectionPoolProxy)
|
|
127
|
+
|
|
128
|
+
# now let Rails do its thing with the data type it expects
|
|
129
|
+
super
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def retrieve_connection_pool(spec_name)
|
|
133
|
+
owner_to_pool.fetch(spec_name) do
|
|
134
|
+
if ancestor_pool = pool_from_any_process_for(spec_name)
|
|
121
135
|
# A connection was established in an ancestor process that must have
|
|
122
136
|
# subsequently forked. We can't reuse the connection, but we can copy
|
|
123
137
|
# the specification and establish a new connection with it.
|
|
124
|
-
if ancestor_pool.is_a?(ConnectionPoolProxy)
|
|
125
|
-
|
|
138
|
+
spec = if ancestor_pool.is_a?(ConnectionPoolProxy)
|
|
139
|
+
ancestor_pool.default_pool.spec
|
|
126
140
|
else
|
|
127
|
-
|
|
141
|
+
ancestor_pool.spec
|
|
142
|
+
end
|
|
143
|
+
# avoid copying "duplicate" pools that implement shard categories.
|
|
144
|
+
# they'll have a spec.name of primary, but a spec_name of something else, like unsharded
|
|
145
|
+
if spec.name == spec_name
|
|
146
|
+
pool = establish_connection(spec.to_hash)
|
|
147
|
+
pool.instance_variable_set(:@schema_cache, ancestor_pool.schema_cache) if ancestor_pool.schema_cache
|
|
148
|
+
next pool
|
|
128
149
|
end
|
|
129
|
-
else
|
|
130
|
-
owner_to_pool[owner.name] = nil
|
|
131
|
-
end
|
|
132
|
-
}
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def retrieve_connection_pool(klass)
|
|
136
|
-
class_to_pool[klass.name] ||= begin
|
|
137
|
-
original_klass = klass
|
|
138
|
-
until pool = pool_for(klass)
|
|
139
|
-
klass = klass.superclass
|
|
140
|
-
break unless klass <= Base
|
|
141
150
|
end
|
|
142
151
|
|
|
143
|
-
if
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
152
|
+
if spec_name != "primary"
|
|
153
|
+
primary_pool = retrieve_connection_pool("primary")
|
|
154
|
+
if primary_pool.is_a?(ConnectionPoolProxy)
|
|
155
|
+
pool = ConnectionPoolProxy.new(spec_name.to_sym, primary_pool.default_pool, @shard_connection_pools)
|
|
156
|
+
pool.schema_cache.copy_references(primary_pool.schema_cache)
|
|
157
|
+
owner_to_pool[spec_name] = pool
|
|
158
|
+
else
|
|
159
|
+
primary_pool
|
|
160
|
+
end
|
|
161
|
+
else
|
|
162
|
+
owner_to_pool[spec_name] = nil
|
|
150
163
|
end
|
|
151
|
-
|
|
152
|
-
class_to_pool[original_klass.name] = pool
|
|
153
164
|
end
|
|
154
165
|
end
|
|
155
166
|
|
|
156
167
|
def clear_idle_connections!(since_when)
|
|
157
|
-
|
|
158
|
-
connection_pools.values.each{ |pool| pool.clear_idle_connections!(since_when) }
|
|
168
|
+
connection_pool_list.each{ |pool| pool.clear_idle_connections!(since_when) }
|
|
159
169
|
end
|
|
160
170
|
|
|
161
171
|
def switchman_connection_pool_proxies
|
|
162
|
-
|
|
172
|
+
owner_to_pool.values.uniq.select{|p| p.is_a?(ConnectionPoolProxy)}
|
|
163
173
|
end
|
|
164
174
|
|
|
165
175
|
private
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'switchman/errors'
|
|
2
4
|
|
|
3
5
|
module Switchman
|
|
4
6
|
module ActiveRecord
|
|
5
7
|
module ConnectionPool
|
|
6
8
|
def shard
|
|
7
|
-
Thread.current[
|
|
9
|
+
Thread.current[tls_key] || Shard.default
|
|
8
10
|
end
|
|
9
11
|
|
|
10
12
|
def shard=(value)
|
|
11
|
-
Thread.current[
|
|
13
|
+
Thread.current[tls_key] = value
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
def default_schema
|
|
@@ -32,19 +34,14 @@ module Switchman
|
|
|
32
34
|
conn
|
|
33
35
|
end
|
|
34
36
|
|
|
35
|
-
def connection
|
|
36
|
-
conn = super
|
|
37
|
+
def connection(switch_shard: true)
|
|
38
|
+
conn = super()
|
|
37
39
|
raise NonExistentShardError if shard.new_record?
|
|
38
|
-
switch_database(conn) if conn.shard != self.shard
|
|
40
|
+
switch_database(conn) if conn.shard != self.shard && switch_shard
|
|
39
41
|
conn
|
|
40
42
|
end
|
|
41
43
|
|
|
42
|
-
def release_connection(with_id =
|
|
43
|
-
with_id ||= if ::Rails.version >= '5'
|
|
44
|
-
Thread.current
|
|
45
|
-
else
|
|
46
|
-
current_connection_id
|
|
47
|
-
end
|
|
44
|
+
def release_connection(with_id = Thread.current)
|
|
48
45
|
super(with_id)
|
|
49
46
|
|
|
50
47
|
if spec.config[:idle_timeout]
|
|
@@ -52,6 +49,20 @@ module Switchman
|
|
|
52
49
|
end
|
|
53
50
|
end
|
|
54
51
|
|
|
52
|
+
def remove_shard!(shard)
|
|
53
|
+
synchronize do
|
|
54
|
+
# The shard might be currently active, so we need to update our own shard
|
|
55
|
+
if self.shard == shard
|
|
56
|
+
self.shard = Shard.default
|
|
57
|
+
end
|
|
58
|
+
# Update out any connections that may be using this shard
|
|
59
|
+
@connections.each do |conn|
|
|
60
|
+
# This will also update the connection's shard to the default shard
|
|
61
|
+
switch_database(conn) if conn.shard == shard
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
55
66
|
def clear_idle_connections!(since_when)
|
|
56
67
|
synchronize do
|
|
57
68
|
@connections.reject! do |conn|
|
|
@@ -75,20 +86,14 @@ module Switchman
|
|
|
75
86
|
end
|
|
76
87
|
|
|
77
88
|
spec.config[:shard_name] = self.shard.name
|
|
78
|
-
case conn.adapter_name
|
|
79
|
-
when 'MySQL', 'Mysql2'
|
|
80
|
-
conn.execute("USE #{spec.config[:database]}")
|
|
81
|
-
when 'PostgreSQL'
|
|
82
|
-
if conn.schema_search_path != spec.config[:schema_search_path]
|
|
83
|
-
conn.schema_search_path = spec.config[:schema_search_path]
|
|
84
|
-
end
|
|
85
|
-
when 'SQLite'
|
|
86
|
-
# This is an artifact of the adapter modifying the path to be an absolute path when it is instantiated; just let it slide
|
|
87
|
-
else
|
|
88
|
-
raise("Cannot switch databases on same DatabaseServer with adapter type: #{conn.adapter_name}. Limit one Shard per DatabaseServer.")
|
|
89
|
-
end
|
|
90
89
|
conn.shard = shard
|
|
91
90
|
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def tls_key
|
|
95
|
+
"#{object_id}_shard".to_sym
|
|
96
|
+
end
|
|
92
97
|
end
|
|
93
98
|
end
|
|
94
99
|
end
|
|
@@ -1,31 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Switchman
|
|
2
4
|
module ActiveRecord
|
|
3
5
|
module FinderMethods
|
|
4
6
|
def find_one(id)
|
|
7
|
+
return super(id) unless klass.integral_id?
|
|
8
|
+
|
|
5
9
|
if shard_source_value != :implicit
|
|
6
|
-
|
|
10
|
+
current_shard = Shard.current(klass.shard_category)
|
|
11
|
+
result = self.activate do |relation, shard|
|
|
12
|
+
current_id = Shard.relative_id_for(id, current_shard, shard)
|
|
13
|
+
# current_id will be nil for non-integral id
|
|
14
|
+
next unless current_id
|
|
15
|
+
# skip the shard if the object can't be on it. unless we're only looking at one shard;
|
|
16
|
+
# we might be expecting a shadow object
|
|
17
|
+
next if current_id > Shard::IDS_PER_SHARD && self.all_shards.length > 1
|
|
18
|
+
relation.call_super(:find_one, FinderMethods, current_id)
|
|
19
|
+
end
|
|
20
|
+
if result.is_a?(Array)
|
|
21
|
+
result = result.first
|
|
22
|
+
end
|
|
23
|
+
# we may have skipped all shards
|
|
24
|
+
raise_record_not_found_exception!(id, 0, 1) unless result
|
|
25
|
+
return result
|
|
7
26
|
end
|
|
8
27
|
|
|
9
28
|
local_id, shard = Shard.local_id_for(id)
|
|
10
29
|
if shard
|
|
11
|
-
|
|
12
|
-
# find_one uses binds, so we can't depend on QueryMethods
|
|
13
|
-
# catching it
|
|
14
|
-
begin
|
|
15
|
-
old_shard_value = shard_value
|
|
16
|
-
self.shard_value = shard
|
|
17
|
-
super(local_id)
|
|
18
|
-
ensure
|
|
19
|
-
self.shard_value = old_shard_value
|
|
20
|
-
end
|
|
21
|
-
else
|
|
22
|
-
shard.activate { super(local_id) }
|
|
23
|
-
end
|
|
30
|
+
shard.activate { super(local_id) }
|
|
24
31
|
else
|
|
25
|
-
super
|
|
32
|
+
super(id)
|
|
26
33
|
end
|
|
27
34
|
end
|
|
28
35
|
|
|
36
|
+
def find_some_ordered(ids)
|
|
37
|
+
current_shard = Shard.current(klass.shard_category)
|
|
38
|
+
ids = ids.map{|id| Shard.relative_id_for(id, current_shard, current_shard)}
|
|
39
|
+
super(ids)
|
|
40
|
+
end
|
|
41
|
+
|
|
29
42
|
def find_or_instantiator_by_attributes(match, attributes, *args)
|
|
30
43
|
primary_shard.activate { super }
|
|
31
44
|
end
|
|
@@ -34,13 +47,10 @@ module Switchman
|
|
|
34
47
|
conditions = conditions.id if ::ActiveRecord::Base === conditions
|
|
35
48
|
return false if !conditions
|
|
36
49
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
join_dependency = construct_join_dependency_for_association_find
|
|
42
|
-
relation = construct_relation_for_association_find(join_dependency)
|
|
43
|
-
end
|
|
50
|
+
relation = ::Rails.version >= "5.2" ?
|
|
51
|
+
apply_join_dependency(eager_loading: false) :
|
|
52
|
+
apply_join_dependency(self, construct_join_dependency)
|
|
53
|
+
return false if ::ActiveRecord::NullRelation === relation
|
|
44
54
|
|
|
45
55
|
relation = relation.except(:select, :order).select("1 AS one").limit(1)
|
|
46
56
|
|
|
@@ -51,12 +61,11 @@ module Switchman
|
|
|
51
61
|
relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none
|
|
52
62
|
end
|
|
53
63
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
raise if ::Rails.version >= '4.1' || !(::ActiveRecord::ThrowResult === $!)
|
|
64
|
+
relation.activate do |shard_rel|
|
|
65
|
+
return true if ::Rails.version >= "5.2" ?
|
|
66
|
+
connection.select_value(shard_rel.arel, "#{name} Exists") :
|
|
67
|
+
connection.select_value(shard_rel, "#{name} Exists", shard_rel.bound_attributes)
|
|
68
|
+
end
|
|
60
69
|
false
|
|
61
70
|
end
|
|
62
71
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Switchman
|
|
2
4
|
module ActiveRecord
|
|
3
5
|
module LogSubscriber
|
|
@@ -8,35 +10,28 @@ module Switchman
|
|
|
8
10
|
|
|
9
11
|
payload = event.payload
|
|
10
12
|
|
|
11
|
-
return if
|
|
13
|
+
return if ::ActiveRecord::LogSubscriber::IGNORE_PAYLOAD_NAMES.include?(payload[:name])
|
|
12
14
|
|
|
13
|
-
name =
|
|
15
|
+
name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
|
|
16
|
+
name = "CACHE #{name}" if payload[:cached]
|
|
14
17
|
sql = payload[:sql].squeeze(' '.freeze)
|
|
15
18
|
binds = nil
|
|
16
19
|
shard = payload[:shard]
|
|
17
20
|
shard = " [#{shard[:database_server_id]}:#{shard[:id]} #{shard[:env]}]" if shard
|
|
18
21
|
|
|
19
22
|
unless (payload[:binds] || []).empty?
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
use_old_format = (::Rails.version < '5.1.5')
|
|
24
|
+
args = use_old_format ?
|
|
25
|
+
[payload[:binds], payload[:type_casted_binds]] :
|
|
26
|
+
[payload[:type_casted_binds]]
|
|
27
|
+
casted_params = type_casted_binds(*args)
|
|
28
|
+
binds = " " + payload[:binds].zip(casted_params).map { |attr, value|
|
|
29
|
+
render_bind(attr, value)
|
|
26
30
|
}.inspect
|
|
27
31
|
end
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
sql = color(sql, sql_color(sql), true)
|
|
32
|
-
else
|
|
33
|
-
if odd?
|
|
34
|
-
name = color(name, self.class::CYAN, true)
|
|
35
|
-
sql = color(sql, nil, true)
|
|
36
|
-
else
|
|
37
|
-
name = color(name, self.class::MAGENTA, true)
|
|
38
|
-
end
|
|
39
|
-
end
|
|
33
|
+
name = colorize_payload_name(name, payload[:name])
|
|
34
|
+
sql = color(sql, sql_color(sql), true)
|
|
40
35
|
|
|
41
36
|
debug " #{name} #{sql}#{binds}#{shard}"
|
|
42
37
|
end
|