switchman 1.13.3 → 2.2.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/app/models/switchman/shard.rb +712 -11
- data/db/migrate/20130328212039_create_switchman_shards.rb +2 -0
- data/db/migrate/20130328224244_create_default_shard.rb +3 -1
- data/db/migrate/20161206323434_add_back_default_string_limits_switchman.rb +2 -0
- data/db/migrate/20180828183945_add_default_shard_index.rb +3 -1
- data/db/migrate/20180828192111_add_timestamps_to_shards.rb +2 -0
- data/db/migrate/20190114212900_add_unique_name_indexes.rb +2 -0
- data/lib/switchman/action_controller/caching.rb +2 -0
- data/lib/switchman/active_record/abstract_adapter.rb +6 -4
- data/lib/switchman/active_record/association.rb +45 -16
- data/lib/switchman/active_record/attribute_methods.rb +43 -17
- data/lib/switchman/active_record/base.rb +60 -20
- data/lib/switchman/active_record/batches.rb +2 -0
- data/lib/switchman/active_record/calculations.rb +5 -2
- data/lib/switchman/active_record/connection_handler.rb +48 -25
- data/lib/switchman/active_record/connection_pool.rb +19 -15
- data/lib/switchman/active_record/finder_methods.rb +2 -0
- data/lib/switchman/active_record/log_subscriber.rb +10 -12
- data/lib/switchman/active_record/migration.rb +48 -2
- data/lib/switchman/active_record/model_schema.rb +3 -1
- data/lib/switchman/active_record/persistence.rb +13 -2
- data/lib/switchman/active_record/postgresql_adapter.rb +149 -139
- data/lib/switchman/active_record/predicate_builder.rb +3 -1
- data/lib/switchman/active_record/query_cache.rb +19 -107
- data/lib/switchman/active_record/query_methods.rb +25 -3
- data/lib/switchman/active_record/reflection.rb +21 -8
- data/lib/switchman/active_record/relation.rb +66 -10
- data/lib/switchman/active_record/spawn_methods.rb +2 -0
- data/lib/switchman/active_record/statement_cache.rb +6 -25
- 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 +2 -0
- data/lib/switchman/active_support/cache.rb +18 -0
- data/lib/switchman/arel.rb +2 -0
- data/lib/switchman/call_super.rb +2 -0
- data/lib/switchman/connection_pool_proxy.rb +44 -22
- data/lib/switchman/database_server.rb +34 -19
- data/lib/switchman/default_shard.rb +3 -0
- data/lib/switchman/engine.rb +20 -15
- 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 +9 -7
- data/lib/switchman/rails.rb +2 -0
- data/lib/switchman/schema_cache.rb +11 -1
- data/lib/switchman/sharded_instrumenter.rb +3 -1
- data/lib/switchman/standard_error.rb +3 -1
- data/lib/switchman/test_helper.rb +7 -11
- data/lib/switchman/version.rb +3 -1
- data/lib/switchman.rb +5 -1
- data/lib/tasks/switchman.rake +24 -17
- metadata +53 -26
- data/app/models/switchman/shard_internal.rb +0 -714
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Switchman
|
|
2
4
|
module ActiveRecord
|
|
3
5
|
module QueryMethods
|
|
@@ -227,6 +229,14 @@ module Switchman
|
|
|
227
229
|
connection.with_local_table_name { super }
|
|
228
230
|
end
|
|
229
231
|
|
|
232
|
+
def arel_column(columns)
|
|
233
|
+
connection.with_local_table_name { super }
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def table_name_matches?(from)
|
|
237
|
+
connection.with_global_table_name { super }
|
|
238
|
+
end
|
|
239
|
+
|
|
230
240
|
# semi-private
|
|
231
241
|
public
|
|
232
242
|
def transpose_predicates(predicates,
|
|
@@ -236,6 +246,18 @@ module Switchman
|
|
|
236
246
|
binds: nil,
|
|
237
247
|
dup_binds_on_mutation: false)
|
|
238
248
|
result = predicates.map do |predicate|
|
|
249
|
+
if ::Rails.version >= '5.2' && predicate.is_a?(::Arel::Nodes::And)
|
|
250
|
+
new_predicates, _binds = transpose_predicates(predicate.children, source_shard, target_shard,
|
|
251
|
+
remove_nonlocal_primary_keys,
|
|
252
|
+
binds: binds,
|
|
253
|
+
dup_binds_on_mutation: dup_binds_on_mutation)
|
|
254
|
+
next (if new_predicates == predicate.children
|
|
255
|
+
predicate
|
|
256
|
+
else
|
|
257
|
+
::Arel::Nodes::And.new(new_predicates)
|
|
258
|
+
end)
|
|
259
|
+
end
|
|
260
|
+
|
|
239
261
|
next predicate unless predicate.is_a?(::Arel::Nodes::Binary)
|
|
240
262
|
next predicate unless predicate.left.is_a?(::Arel::Attributes::Attribute)
|
|
241
263
|
relation, column = relation_and_column(predicate.left)
|
|
@@ -244,7 +266,7 @@ module Switchman
|
|
|
244
266
|
remove = true if type == :primary &&
|
|
245
267
|
remove_nonlocal_primary_keys &&
|
|
246
268
|
predicate.left.relation.model == klass &&
|
|
247
|
-
predicate.is_a?(::Arel::Nodes::Equality)
|
|
269
|
+
(predicate.is_a?(::Arel::Nodes::Equality) || predicate.is_a?(::Arel::Nodes::In))
|
|
248
270
|
|
|
249
271
|
current_source_shard =
|
|
250
272
|
if source_shard
|
|
@@ -259,7 +281,7 @@ module Switchman
|
|
|
259
281
|
new_right_value =
|
|
260
282
|
case predicate.right
|
|
261
283
|
when Array
|
|
262
|
-
predicate.right.map {|val| transpose_predicate_value(val, current_source_shard, target_shard, type, remove) }
|
|
284
|
+
predicate.right.map {|val| transpose_predicate_value(val, current_source_shard, target_shard, type, remove).presence }.compact
|
|
263
285
|
else
|
|
264
286
|
transpose_predicate_value(predicate.right, current_source_shard, target_shard, type, remove)
|
|
265
287
|
end
|
|
@@ -334,7 +356,7 @@ module Switchman
|
|
|
334
356
|
value
|
|
335
357
|
else
|
|
336
358
|
local_id = Shard.relative_id_for(current_id, current_shard, target_shard) || current_id
|
|
337
|
-
|
|
359
|
+
return nil if remove_non_local_ids && local_id.is_a?(Integer) && local_id > Shard::IDS_PER_SHARD
|
|
338
360
|
if current_id != local_id
|
|
339
361
|
# make a new bind param
|
|
340
362
|
::Arel::Nodes::BindParam.new(query_att.class.new(query_att.name, local_id, query_att.type))
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Switchman
|
|
2
4
|
module ActiveRecord
|
|
3
5
|
module Reflection
|
|
@@ -26,15 +28,26 @@ module Switchman
|
|
|
26
28
|
# this technically belongs on AssociationReflection, but we put it on
|
|
27
29
|
# ThroughReflection as well, instead of delegating to its internal
|
|
28
30
|
# HasManyAssociation, losing its proper `klass`
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
if ::Rails.version < '6.0.4'
|
|
32
|
+
def association_scope_cache(conn, owner, &block)
|
|
33
|
+
key = conn.prepared_statements
|
|
34
|
+
if polymorphic?
|
|
35
|
+
key = [key, owner._read_attribute(@foreign_type)]
|
|
36
|
+
end
|
|
37
|
+
key = [key, shard(owner).id].flatten
|
|
38
|
+
@association_scope_cache[key] ||= @scope_lock.synchronize {
|
|
39
|
+
@association_scope_cache[key] ||= (::Rails.version >= "5.2" ? ::ActiveRecord::StatementCache.create(conn, &block) : block.call)
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
else
|
|
43
|
+
def association_scope_cache(klass, owner, &block)
|
|
44
|
+
key = self
|
|
45
|
+
if polymorphic?
|
|
46
|
+
key = [key, owner._read_attribute(@foreign_type)]
|
|
47
|
+
end
|
|
48
|
+
key = [key, shard(owner).id].flatten
|
|
49
|
+
klass.cached_find_by_statement(key, &block)
|
|
33
50
|
end
|
|
34
|
-
key = [key, shard(owner).id].flatten
|
|
35
|
-
@association_scope_cache[key] ||= @scope_lock.synchronize {
|
|
36
|
-
@association_scope_cache[key] ||= (::Rails.version >= "5.2" ? ::ActiveRecord::StatementCache.create(conn, &block) : block.call)
|
|
37
|
-
}
|
|
38
51
|
end
|
|
39
52
|
end
|
|
40
53
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Switchman
|
|
2
4
|
module ActiveRecord
|
|
3
5
|
module Relation
|
|
@@ -5,7 +7,7 @@ module Switchman
|
|
|
5
7
|
klass::SINGLE_VALUE_METHODS.concat [ :shard, :shard_source ]
|
|
6
8
|
end
|
|
7
9
|
|
|
8
|
-
def initialize(
|
|
10
|
+
def initialize(*, **)
|
|
9
11
|
super
|
|
10
12
|
self.shard_value = Shard.current(klass ? klass.shard_category : :primary) unless shard_value
|
|
11
13
|
self.shard_source_value = :implicit unless shard_source_value
|
|
@@ -17,7 +19,7 @@ module Switchman
|
|
|
17
19
|
result
|
|
18
20
|
end
|
|
19
21
|
|
|
20
|
-
def merge(*
|
|
22
|
+
def merge(*)
|
|
21
23
|
relation = super
|
|
22
24
|
if relation.shard_value != self.shard_value && relation.shard_source_value == :implicit
|
|
23
25
|
relation.shard_value = self.shard_value
|
|
@@ -26,15 +28,15 @@ module Switchman
|
|
|
26
28
|
relation
|
|
27
29
|
end
|
|
28
30
|
|
|
29
|
-
def new(
|
|
31
|
+
def new(*, &block)
|
|
30
32
|
primary_shard.activate(klass.shard_category) { super }
|
|
31
33
|
end
|
|
32
34
|
|
|
33
|
-
def create(
|
|
35
|
+
def create(*, &block)
|
|
34
36
|
primary_shard.activate(klass.shard_category) { super }
|
|
35
37
|
end
|
|
36
38
|
|
|
37
|
-
def create!(
|
|
39
|
+
def create!(*, &block)
|
|
38
40
|
primary_shard.activate(klass.shard_category) { super }
|
|
39
41
|
end
|
|
40
42
|
|
|
@@ -60,7 +62,7 @@ module Switchman
|
|
|
60
62
|
%I{update_all delete_all}.each do |method|
|
|
61
63
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
62
64
|
def #{method}(*args)
|
|
63
|
-
result = self.activate { |relation| relation.call_super(#{method.inspect}, Relation, *args) }
|
|
65
|
+
result = self.activate(unordered: true) { |relation| relation.call_super(#{method.inspect}, Relation, *args) }
|
|
64
66
|
result = result.sum if result.is_a?(Array)
|
|
65
67
|
result
|
|
66
68
|
end
|
|
@@ -95,7 +97,7 @@ module Switchman
|
|
|
95
97
|
end
|
|
96
98
|
end
|
|
97
99
|
|
|
98
|
-
def activate(&block)
|
|
100
|
+
def activate(unordered: false, &block)
|
|
99
101
|
shards = all_shards
|
|
100
102
|
if (Array === shards && shards.length == 1)
|
|
101
103
|
if shards.first == DefaultShard || shards.first == Shard.current(klass.shard_category)
|
|
@@ -104,10 +106,64 @@ module Switchman
|
|
|
104
106
|
shards.first.activate(klass.shard_category) { yield(self, shards.first) }
|
|
105
107
|
end
|
|
106
108
|
else
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
result_count = 0
|
|
110
|
+
can_order = false
|
|
111
|
+
result = Shard.with_each_shard(shards, [klass.shard_category]) do
|
|
112
|
+
# don't even query other shards if we're already past the limit
|
|
113
|
+
next if limit_value && result_count >= limit_value && order_values.empty?
|
|
114
|
+
|
|
115
|
+
relation = shard(Shard.current(klass.shard_category), :to_a)
|
|
116
|
+
# do a minimal query if possible
|
|
117
|
+
relation = relation.limit(limit_value - result_count) if limit_value && !result_count.zero? && order_values.empty?
|
|
118
|
+
|
|
119
|
+
shard_results = relation.activate(&block)
|
|
120
|
+
|
|
121
|
+
if shard_results.present? && !unordered
|
|
122
|
+
can_order ||= can_order_cross_shard_results? unless order_values.empty?
|
|
123
|
+
raise OrderOnMultiShardQuery if !can_order && !order_values.empty? && result_count.positive?
|
|
124
|
+
|
|
125
|
+
result_count += shard_results.is_a?(Array) ? shard_results.length : 1
|
|
126
|
+
end
|
|
127
|
+
shard_results
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
result = reorder_cross_shard_results(result) if can_order
|
|
131
|
+
result.slice!(limit_value..-1) if limit_value
|
|
132
|
+
result
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def can_order_cross_shard_results?
|
|
137
|
+
# we only presume to be able to post-sort the most basic of orderings
|
|
138
|
+
order_values.all? { |ov| ov.is_a?(::Arel::Nodes::Ordering) && ov.expr.is_a?(::Arel::Attributes::Attribute) }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def reorder_cross_shard_results(results)
|
|
142
|
+
results.sort! do |l, r|
|
|
143
|
+
result = 0
|
|
144
|
+
order_values.each do |ov|
|
|
145
|
+
if !l.is_a?(::ActiveRecord::Base)
|
|
146
|
+
a, b = l, r
|
|
147
|
+
elsif l.respond_to?(ov.expr.name)
|
|
148
|
+
a = l.send(ov.expr.name)
|
|
149
|
+
b = r.send(ov.expr.name)
|
|
150
|
+
else
|
|
151
|
+
a = l.attributes[ov.expr.name]
|
|
152
|
+
b = r.attributes[ov.expr.name]
|
|
153
|
+
end
|
|
154
|
+
next if a == b
|
|
155
|
+
|
|
156
|
+
if a.nil? || b.nil?
|
|
157
|
+
result = 1 if a.nil?
|
|
158
|
+
result *= -1 if ov.is_a?(::Arel::Nodes::Descending)
|
|
159
|
+
else
|
|
160
|
+
result = a <=> b
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
result *= -1 if ov.is_a?(::Arel::Nodes::Descending)
|
|
164
|
+
break unless result.zero?
|
|
110
165
|
end
|
|
166
|
+
result
|
|
111
167
|
end
|
|
112
168
|
end
|
|
113
169
|
end
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Switchman
|
|
2
4
|
module ActiveRecord
|
|
3
5
|
module StatementCache
|
|
4
6
|
module ClassMethods
|
|
5
|
-
def create(connection, block
|
|
7
|
+
def create(connection, &block)
|
|
6
8
|
relation = block.call ::ActiveRecord::StatementCache::Params.new
|
|
7
9
|
|
|
8
10
|
if ::Rails.version >= "5.2"
|
|
@@ -46,37 +48,16 @@ module Switchman
|
|
|
46
48
|
bind_values = bind_map.bind(params, current_shard, target_shard)
|
|
47
49
|
|
|
48
50
|
target_shard.activate(klass.shard_category) do
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
klass.find_by_sql(sql, bind_values)
|
|
52
|
-
else
|
|
53
|
-
sql = generic_query_builder(connection).sql_for(bind_values, connection)
|
|
54
|
-
klass.find_by_sql(sql, bind_values)
|
|
55
|
-
end
|
|
51
|
+
sql = qualified_query_builder(target_shard, klass).sql_for(bind_values, connection)
|
|
52
|
+
klass.find_by_sql(sql, bind_values)
|
|
56
53
|
end
|
|
57
54
|
end
|
|
58
55
|
|
|
59
|
-
if ::Rails.version < '5.
|
|
60
|
-
def generic_query_builder(connection)
|
|
61
|
-
@query_builder ||= connection.cacheable_query(@arel)
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def qualified_query_builder(shard, klass)
|
|
65
|
-
@qualified_query_builders[shard.id] ||= klass.connection.cacheable_query(@arel)
|
|
66
|
-
end
|
|
67
|
-
elsif ::Rails.version < '5.2'
|
|
68
|
-
def generic_query_builder(connection)
|
|
69
|
-
@query_builder ||= connection.cacheable_query(self.class, @arel)
|
|
70
|
-
end
|
|
71
|
-
|
|
56
|
+
if ::Rails.version < '5.2'
|
|
72
57
|
def qualified_query_builder(shard, klass)
|
|
73
58
|
@qualified_query_builders[shard.id] ||= klass.connection.cacheable_query(self.class, @arel)
|
|
74
59
|
end
|
|
75
60
|
else
|
|
76
|
-
def generic_query_builder(connection)
|
|
77
|
-
@query_builder ||= connection.cacheable_query(self.class, @arel).first
|
|
78
|
-
end
|
|
79
|
-
|
|
80
61
|
def qualified_query_builder(shard, klass)
|
|
81
62
|
@qualified_query_builders[shard.id] ||= klass.connection.cacheable_query(self.class, @arel).first
|
|
82
63
|
end
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Switchman
|
|
2
4
|
module ActiveRecord
|
|
3
5
|
module TableDefinition
|
|
4
|
-
def column(name, type,
|
|
5
|
-
Engine.foreign_key_check(name, type,
|
|
6
|
+
def column(name, type, limit: nil, **)
|
|
7
|
+
Engine.foreign_key_check(name, type, limit: limit)
|
|
6
8
|
super
|
|
7
9
|
end
|
|
8
10
|
end
|
|
@@ -1,13 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Switchman
|
|
2
4
|
module ActiveSupport
|
|
3
5
|
module Cache
|
|
4
6
|
module ClassMethods
|
|
5
7
|
def lookup_store(*store_options)
|
|
6
8
|
store = super
|
|
9
|
+
# can't use defined?, because it's a _ruby_ autoloaded constant,
|
|
10
|
+
# so just checking that will cause it to get required
|
|
11
|
+
if store.class.name == "ActiveSupport::Cache::RedisCacheStore" && !::ActiveSupport::Cache::RedisCacheStore.ancestors.include?(RedisCacheStore)
|
|
12
|
+
::ActiveSupport::Cache::RedisCacheStore.prepend(RedisCacheStore)
|
|
13
|
+
end
|
|
7
14
|
store.options[:namespace] ||= lambda { Shard.current.default? ? nil : "shard_#{Shard.current.id}" }
|
|
8
15
|
store
|
|
9
16
|
end
|
|
10
17
|
end
|
|
18
|
+
|
|
19
|
+
module RedisCacheStore
|
|
20
|
+
def clear(namespace: nil, **)
|
|
21
|
+
# RedisCacheStore tries to be smart and only clear the cache under your namespace, if you have one set
|
|
22
|
+
# unfortunately, it uses the keys command, which is extraordinarily inefficient in a large redis instance
|
|
23
|
+
# fortunately, we can assume we control the entire instance, because we set up the namespacing, so just
|
|
24
|
+
# always unset it temporarily for clear calls
|
|
25
|
+
namespace = nil
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
end
|
|
11
29
|
end
|
|
12
30
|
end
|
|
13
31
|
end
|
data/lib/switchman/arel.rb
CHANGED
data/lib/switchman/call_super.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'switchman/schema_cache'
|
|
2
4
|
|
|
3
5
|
module Switchman
|
|
@@ -23,20 +25,27 @@ module Switchman
|
|
|
23
25
|
@category = category
|
|
24
26
|
@default_pool = default_pool
|
|
25
27
|
@connection_pools = shard_connection_pools
|
|
26
|
-
@schema_cache =
|
|
28
|
+
@schema_cache = default_pool.get_schema_cache(nil) if ::Rails.version >= '6'
|
|
29
|
+
@schema_cache = SchemaCache.new(self) unless @schema_cache.is_a?(SchemaCache)
|
|
30
|
+
if ::Rails.version >= '6'
|
|
31
|
+
@default_pool.set_schema_cache(@schema_cache)
|
|
32
|
+
@connection_pools.each_value do |pool|
|
|
33
|
+
pool.set_schema_cache(@schema_cache)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
27
36
|
end
|
|
28
37
|
|
|
29
38
|
def active_shard
|
|
30
39
|
Shard.current(@category)
|
|
31
40
|
end
|
|
32
41
|
|
|
33
|
-
def
|
|
34
|
-
::Rails.env.test? ? :
|
|
42
|
+
def active_guard_rail_environment
|
|
43
|
+
::Rails.env.test? ? :primary : active_shard.database_server.guard_rail_environment
|
|
35
44
|
end
|
|
36
45
|
|
|
37
46
|
def current_pool
|
|
38
47
|
current_active_shard = active_shard
|
|
39
|
-
pool = self.default_pool if current_active_shard.database_server == Shard.default.database_server &&
|
|
48
|
+
pool = self.default_pool if current_active_shard.database_server == Shard.default.database_server && active_guard_rail_environment == :primary && (current_active_shard.default? || current_active_shard.database_server.shareable?)
|
|
40
49
|
pool = @connection_pools[pool_key] ||= create_pool unless pool
|
|
41
50
|
pool.shard = current_active_shard
|
|
42
51
|
pool
|
|
@@ -46,21 +55,21 @@ module Switchman
|
|
|
46
55
|
connection_pools.map(&:connections).inject([], &:+)
|
|
47
56
|
end
|
|
48
57
|
|
|
49
|
-
def connection
|
|
58
|
+
def connection(switch_shard: true)
|
|
50
59
|
pool = current_pool
|
|
51
60
|
begin
|
|
52
|
-
connection = pool.connection
|
|
53
|
-
connection.instance_variable_set(:@schema_cache, @schema_cache)
|
|
61
|
+
connection = pool.connection(switch_shard: switch_shard)
|
|
62
|
+
connection.instance_variable_set(:@schema_cache, @schema_cache) unless ::Rails.version >= '6'
|
|
54
63
|
connection
|
|
55
64
|
rescue ConnectionError
|
|
56
|
-
raise if active_shard.database_server == Shard.default.database_server &&
|
|
57
|
-
configs = active_shard.database_server.config(
|
|
65
|
+
raise if active_shard.database_server == Shard.default.database_server && active_guard_rail_environment == :primary
|
|
66
|
+
configs = active_shard.database_server.config(active_guard_rail_environment)
|
|
58
67
|
raise unless configs.is_a?(Array)
|
|
59
68
|
configs.each_with_index do |config, idx|
|
|
60
69
|
pool = create_pool(config.dup)
|
|
61
70
|
begin
|
|
62
71
|
connection = pool.connection
|
|
63
|
-
connection.instance_variable_set(:@schema_cache, @schema_cache)
|
|
72
|
+
connection.instance_variable_set(:@schema_cache, @schema_cache) unless ::Rails.version >= '6'
|
|
64
73
|
rescue ConnectionError
|
|
65
74
|
raise if idx == configs.length - 1
|
|
66
75
|
next
|
|
@@ -71,6 +80,14 @@ module Switchman
|
|
|
71
80
|
end
|
|
72
81
|
end
|
|
73
82
|
|
|
83
|
+
def get_schema_cache(_connection)
|
|
84
|
+
@schema_cache
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def set_schema_cache(cache)
|
|
88
|
+
@schema_cache.copy_values(cache)
|
|
89
|
+
end
|
|
90
|
+
|
|
74
91
|
%w{release_connection
|
|
75
92
|
disconnect!
|
|
76
93
|
flush!
|
|
@@ -98,6 +115,10 @@ module Switchman
|
|
|
98
115
|
connection_pools.each { |pool| pool.clear_idle_connections!(since_when) }
|
|
99
116
|
end
|
|
100
117
|
|
|
118
|
+
def remove_shard!(shard)
|
|
119
|
+
connection_pools.each { |pool| pool.remove_shard!(shard) }
|
|
120
|
+
end
|
|
121
|
+
|
|
101
122
|
protected
|
|
102
123
|
|
|
103
124
|
def connection_pools
|
|
@@ -105,7 +126,7 @@ module Switchman
|
|
|
105
126
|
end
|
|
106
127
|
|
|
107
128
|
def pool_key
|
|
108
|
-
[
|
|
129
|
+
[active_guard_rail_environment,
|
|
109
130
|
active_shard.database_server.shareable? ? active_shard.database_server.pool_key : active_shard]
|
|
110
131
|
end
|
|
111
132
|
|
|
@@ -113,7 +134,7 @@ module Switchman
|
|
|
113
134
|
shard = active_shard
|
|
114
135
|
unless config
|
|
115
136
|
if shard != Shard.default
|
|
116
|
-
config = shard.database_server.config(
|
|
137
|
+
config = shard.database_server.config(active_guard_rail_environment)
|
|
117
138
|
config = config.first if config.is_a?(Array)
|
|
118
139
|
config = config.dup
|
|
119
140
|
else
|
|
@@ -123,27 +144,28 @@ module Switchman
|
|
|
123
144
|
# different models could be using different configs on the default
|
|
124
145
|
# shard, and database server wouldn't know about that
|
|
125
146
|
config = default_pool.spec.instance_variable_get(:@config)
|
|
126
|
-
if config[
|
|
127
|
-
config = config.merge(config[
|
|
128
|
-
elsif config[
|
|
129
|
-
config = config.merge(config[
|
|
147
|
+
if config[active_guard_rail_environment].is_a?(Hash)
|
|
148
|
+
config = config.merge(config[active_guard_rail_environment])
|
|
149
|
+
elsif config[active_guard_rail_environment].is_a?(Array)
|
|
150
|
+
config = config.merge(config[active_guard_rail_environment].first)
|
|
130
151
|
else
|
|
131
152
|
config = config.dup
|
|
132
153
|
end
|
|
133
154
|
end
|
|
134
155
|
end
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
156
|
+
spec = ::ActiveRecord::ConnectionAdapters::ConnectionSpecification.new(
|
|
157
|
+
default_pool.spec.name,
|
|
158
|
+
config,
|
|
159
|
+
"#{config[:adapter]}_connection"
|
|
160
|
+
)
|
|
138
161
|
# unfortunately the AR code that does this require logic can't really be
|
|
139
162
|
# called in isolation
|
|
140
163
|
require "active_record/connection_adapters/#{config[:adapter]}_adapter"
|
|
141
164
|
|
|
142
165
|
::ActiveRecord::ConnectionAdapters::ConnectionPool.new(spec).tap do |pool|
|
|
143
166
|
pool.shard = shard
|
|
144
|
-
if ::Rails.version >= '
|
|
145
|
-
|
|
146
|
-
end
|
|
167
|
+
pool.set_schema_cache(@schema_cache) if ::Rails.version >= '6'
|
|
168
|
+
pool.enable_query_cache! if !@connection_pools.empty? && @connection_pools.first.last.query_cache_enabled
|
|
147
169
|
end
|
|
148
170
|
end
|
|
149
171
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "securerandom"
|
|
2
4
|
|
|
3
5
|
module Switchman
|
|
@@ -40,8 +42,14 @@ module Switchman
|
|
|
40
42
|
def database_servers
|
|
41
43
|
unless @database_servers
|
|
42
44
|
@database_servers = {}.with_indifferent_access
|
|
43
|
-
::
|
|
44
|
-
|
|
45
|
+
if ::Rails.version >= '6.0'
|
|
46
|
+
::ActiveRecord::Base.configurations.configurations.each do |config|
|
|
47
|
+
@database_servers[config.env_name] = DatabaseServer.new(config.env_name, config.config)
|
|
48
|
+
end
|
|
49
|
+
else
|
|
50
|
+
::ActiveRecord::Base.configurations.each do |(id, config)|
|
|
51
|
+
@database_servers[id] = DatabaseServer.new(id, config)
|
|
52
|
+
end
|
|
45
53
|
end
|
|
46
54
|
end
|
|
47
55
|
@database_servers
|
|
@@ -63,12 +71,12 @@ module Switchman
|
|
|
63
71
|
@fake
|
|
64
72
|
end
|
|
65
73
|
|
|
66
|
-
def config(environment = :
|
|
74
|
+
def config(environment = :primary)
|
|
67
75
|
@configs[environment] ||= begin
|
|
68
76
|
if @config[environment].is_a?(Array)
|
|
69
77
|
@config[environment].map do |config|
|
|
70
78
|
config = @config.merge((config || {}).symbolize_keys)
|
|
71
|
-
# make sure
|
|
79
|
+
# make sure GuardRail doesn't get any brilliant ideas about choosing the first possible server
|
|
72
80
|
config.delete(environment)
|
|
73
81
|
config
|
|
74
82
|
end
|
|
@@ -80,33 +88,33 @@ module Switchman
|
|
|
80
88
|
end
|
|
81
89
|
end
|
|
82
90
|
|
|
83
|
-
def
|
|
84
|
-
@
|
|
91
|
+
def guard_rail_environment
|
|
92
|
+
@guard_rail_environment || ::GuardRail.environment
|
|
85
93
|
end
|
|
86
94
|
|
|
87
95
|
# locks this db to a specific environment, except for
|
|
88
96
|
# when doing writes (then it falls back to the current
|
|
89
|
-
# value of
|
|
90
|
-
def
|
|
91
|
-
@
|
|
97
|
+
# value of GuardRail.environment)
|
|
98
|
+
def guard!(environment = :secondary)
|
|
99
|
+
@guard_rail_environment = environment
|
|
92
100
|
end
|
|
93
101
|
|
|
94
|
-
def
|
|
95
|
-
@
|
|
102
|
+
def unguard!
|
|
103
|
+
@guard_rail_environment = nil
|
|
96
104
|
end
|
|
97
105
|
|
|
98
|
-
def
|
|
99
|
-
old_env = @
|
|
100
|
-
|
|
106
|
+
def unguard
|
|
107
|
+
old_env = @guard_rail_environment
|
|
108
|
+
unguard!
|
|
101
109
|
yield
|
|
102
110
|
ensure
|
|
103
|
-
|
|
111
|
+
guard!(old_env)
|
|
104
112
|
end
|
|
105
113
|
|
|
106
114
|
def shareable?
|
|
107
115
|
@shareable_environment_key ||= []
|
|
108
|
-
environment =
|
|
109
|
-
explicit_user = ::
|
|
116
|
+
environment = guard_rail_environment
|
|
117
|
+
explicit_user = ::GuardRail.global_config[:username]
|
|
110
118
|
return @shareable if @shareable_environment_key == [environment, explicit_user]
|
|
111
119
|
@shareable_environment_key = [environment, explicit_user]
|
|
112
120
|
if explicit_user
|
|
@@ -179,13 +187,18 @@ module Switchman
|
|
|
179
187
|
shard = Shard.create!(:id => shard_id,
|
|
180
188
|
:name => name,
|
|
181
189
|
:database_server => self)
|
|
190
|
+
schema_already_existed = false
|
|
182
191
|
|
|
183
192
|
begin
|
|
184
193
|
self.class.creating_new_shard = true
|
|
185
194
|
shard.activate(*Shard.categories) do
|
|
186
|
-
::
|
|
195
|
+
::GuardRail.activate(:deploy) do
|
|
187
196
|
begin
|
|
188
197
|
if create_statement
|
|
198
|
+
if (::ActiveRecord::Base.connection.select_value("SELECT 1 FROM pg_namespace WHERE nspname=#{::ActiveRecord::Base.connection.quote(name)}"))
|
|
199
|
+
schema_already_existed = true
|
|
200
|
+
raise "This schema already exists; cannot overwrite"
|
|
201
|
+
end
|
|
189
202
|
Array(create_statement.call).each do |stmt|
|
|
190
203
|
::ActiveRecord::Base.connection.execute(stmt)
|
|
191
204
|
end
|
|
@@ -223,7 +236,9 @@ module Switchman
|
|
|
223
236
|
shard
|
|
224
237
|
rescue
|
|
225
238
|
shard.destroy
|
|
226
|
-
|
|
239
|
+
unless schema_already_existed
|
|
240
|
+
shard.drop_database rescue nil
|
|
241
|
+
end
|
|
227
242
|
reset_column_information unless create_schema == false rescue nil
|
|
228
243
|
raise
|
|
229
244
|
ensure
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'switchman/database_server'
|
|
2
4
|
|
|
3
5
|
module Switchman
|
|
4
6
|
class DefaultShard
|
|
5
7
|
def id; 'default'; end
|
|
8
|
+
alias cache_key id
|
|
6
9
|
def activate(*categories); yield; end
|
|
7
10
|
def activate!(*categories); end
|
|
8
11
|
def default?; true; end
|