switchman 3.1.0 → 3.5.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 +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/associations.rb +97 -17
- data/lib/switchman/active_record/attribute_methods.rb +72 -43
- data/lib/switchman/active_record/base.rb +107 -21
- data/lib/switchman/active_record/calculations.rb +37 -33
- data/lib/switchman/active_record/connection_pool.rb +21 -2
- data/lib/switchman/active_record/database_configurations.rb +12 -7
- data/lib/switchman/active_record/finder_methods.rb +1 -1
- data/lib/switchman/active_record/log_subscriber.rb +2 -2
- data/lib/switchman/active_record/migration.rb +35 -8
- data/lib/switchman/active_record/persistence.rb +8 -0
- data/lib/switchman/active_record/postgresql_adapter.rb +11 -10
- data/lib/switchman/active_record/query_cache.rb +1 -1
- data/lib/switchman/active_record/query_methods.rb +172 -132
- data/lib/switchman/active_record/relation.rb +21 -11
- data/lib/switchman/active_record/spawn_methods.rb +2 -2
- data/lib/switchman/active_record/statement_cache.rb +9 -5
- data/lib/switchman/active_record/tasks/database_tasks.rb +1 -1
- data/lib/switchman/active_record/test_fixtures.rb +19 -16
- data/lib/switchman/active_support/cache.rb +4 -1
- data/lib/switchman/arel.rb +6 -6
- data/lib/switchman/call_super.rb +8 -2
- data/lib/switchman/database_server.rb +21 -26
- data/lib/switchman/default_shard.rb +3 -3
- data/lib/switchman/engine.rb +33 -18
- data/lib/switchman/environment.rb +2 -2
- data/lib/switchman/errors.rb +13 -0
- data/lib/switchman/guard_rail/relation.rb +2 -1
- data/lib/switchman/parallel.rb +2 -2
- data/lib/switchman/r_spec_helper.rb +10 -10
- data/lib/switchman/shard.rb +49 -32
- data/lib/switchman/sharded_instrumenter.rb +5 -1
- data/lib/switchman/shared_schema_cache.rb +11 -0
- data/lib/switchman/test_helper.rb +1 -1
- data/lib/switchman/version.rb +1 -1
- data/lib/switchman.rb +10 -4
- data/lib/tasks/switchman.rake +42 -39
- metadata +24 -9
|
@@ -22,16 +22,20 @@ module Switchman
|
|
|
22
22
|
@integral_id
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
%w[transaction insert_all upsert_all].each do |method|
|
|
26
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
27
|
+
def #{method}(*, **)
|
|
28
|
+
if self != ::ActiveRecord::Base && current_scope
|
|
29
|
+
current_scope.activate do
|
|
30
|
+
db = Shard.current(connection_class_for_self).database_server
|
|
31
|
+
db.unguard { super }
|
|
32
|
+
end
|
|
33
|
+
else
|
|
34
|
+
db = Shard.current(connection_class_for_self).database_server
|
|
35
|
+
db.unguard { super }
|
|
36
|
+
end
|
|
30
37
|
end
|
|
31
|
-
|
|
32
|
-
db = Shard.current(connection_class_for_self).database_server
|
|
33
|
-
db.unguard { super }
|
|
34
|
-
end
|
|
38
|
+
RUBY
|
|
35
39
|
end
|
|
36
40
|
|
|
37
41
|
def reset_column_information
|
|
@@ -63,7 +67,10 @@ module Switchman
|
|
|
63
67
|
end
|
|
64
68
|
|
|
65
69
|
def establish_connection(config_or_env = nil)
|
|
66
|
-
|
|
70
|
+
if config_or_env.is_a?(Symbol) && config_or_env != ::Rails.env.to_sym
|
|
71
|
+
raise ArgumentError,
|
|
72
|
+
"establish connection cannot be used on the non-current shard/role"
|
|
73
|
+
end
|
|
67
74
|
|
|
68
75
|
# Ensure we don't randomly surprise change the connection parms associated with a shard/role
|
|
69
76
|
config_or_env = nil if config_or_env == ::Rails.env.to_sym
|
|
@@ -78,9 +85,15 @@ module Switchman
|
|
|
78
85
|
end
|
|
79
86
|
|
|
80
87
|
def connected_to_stack
|
|
81
|
-
|
|
88
|
+
has_own_stack = if ::Rails.version < "7.0"
|
|
89
|
+
Thread.current.thread_variable?(:ar_connected_to_stack)
|
|
90
|
+
else
|
|
91
|
+
::ActiveSupport::IsolatedExecutionState.key?(:active_record_connected_to_stack)
|
|
92
|
+
end
|
|
82
93
|
|
|
83
94
|
ret = super
|
|
95
|
+
return ret if has_own_stack
|
|
96
|
+
|
|
84
97
|
DatabaseServer.guard_servers
|
|
85
98
|
ret
|
|
86
99
|
end
|
|
@@ -92,10 +105,13 @@ module Switchman
|
|
|
92
105
|
sharded_role = nil
|
|
93
106
|
connected_to_stack.reverse_each do |hash|
|
|
94
107
|
shard_role = hash.dig(:shard_roles, target_shard)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
108
|
+
unless shard_role &&
|
|
109
|
+
(hash[:klasses].include?(::ActiveRecord::Base) || hash[:klasses].include?(connection_class_for_self))
|
|
110
|
+
next
|
|
98
111
|
end
|
|
112
|
+
|
|
113
|
+
sharded_role = shard_role
|
|
114
|
+
break
|
|
99
115
|
end
|
|
100
116
|
# Allow a shard-specific role to be reverted to regular inheritance
|
|
101
117
|
return sharded_role if sharded_role && sharded_role != :_switchman_inherit
|
|
@@ -115,13 +131,15 @@ module Switchman
|
|
|
115
131
|
|
|
116
132
|
def current_switchman_shard
|
|
117
133
|
connected_to_stack.reverse_each do |hash|
|
|
118
|
-
|
|
134
|
+
if hash[:switchman_shard] && hash[:klasses].include?(connection_class_for_self)
|
|
135
|
+
return hash[:switchman_shard]
|
|
136
|
+
end
|
|
119
137
|
end
|
|
120
138
|
|
|
121
139
|
Shard.default
|
|
122
140
|
end
|
|
123
141
|
|
|
124
|
-
if ::Rails.version <
|
|
142
|
+
if ::Rails.version < "7.0"
|
|
125
143
|
def connection_class_for_self
|
|
126
144
|
connection_classes
|
|
127
145
|
end
|
|
@@ -138,30 +156,86 @@ module Switchman
|
|
|
138
156
|
else
|
|
139
157
|
Shard.current(self.class.connection_class_for_self)
|
|
140
158
|
end
|
|
159
|
+
|
|
160
|
+
@loaded_from_shard ||= Shard.current(self.class.connection_class_for_self)
|
|
161
|
+
readonly! if shadow_record? && !Switchman.config[:writable_shadow_records]
|
|
141
162
|
super
|
|
142
163
|
end
|
|
143
164
|
|
|
165
|
+
def shadow_record?
|
|
166
|
+
pkey = self[self.class.primary_key]
|
|
167
|
+
return false unless self.class.sharded_column?(self.class.primary_key) && pkey
|
|
168
|
+
|
|
169
|
+
pkey > Shard::IDS_PER_SHARD
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def save_shadow_record(new_attrs: attributes, target_shard: Shard.current)
|
|
173
|
+
return if target_shard == shard
|
|
174
|
+
|
|
175
|
+
shadow_attrs = {}
|
|
176
|
+
new_attrs.each do |attr, value|
|
|
177
|
+
shadow_attrs[attr] = if self.class.sharded_column?(attr)
|
|
178
|
+
Shard.relative_id_for(value, shard, target_shard)
|
|
179
|
+
else
|
|
180
|
+
value
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
target_shard.activate do
|
|
184
|
+
self.class.upsert(shadow_attrs, unique_by: self.class.primary_key)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def destroy_shadow_records(target_shards: [Shard.current])
|
|
189
|
+
raise Errors::ShadowRecordError, "Cannot be called on a shadow record." if shadow_record?
|
|
190
|
+
|
|
191
|
+
unless self.class.sharded_column?(self.class.primary_key)
|
|
192
|
+
raise Errors::MethodUnsupportedForUnshardedTableError,
|
|
193
|
+
"Cannot be called on a record belonging to an unsharded table."
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
Array(target_shards).each do |target_shard|
|
|
197
|
+
next if target_shard == shard
|
|
198
|
+
|
|
199
|
+
target_shard.activate { self.class.where("id = ?", global_id).delete_all }
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Returns "the shard that this record was actually loaded from" , as
|
|
204
|
+
# opposed to "the shard this record belongs on", which might be
|
|
205
|
+
# different if this is a shadow record.
|
|
206
|
+
def loaded_from_shard
|
|
207
|
+
@loaded_from_shard || shard
|
|
208
|
+
end
|
|
209
|
+
|
|
144
210
|
def shard
|
|
145
|
-
@shard ||
|
|
211
|
+
@shard || fallback_shard
|
|
146
212
|
end
|
|
147
213
|
|
|
148
214
|
def shard=(new_shard)
|
|
149
215
|
raise ::ActiveRecord::ReadOnlyRecord if !new_record? || @shard_set_in_stone
|
|
150
216
|
|
|
151
|
-
|
|
217
|
+
if shard == new_shard
|
|
218
|
+
@loaded_from_shard = new_shard
|
|
219
|
+
return
|
|
220
|
+
end
|
|
152
221
|
|
|
153
222
|
attributes.each do |attr, value|
|
|
154
223
|
self[attr] = Shard.relative_id_for(value, shard, new_shard) if self.class.sharded_column?(attr)
|
|
155
224
|
end
|
|
225
|
+
@loaded_from_shard = new_shard
|
|
156
226
|
@shard = new_shard
|
|
157
227
|
end
|
|
158
228
|
|
|
159
229
|
def save(*, **)
|
|
230
|
+
raise Errors::ManuallyCreatedShadowRecordError if creating_shadow_record?
|
|
231
|
+
|
|
160
232
|
@shard_set_in_stone = true
|
|
161
233
|
super
|
|
162
234
|
end
|
|
163
235
|
|
|
164
236
|
def save!(*, **)
|
|
237
|
+
raise Errors::ManuallyCreatedShadowRecordError if creating_shadow_record?
|
|
238
|
+
|
|
165
239
|
@shard_set_in_stone = true
|
|
166
240
|
super
|
|
167
241
|
end
|
|
@@ -193,7 +267,7 @@ module Switchman
|
|
|
193
267
|
end
|
|
194
268
|
|
|
195
269
|
def hash
|
|
196
|
-
self.class.sharded_primary_key? ? self.class
|
|
270
|
+
self.class.sharded_primary_key? ? [self.class, global_id].hash : super
|
|
197
271
|
end
|
|
198
272
|
|
|
199
273
|
def to_param
|
|
@@ -214,8 +288,10 @@ module Switchman
|
|
|
214
288
|
|
|
215
289
|
def id_for_database
|
|
216
290
|
if self.class.sharded_primary_key?
|
|
217
|
-
# It's an int, so
|
|
218
|
-
# In theory we should do
|
|
291
|
+
# It's an int, so it's safe to just return it without passing it
|
|
292
|
+
# through anything else. In theory we should do
|
|
293
|
+
# `@attributes[@primary_key].type.serialize(id)`, but that seems to
|
|
294
|
+
# have surprising side-effects
|
|
219
295
|
id
|
|
220
296
|
else
|
|
221
297
|
super
|
|
@@ -242,6 +318,16 @@ module Switchman
|
|
|
242
318
|
self.class.connection_class_for_self
|
|
243
319
|
end
|
|
244
320
|
end
|
|
321
|
+
|
|
322
|
+
private
|
|
323
|
+
|
|
324
|
+
def fallback_shard
|
|
325
|
+
Shard.current(self.class.connection_class_for_self) || Shard.default
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def creating_shadow_record?
|
|
329
|
+
new_record? && shadow_record?
|
|
330
|
+
end
|
|
245
331
|
end
|
|
246
332
|
end
|
|
247
333
|
end
|
|
@@ -28,7 +28,7 @@ module Switchman
|
|
|
28
28
|
|
|
29
29
|
def execute_simple_calculation(operation, column_name, distinct)
|
|
30
30
|
operation = operation.to_s.downcase
|
|
31
|
-
if operation ==
|
|
31
|
+
if operation == "average"
|
|
32
32
|
result = calculate_simple_average(column_name, distinct)
|
|
33
33
|
else
|
|
34
34
|
result = activate do |relation|
|
|
@@ -36,11 +36,11 @@ module Switchman
|
|
|
36
36
|
end
|
|
37
37
|
if result.is_a?(Array)
|
|
38
38
|
case operation
|
|
39
|
-
when
|
|
39
|
+
when "count", "sum"
|
|
40
40
|
result = result.sum
|
|
41
|
-
when
|
|
41
|
+
when "minimum"
|
|
42
42
|
result = result.min
|
|
43
|
-
when
|
|
43
|
+
when "maximum"
|
|
44
44
|
result = result.max
|
|
45
45
|
end
|
|
46
46
|
end
|
|
@@ -52,20 +52,20 @@ module Switchman
|
|
|
52
52
|
# See activerecord#execute_simple_calculation
|
|
53
53
|
relation = except(:order)
|
|
54
54
|
column = aggregate_column(column_name)
|
|
55
|
-
relation.select_values = [operation_over_aggregate_column(column,
|
|
56
|
-
operation_over_aggregate_column(column,
|
|
55
|
+
relation.select_values = [operation_over_aggregate_column(column, "average", distinct).as("average"),
|
|
56
|
+
operation_over_aggregate_column(column, "count", distinct).as("count")]
|
|
57
57
|
|
|
58
58
|
initial_results = relation.activate { |rel| klass.connection.select_all(rel) }
|
|
59
59
|
if initial_results.is_a?(Array)
|
|
60
60
|
initial_results.each do |r|
|
|
61
|
-
r[
|
|
62
|
-
r[
|
|
61
|
+
r["average"] = type_cast_calculated_value_switchman(r["average"], column_name, "average")
|
|
62
|
+
r["count"] = type_cast_calculated_value_switchman(r["count"], column_name, "count")
|
|
63
63
|
end
|
|
64
|
-
result = initial_results.
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
result = initial_results.sum { |r| r["average"] * r["count"] } / initial_results.sum do |r|
|
|
65
|
+
r["count"]
|
|
66
|
+
end
|
|
67
67
|
else
|
|
68
|
-
result = type_cast_calculated_value_switchman(initial_results.first[
|
|
68
|
+
result = type_cast_calculated_value_switchman(initial_results.first["average"], column_name, "average")
|
|
69
69
|
end
|
|
70
70
|
result
|
|
71
71
|
end
|
|
@@ -90,7 +90,7 @@ module Switchman
|
|
|
90
90
|
row[opts[:aggregate_alias]] = type_cast_calculated_value_switchman(
|
|
91
91
|
row[opts[:aggregate_alias]], column_name, opts[:operation]
|
|
92
92
|
)
|
|
93
|
-
row[
|
|
93
|
+
row["count"] = row["count"].to_i if opts[:operation] == "average"
|
|
94
94
|
|
|
95
95
|
opts[:group_columns].each do |aliaz, _type, group_column_name|
|
|
96
96
|
if opts[:associated] && (aliaz == opts[:group_aliases].first)
|
|
@@ -109,7 +109,7 @@ module Switchman
|
|
|
109
109
|
private
|
|
110
110
|
|
|
111
111
|
def type_cast_calculated_value_switchman(value, column_name, operation)
|
|
112
|
-
if ::Rails.version <
|
|
112
|
+
if ::Rails.version < "7.0"
|
|
113
113
|
type_cast_calculated_value(value, operation) do |val|
|
|
114
114
|
column = aggregate_column(column_name)
|
|
115
115
|
type ||= column.try(:type_caster) ||
|
|
@@ -125,7 +125,7 @@ module Switchman
|
|
|
125
125
|
end
|
|
126
126
|
|
|
127
127
|
def column_name_for(field)
|
|
128
|
-
field.respond_to?(:name) ? field.name.to_s : field.to_s.split(
|
|
128
|
+
field.respond_to?(:name) ? field.name.to_s : field.to_s.split(".").last
|
|
129
129
|
end
|
|
130
130
|
|
|
131
131
|
def grouped_calculation_options(operation, column_name, distinct)
|
|
@@ -135,7 +135,8 @@ module Switchman
|
|
|
135
135
|
group_attrs = group_values
|
|
136
136
|
if group_attrs.first.respond_to?(:to_sym)
|
|
137
137
|
association = klass.reflect_on_association(group_attrs.first.to_sym)
|
|
138
|
-
|
|
138
|
+
# only count belongs_to associations
|
|
139
|
+
associated = group_attrs.size == 1 && association && association.macro == :belongs_to
|
|
139
140
|
group_fields = Array(associated ? association.foreign_key : group_attrs)
|
|
140
141
|
else
|
|
141
142
|
group_fields = group_attrs
|
|
@@ -146,18 +147,20 @@ module Switchman
|
|
|
146
147
|
group_columns = group_aliases.zip(group_fields).map do |aliaz, field|
|
|
147
148
|
[aliaz, type_for(field), column_name_for(field)]
|
|
148
149
|
end
|
|
149
|
-
opts.merge!(association: association,
|
|
150
|
-
|
|
150
|
+
opts.merge!(association: association,
|
|
151
|
+
associated: associated,
|
|
152
|
+
group_aliases: group_aliases,
|
|
153
|
+
group_columns: group_columns,
|
|
151
154
|
group_fields: group_fields)
|
|
152
155
|
|
|
153
156
|
opts
|
|
154
157
|
end
|
|
155
158
|
|
|
156
159
|
def aggregate_alias_for(operation, column_name)
|
|
157
|
-
if operation ==
|
|
158
|
-
|
|
159
|
-
elsif operation ==
|
|
160
|
-
|
|
160
|
+
if operation == "count" && column_name == :all
|
|
161
|
+
"count_all"
|
|
162
|
+
elsif operation == "average"
|
|
163
|
+
"average"
|
|
161
164
|
else
|
|
162
165
|
column_alias_for("#{operation} #{column_name}")
|
|
163
166
|
end
|
|
@@ -173,13 +176,14 @@ module Switchman
|
|
|
173
176
|
opts[:distinct]
|
|
174
177
|
).as(opts[:aggregate_alias])
|
|
175
178
|
]
|
|
176
|
-
if opts[:operation] ==
|
|
179
|
+
if opts[:operation] == "average"
|
|
177
180
|
# include count in average so we can recalculate the average
|
|
178
181
|
# across all shards if needed
|
|
179
182
|
select_values << operation_over_aggregate_column(
|
|
180
183
|
aggregate_column(opts[:column_name]),
|
|
181
|
-
|
|
182
|
-
|
|
184
|
+
"count",
|
|
185
|
+
opts[:distinct]
|
|
186
|
+
).as("count")
|
|
183
187
|
end
|
|
184
188
|
|
|
185
189
|
haves = having_clause.send(:predicates)
|
|
@@ -205,22 +209,22 @@ module Switchman
|
|
|
205
209
|
key = key.first if key.size == 1
|
|
206
210
|
value = row[opts[:aggregate_alias]]
|
|
207
211
|
|
|
208
|
-
if opts[:operation] ==
|
|
212
|
+
if opts[:operation] == "average"
|
|
209
213
|
if result.key?(key)
|
|
210
214
|
old_value, old_count = result[key]
|
|
211
|
-
new_count = old_count + row[
|
|
212
|
-
new_value = ((old_value * old_count) + (value * row[
|
|
215
|
+
new_count = old_count + row["count"]
|
|
216
|
+
new_value = ((old_value * old_count) + (value * row["count"])) / new_count
|
|
213
217
|
result[key] = [new_value, new_count]
|
|
214
218
|
else
|
|
215
|
-
result[key] = [value, row[
|
|
219
|
+
result[key] = [value, row["count"]]
|
|
216
220
|
end
|
|
217
221
|
elsif result.key?(key)
|
|
218
222
|
case opts[:operation]
|
|
219
|
-
when
|
|
223
|
+
when "count", "sum"
|
|
220
224
|
result[key] += value
|
|
221
|
-
when
|
|
225
|
+
when "minimum"
|
|
222
226
|
result[key] = value if value < result[key]
|
|
223
|
-
when
|
|
227
|
+
when "maximum"
|
|
224
228
|
result[key] = value if value > result[key]
|
|
225
229
|
end
|
|
226
230
|
else
|
|
@@ -228,7 +232,7 @@ module Switchman
|
|
|
228
232
|
end
|
|
229
233
|
end
|
|
230
234
|
|
|
231
|
-
result.transform_values!(&:first) if opts[:operation] ==
|
|
235
|
+
result.transform_values!(&:first) if opts[:operation] == "average"
|
|
232
236
|
|
|
233
237
|
result
|
|
234
238
|
end
|
|
@@ -3,6 +3,23 @@
|
|
|
3
3
|
module Switchman
|
|
4
4
|
module ActiveRecord
|
|
5
5
|
module ConnectionPool
|
|
6
|
+
def get_schema_cache(connection)
|
|
7
|
+
self.schema_cache ||= SharedSchemaCache.get_schema_cache(connection)
|
|
8
|
+
self.schema_cache.connection = connection
|
|
9
|
+
|
|
10
|
+
self.schema_cache
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# rubocop:disable Naming/AccessorMethodName override method
|
|
14
|
+
def set_schema_cache(cache)
|
|
15
|
+
schema_cache = get_schema_cache(cache.connection)
|
|
16
|
+
|
|
17
|
+
cache.instance_variables.each do |x|
|
|
18
|
+
schema_cache.instance_variable_set(x, cache.instance_variable_get(x))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
# rubocop:enable Naming/AccessorMethodName override method
|
|
22
|
+
|
|
6
23
|
def default_schema
|
|
7
24
|
connection unless @schemas
|
|
8
25
|
# default shard will not switch databases immediately, so it won't be set yet
|
|
@@ -31,7 +48,9 @@ module Switchman
|
|
|
31
48
|
end
|
|
32
49
|
|
|
33
50
|
def switch_database(conn)
|
|
34
|
-
|
|
51
|
+
if !@schemas && conn.adapter_name == "PostgreSQL" && !current_shard.database_server.config[:shard_name]
|
|
52
|
+
@schemas = conn.current_schemas
|
|
53
|
+
end
|
|
35
54
|
|
|
36
55
|
conn.shard = current_shard
|
|
37
56
|
end
|
|
@@ -39,7 +58,7 @@ module Switchman
|
|
|
39
58
|
private
|
|
40
59
|
|
|
41
60
|
def current_shard
|
|
42
|
-
::Rails.version <
|
|
61
|
+
(::Rails.version < "7.0") ? connection_klass.current_switchman_shard : connection_class.current_switchman_shard
|
|
43
62
|
end
|
|
44
63
|
|
|
45
64
|
def tls_key
|
|
@@ -10,9 +10,9 @@ module Switchman
|
|
|
10
10
|
def configs_for(include_replicas: false, name: nil, **)
|
|
11
11
|
res = super
|
|
12
12
|
if name && !include_replicas
|
|
13
|
-
return nil unless name.end_with?(
|
|
13
|
+
return nil unless name.end_with?("primary")
|
|
14
14
|
elsif !include_replicas
|
|
15
|
-
return res.select { |config| config.name.end_with?(
|
|
15
|
+
return res.select { |config| config.name.end_with?("primary") }
|
|
16
16
|
end
|
|
17
17
|
res
|
|
18
18
|
end
|
|
@@ -29,19 +29,24 @@ module Switchman
|
|
|
29
29
|
db_configs = configs.flat_map do |env_name, config|
|
|
30
30
|
# It would be nice to do the auto-fallback that we want here, but we haven't
|
|
31
31
|
# actually done that for years (or maybe ever) and it will be a big lift to get working
|
|
32
|
-
roles = config.keys.select
|
|
32
|
+
roles = config.keys.select do |k|
|
|
33
|
+
config[k].is_a?(Hash) || (config[k].is_a?(Array) && config[k].all?(Hash))
|
|
34
|
+
end
|
|
33
35
|
base_config = config.except(*roles)
|
|
34
36
|
|
|
35
37
|
name = "#{env_name}/primary"
|
|
36
|
-
name =
|
|
38
|
+
name = "primary" if env_name == default_env
|
|
37
39
|
base_db = build_db_config_from_raw_config(env_name, name, base_config)
|
|
38
40
|
[base_db] + roles.map do |role|
|
|
39
|
-
build_db_config_from_raw_config(
|
|
40
|
-
|
|
41
|
+
build_db_config_from_raw_config(
|
|
42
|
+
env_name,
|
|
43
|
+
"#{env_name}/#{role}",
|
|
44
|
+
base_config.merge(config[role].is_a?(Array) ? config[role].first : config[role])
|
|
45
|
+
)
|
|
41
46
|
end
|
|
42
47
|
end
|
|
43
48
|
|
|
44
|
-
db_configs << environment_url_config(default_env,
|
|
49
|
+
db_configs << environment_url_config(default_env, "primary", {}) unless db_configs.find(&:for_current_env?)
|
|
45
50
|
|
|
46
51
|
merge_db_environment_variables(default_env, db_configs.compact)
|
|
47
52
|
end
|
|
@@ -49,7 +49,7 @@ module Switchman
|
|
|
49
49
|
relation = apply_join_dependency(eager_loading: false)
|
|
50
50
|
return false if ::ActiveRecord::NullRelation === relation
|
|
51
51
|
|
|
52
|
-
relation = relation.except(:select, :order).select(
|
|
52
|
+
relation = relation.except(:select, :order).select("1 AS one").limit(1)
|
|
53
53
|
|
|
54
54
|
case conditions
|
|
55
55
|
when Array, Hash
|
|
@@ -14,14 +14,14 @@ module Switchman
|
|
|
14
14
|
|
|
15
15
|
name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
|
|
16
16
|
name = "CACHE #{name}" if payload[:cached]
|
|
17
|
-
sql = payload[:sql].squeeze(
|
|
17
|
+
sql = payload[:sql].squeeze(" ")
|
|
18
18
|
binds = nil
|
|
19
19
|
shard = payload[:shard]
|
|
20
20
|
shard = " [#{shard[:database_server_id]}:#{shard[:id]} #{shard[:env]}]" if shard
|
|
21
21
|
|
|
22
22
|
unless (payload[:binds] || []).empty?
|
|
23
23
|
casted_params = type_casted_binds(payload[:type_casted_binds])
|
|
24
|
-
binds =
|
|
24
|
+
binds = " " + payload[:binds].zip(casted_params).map do |attr, value|
|
|
25
25
|
render_bind(attr, value)
|
|
26
26
|
end.inspect
|
|
27
27
|
end
|
|
@@ -14,19 +14,23 @@ module Switchman
|
|
|
14
14
|
|
|
15
15
|
def connection
|
|
16
16
|
conn = super
|
|
17
|
-
|
|
17
|
+
if conn.shard != ::ActiveRecord::Base.current_switchman_shard
|
|
18
|
+
::ActiveRecord::Base.connection_pool.switch_database(conn)
|
|
19
|
+
end
|
|
18
20
|
conn
|
|
19
21
|
end
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
module Migrator
|
|
23
|
-
# significant change:
|
|
24
|
-
#
|
|
25
|
-
# name you're accessing may not be consistent
|
|
26
|
-
# to run migrations against multiple shards in the same database
|
|
27
|
-
# concurrently
|
|
25
|
+
# significant change: use the shard name instead of the database name
|
|
26
|
+
# in the lock id. Especially if you're going through pgbouncer, the
|
|
27
|
+
# database name you're accessing may not be consistent
|
|
28
28
|
def generate_migrator_advisory_lock_id
|
|
29
|
-
|
|
29
|
+
db_name_hash = Zlib.crc32(Shard.current.name)
|
|
30
|
+
shard_name_hash = ::ActiveRecord::Migrator::MIGRATOR_SALT * db_name_hash
|
|
31
|
+
# Store in internalmetadata to allow other tools to be able to lock out migrations
|
|
32
|
+
::ActiveRecord::InternalMetadata[:migrator_advisory_lock_id] = shard_name_hash
|
|
33
|
+
shard_name_hash
|
|
30
34
|
end
|
|
31
35
|
|
|
32
36
|
# significant change: strip out prefer_secondary from config
|
|
@@ -42,13 +46,36 @@ module Switchman
|
|
|
42
46
|
end
|
|
43
47
|
|
|
44
48
|
module MigrationContext
|
|
49
|
+
def migrate(...)
|
|
50
|
+
connection = ::ActiveRecord::Base.connection
|
|
51
|
+
connection_pool = ::ActiveRecord::Base.connection_pool
|
|
52
|
+
previous_schema_cache = connection_pool.get_schema_cache(connection)
|
|
53
|
+
temporary_schema_cache = ::ActiveRecord::ConnectionAdapters::SchemaCache.new(connection)
|
|
54
|
+
|
|
55
|
+
reset_column_information
|
|
56
|
+
connection_pool.set_schema_cache(temporary_schema_cache)
|
|
57
|
+
|
|
58
|
+
begin
|
|
59
|
+
super(...)
|
|
60
|
+
ensure
|
|
61
|
+
connection_pool.set_schema_cache(previous_schema_cache)
|
|
62
|
+
reset_column_information
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
45
66
|
def migrations
|
|
46
67
|
return @migrations if instance_variable_defined?(:@migrations)
|
|
47
68
|
|
|
48
69
|
migrations_cache = Thread.current[:migrations_cache] ||= {}
|
|
49
|
-
key = Digest::MD5.hexdigest(migration_files.sort.join(
|
|
70
|
+
key = Digest::MD5.hexdigest(migration_files.sort.join(","))
|
|
50
71
|
@migrations = migrations_cache[key] ||= super
|
|
51
72
|
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def reset_column_information
|
|
77
|
+
::ActiveRecord::Base.descendants.reject { |m| m <= UnshardedRecord }.each(&:reset_column_information)
|
|
78
|
+
end
|
|
52
79
|
end
|
|
53
80
|
end
|
|
54
81
|
end
|
|
@@ -16,6 +16,14 @@ module Switchman
|
|
|
16
16
|
db = shard.database_server
|
|
17
17
|
db.unguard { super }
|
|
18
18
|
end
|
|
19
|
+
|
|
20
|
+
def reload(*)
|
|
21
|
+
res = super
|
|
22
|
+
# When a shadow record is reloaded the real record is returned. So
|
|
23
|
+
# we need to ensure the loaded_from_shard is set correctly after a reload.
|
|
24
|
+
@loaded_from_shard = @shard
|
|
25
|
+
res
|
|
26
|
+
end
|
|
19
27
|
end
|
|
20
28
|
end
|
|
21
29
|
end
|
|
@@ -5,9 +5,9 @@ module Switchman
|
|
|
5
5
|
module PostgreSQLAdapter
|
|
6
6
|
# copy/paste; use quote_local_table_name
|
|
7
7
|
def create_database(name, options = {})
|
|
8
|
-
options = { encoding:
|
|
8
|
+
options = { encoding: "utf8" }.merge!(options.symbolize_keys)
|
|
9
9
|
|
|
10
|
-
option_string = options.sum(
|
|
10
|
+
option_string = options.sum("") do |key, value|
|
|
11
11
|
case key
|
|
12
12
|
when :owner
|
|
13
13
|
" OWNER = \"#{value}\""
|
|
@@ -24,7 +24,7 @@ module Switchman
|
|
|
24
24
|
when :connection_limit
|
|
25
25
|
" CONNECTION LIMIT = #{value}"
|
|
26
26
|
else
|
|
27
|
-
|
|
27
|
+
""
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
@@ -37,7 +37,7 @@ module Switchman
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def current_schemas
|
|
40
|
-
select_values(
|
|
40
|
+
select_values("SELECT * FROM unnest(current_schemas(false))")
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def extract_schema_qualified_name(string)
|
|
@@ -49,13 +49,13 @@ module Switchman
|
|
|
49
49
|
# significant change: use the shard name if no explicit schema
|
|
50
50
|
def quoted_scope(name = nil, type: nil)
|
|
51
51
|
schema, name = extract_schema_qualified_name(name)
|
|
52
|
-
type =
|
|
52
|
+
type =
|
|
53
53
|
case type # rubocop:disable Style/HashLikeCase
|
|
54
|
-
when
|
|
54
|
+
when "BASE TABLE"
|
|
55
55
|
"'r','p'"
|
|
56
|
-
when
|
|
56
|
+
when "VIEW"
|
|
57
57
|
"'v','m'"
|
|
58
|
-
when
|
|
58
|
+
when "FOREIGN TABLE"
|
|
59
59
|
"'f'"
|
|
60
60
|
end
|
|
61
61
|
scope = {}
|
|
@@ -67,7 +67,8 @@ module Switchman
|
|
|
67
67
|
|
|
68
68
|
def foreign_keys(table_name)
|
|
69
69
|
super.each do |fk|
|
|
70
|
-
to_table_qualified_name =
|
|
70
|
+
to_table_qualified_name =
|
|
71
|
+
::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(fk.to_table)
|
|
71
72
|
fk.to_table = to_table_qualified_name.identifier if to_table_qualified_name.schema == shard.name
|
|
72
73
|
end
|
|
73
74
|
end
|
|
@@ -101,7 +102,7 @@ module Switchman
|
|
|
101
102
|
|
|
102
103
|
def add_index_options(_table_name, _column_name, **)
|
|
103
104
|
index, algorithm, if_not_exists = super
|
|
104
|
-
algorithm = nil if DatabaseServer.creating_new_shard && algorithm ==
|
|
105
|
+
algorithm = nil if DatabaseServer.creating_new_shard && algorithm == "CONCURRENTLY"
|
|
105
106
|
[index, algorithm, if_not_exists]
|
|
106
107
|
end
|
|
107
108
|
|