switchman 3.4.1 → 4.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Rakefile +15 -14
- data/db/migrate/20180828183945_add_default_shard_index.rb +1 -1
- data/db/migrate/20190114212900_add_unique_name_indexes.rb +10 -4
- data/lib/switchman/active_record/abstract_adapter.rb +10 -6
- data/lib/switchman/active_record/associations.rb +71 -48
- data/lib/switchman/active_record/attribute_methods.rb +84 -37
- data/lib/switchman/active_record/base.rb +72 -41
- data/lib/switchman/active_record/calculations.rb +90 -54
- data/lib/switchman/active_record/connection_handler.rb +18 -0
- data/lib/switchman/active_record/connection_pool.rb +41 -23
- data/lib/switchman/active_record/database_configurations.rb +23 -13
- data/lib/switchman/active_record/finder_methods.rb +20 -14
- data/lib/switchman/active_record/log_subscriber.rb +3 -6
- data/lib/switchman/active_record/migration.rb +19 -19
- data/lib/switchman/active_record/pending_migration_connection.rb +17 -0
- data/lib/switchman/active_record/persistence.rb +22 -0
- data/lib/switchman/active_record/postgresql_adapter.rb +37 -22
- data/lib/switchman/active_record/predicate_builder.rb +2 -2
- data/lib/switchman/active_record/query_cache.rb +26 -17
- data/lib/switchman/active_record/query_methods.rb +148 -44
- data/lib/switchman/active_record/reflection.rb +9 -2
- data/lib/switchman/active_record/relation.rb +86 -16
- data/lib/switchman/active_record/spawn_methods.rb +3 -7
- data/lib/switchman/active_record/statement_cache.rb +4 -4
- data/lib/switchman/active_record/table_definition.rb +1 -1
- data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
- data/lib/switchman/active_record/test_fixtures.rb +71 -25
- data/lib/switchman/active_support/cache.rb +9 -4
- data/lib/switchman/arel.rb +16 -25
- data/lib/switchman/call_super.rb +2 -8
- data/lib/switchman/database_server.rb +67 -24
- data/lib/switchman/default_shard.rb +14 -3
- data/lib/switchman/engine.rb +35 -23
- data/lib/switchman/environment.rb +2 -2
- data/lib/switchman/errors.rb +4 -1
- data/lib/switchman/guard_rail/relation.rb +1 -2
- data/lib/switchman/parallel.rb +5 -5
- data/lib/switchman/r_spec_helper.rb +12 -11
- data/lib/switchman/shard.rb +168 -68
- data/lib/switchman/sharded_instrumenter.rb +9 -3
- data/lib/switchman/standard_error.rb +4 -0
- data/lib/switchman/test_helper.rb +3 -3
- data/lib/switchman/version.rb +1 -1
- data/lib/switchman.rb +27 -15
- data/lib/tasks/switchman.rake +96 -60
- metadata +28 -187
|
@@ -6,9 +6,9 @@ module Switchman
|
|
|
6
6
|
module ClassMethods
|
|
7
7
|
delegate :shard, to: :all
|
|
8
8
|
|
|
9
|
-
def find_ids_in_ranges(opts = {}, &
|
|
9
|
+
def find_ids_in_ranges(opts = {}, &)
|
|
10
10
|
opts.reverse_merge!(loose: true)
|
|
11
|
-
all.find_ids_in_ranges(opts, &
|
|
11
|
+
all.find_ids_in_ranges(opts, &)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def sharded_model
|
|
@@ -22,20 +22,16 @@ module Switchman
|
|
|
22
22
|
@integral_id
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
25
|
+
def transaction(**)
|
|
26
|
+
if self != ::ActiveRecord::Base && current_scope
|
|
27
|
+
current_scope.activate do
|
|
28
|
+
db = Shard.current(connection_class_for_self).database_server
|
|
29
|
+
db.unguard { super }
|
|
37
30
|
end
|
|
38
|
-
|
|
31
|
+
else
|
|
32
|
+
db = Shard.current(connection_class_for_self).database_server
|
|
33
|
+
db.unguard { super }
|
|
34
|
+
end
|
|
39
35
|
end
|
|
40
36
|
|
|
41
37
|
def reset_column_information
|
|
@@ -57,8 +53,13 @@ module Switchman
|
|
|
57
53
|
end
|
|
58
54
|
|
|
59
55
|
def clear_query_caches_for_current_thread
|
|
60
|
-
::ActiveRecord::Base.connection_handler.connection_pool_list
|
|
61
|
-
|
|
56
|
+
pools = ::ActiveRecord::Base.connection_handler.connection_pool_list(:all)
|
|
57
|
+
pools.each do |pool|
|
|
58
|
+
if ::Rails.version < "7.2"
|
|
59
|
+
pool.connection(switch_shard: false).clear_query_cache if pool.active_connection?
|
|
60
|
+
elsif pool.active_connection?
|
|
61
|
+
pool.lease_connection(switch_shard: false).clear_query_cache
|
|
62
|
+
end
|
|
62
63
|
end
|
|
63
64
|
end
|
|
64
65
|
|
|
@@ -67,7 +68,10 @@ module Switchman
|
|
|
67
68
|
end
|
|
68
69
|
|
|
69
70
|
def establish_connection(config_or_env = nil)
|
|
70
|
-
|
|
71
|
+
if config_or_env.is_a?(Symbol) && config_or_env != ::Rails.env.to_sym
|
|
72
|
+
raise ArgumentError,
|
|
73
|
+
"establish connection cannot be used on the non-current shard/role"
|
|
74
|
+
end
|
|
71
75
|
|
|
72
76
|
# Ensure we don't randomly surprise change the connection parms associated with a shard/role
|
|
73
77
|
config_or_env = nil if config_or_env == ::Rails.env.to_sym
|
|
@@ -75,16 +79,18 @@ module Switchman
|
|
|
75
79
|
config_or_env ||= if current_shard == ::Rails.env.to_sym && current_role == :primary
|
|
76
80
|
:primary
|
|
77
81
|
else
|
|
78
|
-
"#{current_shard}/#{current_role}"
|
|
82
|
+
:"#{current_shard}/#{current_role}"
|
|
79
83
|
end
|
|
80
84
|
|
|
81
|
-
super
|
|
85
|
+
super
|
|
82
86
|
end
|
|
83
87
|
|
|
84
88
|
def connected_to_stack
|
|
85
|
-
|
|
89
|
+
has_own_stack = ::ActiveSupport::IsolatedExecutionState.key?(:active_record_connected_to_stack)
|
|
86
90
|
|
|
87
91
|
ret = super
|
|
92
|
+
return ret if has_own_stack
|
|
93
|
+
|
|
88
94
|
DatabaseServer.guard_servers
|
|
89
95
|
ret
|
|
90
96
|
end
|
|
@@ -96,10 +102,13 @@ module Switchman
|
|
|
96
102
|
sharded_role = nil
|
|
97
103
|
connected_to_stack.reverse_each do |hash|
|
|
98
104
|
shard_role = hash.dig(:shard_roles, target_shard)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
105
|
+
unless shard_role &&
|
|
106
|
+
(hash[:klasses].include?(::ActiveRecord::Base) || hash[:klasses].include?(connection_class_for_self))
|
|
107
|
+
next
|
|
102
108
|
end
|
|
109
|
+
|
|
110
|
+
sharded_role = shard_role
|
|
111
|
+
break
|
|
103
112
|
end
|
|
104
113
|
# Allow a shard-specific role to be reverted to regular inheritance
|
|
105
114
|
return sharded_role if sharded_role && sharded_role != :_switchman_inherit
|
|
@@ -119,21 +128,25 @@ module Switchman
|
|
|
119
128
|
|
|
120
129
|
def current_switchman_shard
|
|
121
130
|
connected_to_stack.reverse_each do |hash|
|
|
122
|
-
|
|
131
|
+
if hash[:switchman_shard] && hash[:klasses].include?(connection_class_for_self)
|
|
132
|
+
return hash[:switchman_shard]
|
|
133
|
+
end
|
|
123
134
|
end
|
|
124
135
|
|
|
125
136
|
Shard.default
|
|
126
137
|
end
|
|
127
|
-
|
|
128
|
-
if ::Rails.version < '7.0'
|
|
129
|
-
def connection_class_for_self
|
|
130
|
-
connection_classes
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
138
|
end
|
|
134
139
|
|
|
135
140
|
def self.prepended(klass)
|
|
136
141
|
klass.singleton_class.prepend(ClassMethods)
|
|
142
|
+
klass.singleton_class.prepend(Switchman::ActiveRecord::Relation::InsertUpsertAll) if ::Rails.version < "7.2"
|
|
143
|
+
klass.scope :non_shadow, lambda { |key = primary_key|
|
|
144
|
+
where(key => (QueryMethods::NonTransposingValue.new(0)...
|
|
145
|
+
QueryMethods::NonTransposingValue.new(Shard::IDS_PER_SHARD)))
|
|
146
|
+
}
|
|
147
|
+
klass.scope :shadow, lambda { |key = primary_key|
|
|
148
|
+
where(key => QueryMethods::NonTransposingValue.new(Shard::IDS_PER_SHARD)..)
|
|
149
|
+
}
|
|
137
150
|
end
|
|
138
151
|
|
|
139
152
|
def _run_initialize_callbacks
|
|
@@ -144,7 +157,15 @@ module Switchman
|
|
|
144
157
|
end
|
|
145
158
|
|
|
146
159
|
@loaded_from_shard ||= Shard.current(self.class.connection_class_for_self)
|
|
147
|
-
|
|
160
|
+
if shadow_record? && !Switchman.config[:writable_shadow_records]
|
|
161
|
+
@readonly = true
|
|
162
|
+
@readonly_from_shadow ||= true
|
|
163
|
+
end
|
|
164
|
+
super
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def readonly!
|
|
168
|
+
@readonly_from_shadow = false
|
|
148
169
|
super
|
|
149
170
|
end
|
|
150
171
|
|
|
@@ -155,6 +176,10 @@ module Switchman
|
|
|
155
176
|
pkey > Shard::IDS_PER_SHARD
|
|
156
177
|
end
|
|
157
178
|
|
|
179
|
+
def canonical?
|
|
180
|
+
!shadow_record?
|
|
181
|
+
end
|
|
182
|
+
|
|
158
183
|
def save_shadow_record(new_attrs: attributes, target_shard: Shard.current)
|
|
159
184
|
return if target_shard == shard
|
|
160
185
|
|
|
@@ -167,18 +192,22 @@ module Switchman
|
|
|
167
192
|
end
|
|
168
193
|
end
|
|
169
194
|
target_shard.activate do
|
|
170
|
-
self.class.
|
|
195
|
+
self.class.upsert_all([shadow_attrs], unique_by: self.class.primary_key)
|
|
171
196
|
end
|
|
172
197
|
end
|
|
173
198
|
|
|
174
199
|
def destroy_shadow_records(target_shards: [Shard.current])
|
|
175
|
-
raise Errors::ShadowRecordError,
|
|
176
|
-
|
|
200
|
+
raise Errors::ShadowRecordError, "Cannot be called on a shadow record." if shadow_record?
|
|
201
|
+
|
|
202
|
+
unless self.class.sharded_column?(self.class.primary_key)
|
|
203
|
+
raise Errors::MethodUnsupportedForUnshardedTableError,
|
|
204
|
+
"Cannot be called on a record belonging to an unsharded table."
|
|
205
|
+
end
|
|
177
206
|
|
|
178
207
|
Array(target_shards).each do |target_shard|
|
|
179
208
|
next if target_shard == shard
|
|
180
209
|
|
|
181
|
-
target_shard.activate { self.class.where(
|
|
210
|
+
target_shard.activate { self.class.where("id = ?", global_id).delete_all }
|
|
182
211
|
end
|
|
183
212
|
end
|
|
184
213
|
|
|
@@ -186,7 +215,7 @@ module Switchman
|
|
|
186
215
|
# opposed to "the shard this record belongs on", which might be
|
|
187
216
|
# different if this is a shadow record.
|
|
188
217
|
def loaded_from_shard
|
|
189
|
-
@loaded_from_shard ||
|
|
218
|
+
@loaded_from_shard || shard
|
|
190
219
|
end
|
|
191
220
|
|
|
192
221
|
def shard
|
|
@@ -235,9 +264,9 @@ module Switchman
|
|
|
235
264
|
result
|
|
236
265
|
end
|
|
237
266
|
|
|
238
|
-
def transaction(
|
|
267
|
+
def transaction(...)
|
|
239
268
|
shard.activate(self.class.connection_class_for_self) do
|
|
240
|
-
self.class.transaction(
|
|
269
|
+
self.class.transaction(...)
|
|
241
270
|
end
|
|
242
271
|
end
|
|
243
272
|
|
|
@@ -270,8 +299,10 @@ module Switchman
|
|
|
270
299
|
|
|
271
300
|
def id_for_database
|
|
272
301
|
if self.class.sharded_primary_key?
|
|
273
|
-
# It's an int, so
|
|
274
|
-
# In theory we should do
|
|
302
|
+
# It's an int, so it's safe to just return it without passing it
|
|
303
|
+
# through anything else. In theory we should do
|
|
304
|
+
# `@attributes[@primary_key].type.serialize(id)`, but that seems to
|
|
305
|
+
# have surprising side-effects
|
|
275
306
|
id
|
|
276
307
|
else
|
|
277
308
|
super
|
|
@@ -28,19 +28,19 @@ 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
|
-
result = activate do |relation|
|
|
34
|
+
result = activate(count: operation == "count") do |relation|
|
|
35
35
|
relation.call_super(:execute_simple_calculation, Calculations, operation, column_name, distinct)
|
|
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,12 +90,12 @@ 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)
|
|
97
97
|
row[aliaz] = key_records[Shard.relative_id_for(row[aliaz], shard, target_shard)]
|
|
98
|
-
elsif group_column_name &&
|
|
98
|
+
elsif group_column_name && klass.sharded_column?(group_column_name)
|
|
99
99
|
row[aliaz] = Shard.relative_id_for(row[aliaz], shard, target_shard)
|
|
100
100
|
end
|
|
101
101
|
end
|
|
@@ -106,60 +106,95 @@ module Switchman
|
|
|
106
106
|
compact_grouped_calculation_rows(rows, opts)
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
def ids
|
|
110
|
+
return super unless klass.sharded_primary_key?
|
|
110
111
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
112
|
+
if loaded?
|
|
113
|
+
result = records.map do |record|
|
|
114
|
+
Shard.relative_id_for(record._read_attribute(primary_key),
|
|
115
|
+
record.shard,
|
|
116
|
+
Shard.current(klass.connection_class_for_self))
|
|
117
|
+
end
|
|
118
|
+
return @async ? Promise::Complete.new(result) : result
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
if has_include?(primary_key)
|
|
122
|
+
relation = apply_join_dependency.group(primary_key)
|
|
123
|
+
return relation.ids
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
columns = arel_columns([primary_key])
|
|
127
|
+
base_shard = Shard.current(klass.connection_class_for_self)
|
|
128
|
+
activate do |r|
|
|
129
|
+
relation = r.spawn
|
|
130
|
+
relation.select_values = columns
|
|
131
|
+
|
|
132
|
+
result = if relation.where_clause.contradiction?
|
|
133
|
+
::ActiveRecord::Result.empty
|
|
134
|
+
else
|
|
135
|
+
skip_query_cache_if_necessary do
|
|
136
|
+
klass.connection.select_all(relation, "#{klass.name} Ids", async: @async)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
result.then do |res|
|
|
141
|
+
type_cast_pluck_values(res, columns).map { |id| Shard.relative_id_for(id, Shard.current, base_shard) }
|
|
118
142
|
end
|
|
119
|
-
else
|
|
120
|
-
column = aggregate_column(column_name)
|
|
121
|
-
type ||= column.try(:type_caster) ||
|
|
122
|
-
lookup_cast_type_from_join_dependencies(column_name.to_s) || ::ActiveRecord::Type.default_value
|
|
123
|
-
type_cast_calculated_value(value, operation, type)
|
|
124
143
|
end
|
|
125
144
|
end
|
|
126
145
|
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
def type_cast_calculated_value_switchman(value, column_name, operation)
|
|
149
|
+
column = aggregate_column(column_name)
|
|
150
|
+
type ||= column.try(:type_caster) ||
|
|
151
|
+
lookup_cast_type_from_join_dependencies(column_name.to_s) || ::ActiveRecord::Type.default_value
|
|
152
|
+
type_cast_calculated_value(value, operation, type)
|
|
153
|
+
end
|
|
154
|
+
|
|
127
155
|
def column_name_for(field)
|
|
128
|
-
field.respond_to?(:name) ? field.name.to_s : field.to_s.split(
|
|
156
|
+
field.respond_to?(:name) ? field.name.to_s : field.to_s.split(".").last
|
|
129
157
|
end
|
|
130
158
|
|
|
131
159
|
def grouped_calculation_options(operation, column_name, distinct)
|
|
132
|
-
opts = { operation
|
|
160
|
+
opts = { operation:, column_name:, distinct: }
|
|
161
|
+
|
|
162
|
+
column_alias_tracker = ::ActiveRecord::Calculations::ColumnAliasTracker.new(connection)
|
|
133
163
|
|
|
134
|
-
opts[:aggregate_alias] = aggregate_alias_for(operation, column_name)
|
|
164
|
+
opts[:aggregate_alias] = aggregate_alias_for(operation, column_name, column_alias_tracker)
|
|
135
165
|
group_attrs = group_values
|
|
136
166
|
if group_attrs.first.respond_to?(:to_sym)
|
|
137
167
|
association = klass.reflect_on_association(group_attrs.first.to_sym)
|
|
138
|
-
|
|
168
|
+
# only count belongs_to associations
|
|
169
|
+
associated = group_attrs.size == 1 && association && association.macro == :belongs_to
|
|
139
170
|
group_fields = Array(associated ? association.foreign_key : group_attrs)
|
|
140
171
|
else
|
|
141
172
|
group_fields = group_attrs
|
|
142
173
|
end
|
|
143
174
|
|
|
144
|
-
|
|
145
|
-
|
|
175
|
+
group_aliases = group_fields.map do |field|
|
|
176
|
+
field = connection.visitor.compile(field) if ::Arel.arel_node?(field)
|
|
177
|
+
column_alias_tracker.alias_for(field.to_s.downcase)
|
|
178
|
+
end
|
|
146
179
|
group_columns = group_aliases.zip(group_fields).map do |aliaz, field|
|
|
147
180
|
[aliaz, type_for(field), column_name_for(field)]
|
|
148
181
|
end
|
|
149
|
-
opts.merge!(association
|
|
150
|
-
|
|
151
|
-
|
|
182
|
+
opts.merge!(association:,
|
|
183
|
+
associated:,
|
|
184
|
+
group_aliases:,
|
|
185
|
+
group_columns:,
|
|
186
|
+
group_fields:)
|
|
152
187
|
|
|
153
188
|
opts
|
|
154
189
|
end
|
|
155
190
|
|
|
156
|
-
def aggregate_alias_for(operation, column_name)
|
|
157
|
-
if operation ==
|
|
158
|
-
|
|
159
|
-
elsif operation ==
|
|
160
|
-
|
|
191
|
+
def aggregate_alias_for(operation, column_name, column_alias_tracker)
|
|
192
|
+
if operation == "count" && column_name == :all
|
|
193
|
+
"count_all"
|
|
194
|
+
elsif operation == "average"
|
|
195
|
+
"average"
|
|
161
196
|
else
|
|
162
|
-
|
|
197
|
+
column_alias_tracker.alias_for("#{operation} #{column_name}")
|
|
163
198
|
end
|
|
164
199
|
end
|
|
165
200
|
|
|
@@ -173,13 +208,14 @@ module Switchman
|
|
|
173
208
|
opts[:distinct]
|
|
174
209
|
).as(opts[:aggregate_alias])
|
|
175
210
|
]
|
|
176
|
-
if opts[:operation] ==
|
|
211
|
+
if opts[:operation] == "average"
|
|
177
212
|
# include count in average so we can recalculate the average
|
|
178
213
|
# across all shards if needed
|
|
179
214
|
select_values << operation_over_aggregate_column(
|
|
180
215
|
aggregate_column(opts[:column_name]),
|
|
181
|
-
|
|
182
|
-
|
|
216
|
+
"count",
|
|
217
|
+
opts[:distinct]
|
|
218
|
+
).as("count")
|
|
183
219
|
end
|
|
184
220
|
|
|
185
221
|
haves = having_clause.send(:predicates)
|
|
@@ -205,22 +241,22 @@ module Switchman
|
|
|
205
241
|
key = key.first if key.size == 1
|
|
206
242
|
value = row[opts[:aggregate_alias]]
|
|
207
243
|
|
|
208
|
-
if opts[:operation] ==
|
|
244
|
+
if opts[:operation] == "average"
|
|
209
245
|
if result.key?(key)
|
|
210
246
|
old_value, old_count = result[key]
|
|
211
|
-
new_count = old_count + row[
|
|
212
|
-
new_value = ((old_value * old_count) + (value * row[
|
|
247
|
+
new_count = old_count + row["count"]
|
|
248
|
+
new_value = ((old_value * old_count) + (value * row["count"])) / new_count
|
|
213
249
|
result[key] = [new_value, new_count]
|
|
214
250
|
else
|
|
215
|
-
result[key] = [value, row[
|
|
251
|
+
result[key] = [value, row["count"]]
|
|
216
252
|
end
|
|
217
253
|
elsif result.key?(key)
|
|
218
254
|
case opts[:operation]
|
|
219
|
-
when
|
|
255
|
+
when "count", "sum"
|
|
220
256
|
result[key] += value
|
|
221
|
-
when
|
|
257
|
+
when "minimum"
|
|
222
258
|
result[key] = value if value < result[key]
|
|
223
|
-
when
|
|
259
|
+
when "maximum"
|
|
224
260
|
result[key] = value if value > result[key]
|
|
225
261
|
end
|
|
226
262
|
else
|
|
@@ -228,7 +264,7 @@ module Switchman
|
|
|
228
264
|
end
|
|
229
265
|
end
|
|
230
266
|
|
|
231
|
-
result.transform_values!(&:first) if opts[:operation] ==
|
|
267
|
+
result.transform_values!(&:first) if opts[:operation] == "average"
|
|
232
268
|
|
|
233
269
|
result
|
|
234
270
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Switchman
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
module ConnectionHandler
|
|
6
|
+
def resolve_pool_config(config, connection_name, role, shard)
|
|
7
|
+
ret = super
|
|
8
|
+
# Make *all* pool configs use the same schema reflection
|
|
9
|
+
ret.schema_reflection = ConnectionHandler.global_schema_reflection
|
|
10
|
+
ret
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.global_schema_reflection
|
|
14
|
+
@global_schema_reflection ||= ::ActiveRecord::ConnectionAdapters::SchemaReflection.new(nil)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -3,27 +3,11 @@
|
|
|
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
|
-
|
|
23
6
|
def default_schema
|
|
24
|
-
connection
|
|
7
|
+
connection_method = (::Rails.version < "7.2") ? :connection : :lease_connection
|
|
8
|
+
send(connection_method) unless @schemas
|
|
25
9
|
# default shard will not switch databases immediately, so it won't be set yet
|
|
26
|
-
@schemas ||=
|
|
10
|
+
@schemas ||= send(connection_method).current_schemas
|
|
27
11
|
@schemas.first
|
|
28
12
|
end
|
|
29
13
|
|
|
@@ -41,14 +25,44 @@ module Switchman
|
|
|
41
25
|
conn
|
|
42
26
|
end
|
|
43
27
|
|
|
28
|
+
unless ::Rails.version < "7.2"
|
|
29
|
+
def active_connection(switch_shard: true)
|
|
30
|
+
conn = super()
|
|
31
|
+
return nil if conn.nil?
|
|
32
|
+
raise Errors::NonExistentShardError if current_shard.new_record?
|
|
33
|
+
|
|
34
|
+
switch_database(conn) if conn.shard != current_shard && switch_shard
|
|
35
|
+
conn
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def lease_connection(switch_shard: true)
|
|
39
|
+
conn = super()
|
|
40
|
+
raise Errors::NonExistentShardError if current_shard.new_record?
|
|
41
|
+
|
|
42
|
+
switch_database(conn) if conn.shard != current_shard && switch_shard
|
|
43
|
+
conn
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def with_connection(switch_shard: true, **kwargs)
|
|
47
|
+
super(**kwargs) do |conn|
|
|
48
|
+
raise Errors::NonExistentShardError if current_shard.new_record?
|
|
49
|
+
|
|
50
|
+
switch_database(conn) if conn.shard != current_shard && switch_shard
|
|
51
|
+
yield conn
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
44
56
|
def release_connection(with_id = Thread.current)
|
|
45
|
-
super
|
|
57
|
+
super
|
|
46
58
|
|
|
47
59
|
flush
|
|
48
60
|
end
|
|
49
61
|
|
|
50
62
|
def switch_database(conn)
|
|
51
|
-
|
|
63
|
+
if !@schemas && conn.adapter_name == "PostgreSQL" && !current_shard.database_server.config[:shard_name]
|
|
64
|
+
@schemas = conn.current_schemas
|
|
65
|
+
end
|
|
52
66
|
|
|
53
67
|
conn.shard = current_shard
|
|
54
68
|
end
|
|
@@ -56,11 +70,15 @@ module Switchman
|
|
|
56
70
|
private
|
|
57
71
|
|
|
58
72
|
def current_shard
|
|
59
|
-
::Rails.version <
|
|
73
|
+
if ::Rails.version < "8.0"
|
|
74
|
+
connection_class.current_switchman_shard
|
|
75
|
+
else
|
|
76
|
+
connection_descriptor.name.constantize.current_switchman_shard
|
|
77
|
+
end
|
|
60
78
|
end
|
|
61
79
|
|
|
62
80
|
def tls_key
|
|
63
|
-
"#{object_id}_shard"
|
|
81
|
+
:"#{object_id}_shard"
|
|
64
82
|
end
|
|
65
83
|
end
|
|
66
84
|
end
|
|
@@ -7,12 +7,12 @@ module Switchman
|
|
|
7
7
|
# since all should point to the same data, even if multiple are writable
|
|
8
8
|
# (Picks 'primary' since it is guaranteed to exist and switchman handles activating
|
|
9
9
|
# deploy through other means)
|
|
10
|
-
def configs_for(
|
|
10
|
+
def configs_for(include_hidden: false, name: nil, **)
|
|
11
11
|
res = super
|
|
12
|
-
if name && !
|
|
13
|
-
return nil unless name.end_with?(
|
|
14
|
-
elsif !
|
|
15
|
-
return res.select { |config| config.name.end_with?(
|
|
12
|
+
if name && !include_hidden
|
|
13
|
+
return nil unless name.end_with?("primary")
|
|
14
|
+
elsif !include_hidden
|
|
15
|
+
return res.select { |config| config.name.end_with?("primary") }
|
|
16
16
|
end
|
|
17
17
|
res
|
|
18
18
|
end
|
|
@@ -27,21 +27,31 @@ module Switchman
|
|
|
27
27
|
return configs if configs.is_a?(Array)
|
|
28
28
|
|
|
29
29
|
db_configs = configs.flat_map do |env_name, config|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
if config.is_a?(Hash)
|
|
31
|
+
# It would be nice to do the auto-fallback that we want here, but we haven't
|
|
32
|
+
# actually done that for years (or maybe ever) and it will be a big lift to get working
|
|
33
|
+
roles = config.keys.select do |k|
|
|
34
|
+
config[k].is_a?(Hash) || (config[k].is_a?(Array) && config[k].all?(Hash))
|
|
35
|
+
end
|
|
36
|
+
base_config = config.except(*roles)
|
|
37
|
+
else
|
|
38
|
+
base_config = config
|
|
39
|
+
roles = []
|
|
40
|
+
end
|
|
34
41
|
|
|
35
42
|
name = "#{env_name}/primary"
|
|
36
|
-
name =
|
|
43
|
+
name = "primary" if env_name == default_env
|
|
37
44
|
base_db = build_db_config_from_raw_config(env_name, name, base_config)
|
|
38
45
|
[base_db] + roles.map do |role|
|
|
39
|
-
build_db_config_from_raw_config(
|
|
40
|
-
|
|
46
|
+
build_db_config_from_raw_config(
|
|
47
|
+
env_name,
|
|
48
|
+
"#{env_name}/#{role}",
|
|
49
|
+
base_config.merge(config[role].is_a?(Array) ? config[role].first : config[role])
|
|
50
|
+
)
|
|
41
51
|
end
|
|
42
52
|
end
|
|
43
53
|
|
|
44
|
-
db_configs << environment_url_config(default_env,
|
|
54
|
+
db_configs << environment_url_config(default_env, "primary", {}) unless db_configs.find(&:for_current_env?)
|
|
45
55
|
|
|
46
56
|
merge_db_environment_variables(default_env, db_configs.compact)
|
|
47
57
|
end
|