switchman 2.1.0 → 3.0.6
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 +10 -2
- data/app/models/switchman/shard.rb +270 -343
- data/app/models/switchman/unsharded_record.rb +7 -0
- data/db/migrate/20130328212039_create_switchman_shards.rb +1 -1
- data/db/migrate/20130328224244_create_default_shard.rb +5 -5
- data/db/migrate/20161206323434_add_back_default_string_limits_switchman.rb +1 -0
- data/db/migrate/20180828183945_add_default_shard_index.rb +2 -2
- data/db/migrate/20180828192111_add_timestamps_to_shards.rb +8 -6
- data/db/migrate/20190114212900_add_unique_name_indexes.rb +5 -3
- data/lib/switchman/action_controller/caching.rb +2 -2
- data/lib/switchman/active_record/abstract_adapter.rb +0 -8
- data/lib/switchman/active_record/association.rb +78 -89
- data/lib/switchman/active_record/attribute_methods.rb +127 -52
- data/lib/switchman/active_record/base.rb +83 -67
- data/lib/switchman/active_record/calculations.rb +73 -66
- data/lib/switchman/active_record/connection_pool.rb +12 -59
- data/lib/switchman/active_record/database_configurations/database_config.rb +13 -0
- data/lib/switchman/active_record/database_configurations.rb +34 -0
- data/lib/switchman/active_record/finder_methods.rb +11 -16
- data/lib/switchman/active_record/log_subscriber.rb +4 -8
- data/lib/switchman/active_record/migration.rb +19 -45
- data/lib/switchman/active_record/model_schema.rb +1 -1
- data/lib/switchman/active_record/persistence.rb +11 -6
- data/lib/switchman/active_record/postgresql_adapter.rb +33 -161
- data/lib/switchman/active_record/predicate_builder.rb +1 -1
- data/lib/switchman/active_record/query_cache.rb +18 -19
- data/lib/switchman/active_record/query_methods.rb +178 -193
- data/lib/switchman/active_record/reflection.rb +7 -22
- data/lib/switchman/active_record/relation.rb +32 -29
- data/lib/switchman/active_record/spawn_methods.rb +27 -29
- data/lib/switchman/active_record/statement_cache.rb +18 -35
- data/lib/switchman/active_record/tasks/database_tasks.rb +16 -0
- data/lib/switchman/active_record/test_fixtures.rb +43 -0
- data/lib/switchman/active_support/cache.rb +3 -5
- data/lib/switchman/arel.rb +13 -8
- data/lib/switchman/database_server.rb +130 -154
- data/lib/switchman/default_shard.rb +52 -16
- data/lib/switchman/engine.rb +65 -58
- data/lib/switchman/environment.rb +4 -8
- data/lib/switchman/errors.rb +1 -0
- data/lib/switchman/guard_rail/relation.rb +5 -7
- data/lib/switchman/guard_rail.rb +6 -19
- data/lib/switchman/r_spec_helper.rb +29 -57
- data/lib/switchman/rails.rb +14 -12
- data/lib/switchman/sharded_instrumenter.rb +1 -1
- data/lib/switchman/standard_error.rb +15 -3
- data/lib/switchman/test_helper.rb +5 -3
- data/lib/switchman/version.rb +1 -1
- data/lib/switchman.rb +3 -3
- data/lib/tasks/switchman.rake +61 -72
- metadata +90 -48
- data/lib/switchman/active_record/batches.rb +0 -11
- data/lib/switchman/active_record/connection_handler.rb +0 -190
- data/lib/switchman/active_record/where_clause_factory.rb +0 -36
- data/lib/switchman/connection_pool_proxy.rb +0 -173
- data/lib/switchman/schema_cache.rb +0 -28
@@ -6,49 +6,40 @@ module Switchman
|
|
6
6
|
module ClassMethods
|
7
7
|
delegate :shard, to: :all
|
8
8
|
|
9
|
-
def find_ids_in_ranges(opts={}, &block)
|
10
|
-
opts.reverse_merge!(:
|
9
|
+
def find_ids_in_ranges(opts = {}, &block)
|
10
|
+
opts.reverse_merge!(loose: true)
|
11
11
|
all.find_ids_in_ranges(opts, &block)
|
12
12
|
end
|
13
13
|
|
14
|
-
def
|
15
|
-
|
16
|
-
end
|
14
|
+
def sharded_model
|
15
|
+
self.abstract_class = true
|
17
16
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
categories[shard_category].delete(self)
|
22
|
-
categories.delete(shard_category) if categories[shard_category].empty?
|
23
|
-
end
|
24
|
-
categories[category] ||= []
|
25
|
-
categories[category] << self
|
26
|
-
self.connection_specification_name = category.to_s
|
17
|
+
return if self == UnshardedRecord
|
18
|
+
|
19
|
+
Shard.send(:add_sharded_model, self)
|
27
20
|
end
|
28
21
|
|
29
22
|
def integral_id?
|
30
|
-
|
31
|
-
@integral_id = columns_hash[primary_key]&.type == :integer
|
32
|
-
end
|
23
|
+
@integral_id = columns_hash[primary_key]&.type == :integer if @integral_id.nil?
|
33
24
|
@integral_id
|
34
25
|
end
|
35
26
|
|
36
27
|
def transaction(**)
|
37
28
|
if self != ::ActiveRecord::Base && current_scope
|
38
29
|
current_scope.activate do
|
39
|
-
db = Shard.current(
|
40
|
-
if ::GuardRail.environment
|
41
|
-
db.unguard { super }
|
42
|
-
else
|
30
|
+
db = Shard.current(connection_classes).database_server
|
31
|
+
if ::GuardRail.environment == db.guard_rail_environment
|
43
32
|
super
|
33
|
+
else
|
34
|
+
db.unguard { super }
|
44
35
|
end
|
45
36
|
end
|
46
37
|
else
|
47
|
-
db = Shard.current(
|
48
|
-
if ::GuardRail.environment
|
49
|
-
db.unguard { super }
|
50
|
-
else
|
38
|
+
db = Shard.current(connection_classes).database_server
|
39
|
+
if ::GuardRail.environment == db.guard_rail_environment
|
51
40
|
super
|
41
|
+
else
|
42
|
+
db.unguard { super }
|
52
43
|
end
|
53
44
|
end
|
54
45
|
end
|
@@ -72,53 +63,70 @@ module Switchman
|
|
72
63
|
end
|
73
64
|
|
74
65
|
def clear_query_caches_for_current_thread
|
75
|
-
::ActiveRecord::Base.
|
76
|
-
|
77
|
-
pool.connection(switch_shard: false).clear_query_cache if pool.active_connection?
|
78
|
-
end
|
66
|
+
::ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool|
|
67
|
+
pool.connection(switch_shard: false).clear_query_cache if pool.active_connection?
|
79
68
|
end
|
80
69
|
end
|
81
|
-
end
|
82
70
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
if
|
88
|
-
@shard = Shard.shard_for(self[self.class.primary_key], Shard.current(self.class.shard_category))
|
89
|
-
else
|
90
|
-
@shard = Shard.current(self.class.shard_category)
|
91
|
-
end
|
71
|
+
# significant change: _don't_ check if klasses.include?(Base)
|
72
|
+
# i.e. other sharded models don't inherit the current shard of Base
|
73
|
+
def current_shard
|
74
|
+
connected_to_stack.reverse_each do |hash|
|
75
|
+
return hash[:shard] if hash[:shard] && hash[:klasses].include?(connection_classes)
|
92
76
|
end
|
77
|
+
|
78
|
+
default_shard
|
93
79
|
end
|
80
|
+
|
81
|
+
def current_switchman_shard
|
82
|
+
connected_to_stack.reverse_each do |hash|
|
83
|
+
return hash[:switchman_shard] if hash[:switchman_shard] && hash[:klasses].include?(connection_classes)
|
84
|
+
end
|
85
|
+
|
86
|
+
Shard.default
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.prepended(klass)
|
91
|
+
klass.singleton_class.prepend(ClassMethods)
|
92
|
+
end
|
93
|
+
|
94
|
+
def _run_initialize_callbacks
|
95
|
+
@shard ||= if self.class.sharded_primary_key?
|
96
|
+
Shard.shard_for(self[self.class.primary_key], Shard.current(self.class.connection_classes))
|
97
|
+
else
|
98
|
+
Shard.current(self.class.connection_classes)
|
99
|
+
end
|
100
|
+
super
|
94
101
|
end
|
95
102
|
|
96
103
|
def shard
|
97
|
-
@shard || Shard.current(self.class.
|
104
|
+
@shard || Shard.current(self.class.connection_classes) || Shard.default
|
98
105
|
end
|
99
106
|
|
100
107
|
def shard=(new_shard)
|
101
|
-
raise ::ActiveRecord::ReadOnlyRecord if !
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
108
|
+
raise ::ActiveRecord::ReadOnlyRecord if !new_record? || @shard_set_in_stone
|
109
|
+
|
110
|
+
return if shard == new_shard
|
111
|
+
|
112
|
+
attributes.each do |attr, value|
|
113
|
+
self[attr] = Shard.relative_id_for(value, shard, new_shard) if self.class.sharded_column?(attr)
|
107
114
|
end
|
115
|
+
@shard = new_shard
|
108
116
|
end
|
109
117
|
|
110
118
|
def save(*, **)
|
111
119
|
@shard_set_in_stone = true
|
112
|
-
|
120
|
+
super
|
113
121
|
end
|
114
122
|
|
115
123
|
def save!(*, **)
|
116
124
|
@shard_set_in_stone = true
|
117
|
-
|
125
|
+
super
|
118
126
|
end
|
119
127
|
|
120
128
|
def destroy
|
121
|
-
self.class.
|
129
|
+
shard.activate(self.class.connection_classes) { super }
|
122
130
|
end
|
123
131
|
|
124
132
|
def clone
|
@@ -126,23 +134,29 @@ module Switchman
|
|
126
134
|
# TODO: adjust foreign keys
|
127
135
|
# don't use the setter, cause the foreign keys are already
|
128
136
|
# relative to this shard
|
129
|
-
result.instance_variable_set(:@shard,
|
137
|
+
result.instance_variable_set(:@shard, shard)
|
130
138
|
result
|
131
139
|
end
|
132
140
|
|
133
141
|
def transaction(**kwargs, &block)
|
134
|
-
shard.activate(self.class.
|
142
|
+
shard.activate(self.class.connection_classes) do
|
135
143
|
self.class.transaction(**kwargs, &block)
|
136
144
|
end
|
137
145
|
end
|
138
146
|
|
147
|
+
def with_transaction_returning_status
|
148
|
+
shard.activate(self.class.connection_classes) do
|
149
|
+
super
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
139
153
|
def hash
|
140
|
-
self.class.sharded_primary_key? ? self.class.hash ^
|
154
|
+
self.class.sharded_primary_key? ? self.class.hash ^ global_id.hash : super
|
141
155
|
end
|
142
156
|
|
143
157
|
def to_param
|
144
158
|
short_id = Shard.short_id_for(id)
|
145
|
-
short_id
|
159
|
+
short_id&.to_s
|
146
160
|
end
|
147
161
|
|
148
162
|
def initialize_dup(*args)
|
@@ -151,16 +165,18 @@ module Switchman
|
|
151
165
|
copy
|
152
166
|
end
|
153
167
|
|
154
|
-
def
|
155
|
-
|
156
|
-
|
157
|
-
|
168
|
+
def update_columns(*)
|
169
|
+
db = shard.database_server
|
170
|
+
return db.unguard { super } if ::GuardRail.environment != db.guard_rail_environment
|
171
|
+
|
172
|
+
super
|
158
173
|
end
|
159
174
|
|
160
|
-
def
|
161
|
-
|
162
|
-
|
163
|
-
|
175
|
+
def id_for_database
|
176
|
+
if self.class.sharded_primary_key?
|
177
|
+
# It's an int, so so it's safe to just return it without passing it through anything else
|
178
|
+
# In theory we should do `@attributes[@primary_key].type.serialize(id)`, but that seems to have surprising side-effects
|
179
|
+
id
|
164
180
|
else
|
165
181
|
super
|
166
182
|
end
|
@@ -168,22 +184,22 @@ module Switchman
|
|
168
184
|
|
169
185
|
protected
|
170
186
|
|
171
|
-
# see also AttributeMethods#
|
172
|
-
def
|
187
|
+
# see also AttributeMethods#connection_classes_code_for_reflection
|
188
|
+
def connection_classes_for_reflection(reflection)
|
173
189
|
if reflection
|
174
190
|
if reflection.options[:polymorphic]
|
175
191
|
begin
|
176
|
-
read_attribute(reflection.foreign_type)&.constantize&.
|
192
|
+
read_attribute(reflection.foreign_type)&.constantize&.connection_classes
|
177
193
|
rescue NameError
|
178
194
|
# in case someone is abusing foreign_type to not point to an actual class
|
179
|
-
|
195
|
+
::ActiveRecord::Base
|
180
196
|
end
|
181
197
|
else
|
182
198
|
# otherwise we can just return a symbol for the statically known type of the association
|
183
|
-
reflection.klass.
|
199
|
+
reflection.klass.connection_classes
|
184
200
|
end
|
185
201
|
else
|
186
|
-
|
202
|
+
self.class.connection_classes
|
187
203
|
end
|
188
204
|
end
|
189
205
|
end
|
@@ -3,45 +3,44 @@
|
|
3
3
|
module Switchman
|
4
4
|
module ActiveRecord
|
5
5
|
module Calculations
|
6
|
-
|
7
6
|
def pluck(*column_names)
|
8
|
-
target_shard = Shard.current(klass.
|
7
|
+
target_shard = Shard.current(klass.connection_classes)
|
9
8
|
shard_count = 0
|
10
|
-
result =
|
9
|
+
result = activate do |relation, shard|
|
11
10
|
shard_count += 1
|
12
11
|
results = relation.call_super(:pluck, Calculations, *column_names)
|
13
12
|
if column_names.length > 1
|
14
13
|
column_names.each_with_index do |column_name, idx|
|
15
|
-
|
16
|
-
|
14
|
+
next unless klass.sharded_column?(column_name)
|
15
|
+
|
16
|
+
results.each do |r|
|
17
|
+
r[idx] = Shard.relative_id_for(r[idx], shard, target_shard)
|
17
18
|
end
|
18
19
|
end
|
19
|
-
|
20
|
-
|
21
|
-
results = results.map{|result| Shard.relative_id_for(result, shard, target_shard)}
|
22
|
-
end
|
20
|
+
elsif klass.sharded_column?(column_names.first.to_s)
|
21
|
+
results = results.map { |r| Shard.relative_id_for(r, shard, target_shard) }
|
23
22
|
end
|
24
23
|
results
|
25
24
|
end
|
26
|
-
if distinct_value && shard_count > 1
|
27
|
-
result.uniq!
|
28
|
-
end
|
25
|
+
result.uniq! if distinct_value && shard_count > 1
|
29
26
|
result
|
30
27
|
end
|
31
28
|
|
32
29
|
def execute_simple_calculation(operation, column_name, distinct)
|
33
30
|
operation = operation.to_s.downcase
|
34
|
-
if operation ==
|
31
|
+
if operation == 'average'
|
35
32
|
result = calculate_simple_average(column_name, distinct)
|
36
33
|
else
|
37
|
-
result =
|
34
|
+
result = activate do |relation|
|
35
|
+
relation.call_super(:execute_simple_calculation, Calculations, operation, column_name, distinct)
|
36
|
+
end
|
38
37
|
if result.is_a?(Array)
|
39
38
|
case operation
|
40
|
-
when
|
39
|
+
when 'count', 'sum'
|
41
40
|
result = result.sum
|
42
|
-
when
|
41
|
+
when 'minimum'
|
43
42
|
result = result.min
|
44
|
-
when
|
43
|
+
when 'maximum'
|
45
44
|
result = result.max
|
46
45
|
end
|
47
46
|
end
|
@@ -53,18 +52,20 @@ module Switchman
|
|
53
52
|
# See activerecord#execute_simple_calculation
|
54
53
|
relation = except(:order)
|
55
54
|
column = aggregate_column(column_name)
|
56
|
-
relation.select_values = [operation_over_aggregate_column(column,
|
57
|
-
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')]
|
58
57
|
|
59
|
-
initial_results = relation.activate{ |rel| klass.connection.select_all(rel) }
|
58
|
+
initial_results = relation.activate { |rel| klass.connection.select_all(rel) }
|
60
59
|
if initial_results.is_a?(Array)
|
61
60
|
initial_results.each do |r|
|
62
|
-
r[
|
63
|
-
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')
|
64
63
|
end
|
65
|
-
result = initial_results.map{|r| r[
|
64
|
+
result = initial_results.map { |r| r['average'] * r['count'] }.sum / initial_results.map do |r|
|
65
|
+
r['count']
|
66
|
+
end.sum
|
66
67
|
else
|
67
|
-
result =
|
68
|
+
result = type_cast_calculated_value_switchman(initial_results.first['average'], column_name, 'average')
|
68
69
|
end
|
69
70
|
result
|
70
71
|
end
|
@@ -74,27 +75,28 @@ module Switchman
|
|
74
75
|
opts = grouped_calculation_options(operation.to_s.downcase, column_name, distinct)
|
75
76
|
|
76
77
|
relation = build_grouped_calculation_relation(opts)
|
77
|
-
target_shard = Shard.current
|
78
|
+
target_shard = Shard.current
|
78
79
|
|
79
80
|
rows = relation.activate do |rel, shard|
|
80
81
|
calculated_data = klass.connection.select_all(rel)
|
81
82
|
|
82
83
|
if opts[:association]
|
83
84
|
key_ids = calculated_data.collect { |row| row[opts[:group_aliases].first] }
|
84
|
-
key_records = opts[:association].klass.base_class.where(:
|
85
|
-
key_records =
|
85
|
+
key_records = opts[:association].klass.base_class.where(id: key_ids)
|
86
|
+
key_records = key_records.map { |r| [Shard.relative_id_for(r, shard, target_shard), r] }.to_h
|
86
87
|
end
|
87
88
|
|
88
89
|
calculated_data.map do |row|
|
89
|
-
row[opts[:aggregate_alias]] =
|
90
|
-
|
90
|
+
row[opts[:aggregate_alias]] = type_cast_calculated_value_switchman(
|
91
|
+
row[opts[:aggregate_alias]], column_name, opts[:operation]
|
92
|
+
)
|
91
93
|
row['count'] = row['count'].to_i if opts[:operation] == 'average'
|
92
94
|
|
93
|
-
opts[:group_columns].each do |aliaz,
|
95
|
+
opts[:group_columns].each do |aliaz, _type, group_column_name|
|
94
96
|
if opts[:associated] && (aliaz == opts[:group_aliases].first)
|
95
97
|
row[aliaz] = key_records[Shard.relative_id_for(row[aliaz], shard, target_shard)]
|
96
|
-
elsif
|
97
|
-
row[aliaz] = Shard.relative_id_for(
|
98
|
+
elsif group_column_name && @klass.sharded_column?(group_column_name)
|
99
|
+
row[aliaz] = Shard.relative_id_for(row[aliaz], shard, target_shard)
|
98
100
|
end
|
99
101
|
end
|
100
102
|
row
|
@@ -106,12 +108,21 @@ module Switchman
|
|
106
108
|
|
107
109
|
private
|
108
110
|
|
111
|
+
def type_cast_calculated_value_switchman(value, column_name, operation)
|
112
|
+
type_cast_calculated_value(value, operation) do |val|
|
113
|
+
column = aggregate_column(column_name)
|
114
|
+
type ||= column.try(:type_caster) ||
|
115
|
+
lookup_cast_type_from_join_dependencies(column_name.to_s) || Type.default_value
|
116
|
+
type.deserialize(val)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
109
120
|
def column_name_for(field)
|
110
121
|
field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last
|
111
122
|
end
|
112
123
|
|
113
124
|
def grouped_calculation_options(operation, column_name, distinct)
|
114
|
-
opts = {:
|
125
|
+
opts = { operation: operation, column_name: column_name, distinct: distinct }
|
115
126
|
|
116
127
|
opts[:aggregate_alias] = aggregate_alias_for(operation, column_name)
|
117
128
|
group_attrs = group_values
|
@@ -125,12 +136,12 @@ module Switchman
|
|
125
136
|
|
126
137
|
# to_s is because Rails 5 returns a string but Rails 6 returns a symbol.
|
127
138
|
group_aliases = group_fields.map { |field| column_alias_for(field.downcase.to_s).to_s }
|
128
|
-
group_columns = group_aliases.zip(group_fields).map
|
139
|
+
group_columns = group_aliases.zip(group_fields).map do |aliaz, field|
|
129
140
|
[aliaz, type_for(field), column_name_for(field)]
|
130
|
-
|
131
|
-
opts.merge!(:
|
132
|
-
|
133
|
-
|
141
|
+
end
|
142
|
+
opts.merge!(association: association, associated: associated,
|
143
|
+
group_aliases: group_aliases, group_columns: group_columns,
|
144
|
+
group_fields: group_fields)
|
134
145
|
|
135
146
|
opts
|
136
147
|
end
|
@@ -149,28 +160,30 @@ module Switchman
|
|
149
160
|
group = opts[:group_fields]
|
150
161
|
|
151
162
|
select_values = [
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
163
|
+
operation_over_aggregate_column(
|
164
|
+
aggregate_column(opts[:column_name]),
|
165
|
+
opts[:operation],
|
166
|
+
opts[:distinct]
|
167
|
+
).as(opts[:aggregate_alias])
|
156
168
|
]
|
157
|
-
if opts[:operation
|
169
|
+
if opts[:operation] == 'average'
|
158
170
|
# include count in average so we can recalculate the average
|
159
171
|
# across all shards if needed
|
160
172
|
select_values << operation_over_aggregate_column(
|
161
|
-
|
162
|
-
|
173
|
+
aggregate_column(opts[:column_name]),
|
174
|
+
'count', opts[:distinct]
|
175
|
+
).as('count')
|
163
176
|
end
|
164
177
|
|
165
178
|
haves = having_clause.send(:predicates)
|
166
179
|
select_values += select_values unless haves.empty?
|
167
|
-
select_values.concat
|
180
|
+
select_values.concat(opts[:group_fields].zip(opts[:group_aliases]).map do |field, aliaz|
|
168
181
|
if field.respond_to?(:as)
|
169
182
|
field.as(aliaz)
|
170
183
|
else
|
171
184
|
"#{field} AS #{aliaz}"
|
172
185
|
end
|
173
|
-
|
186
|
+
end)
|
174
187
|
|
175
188
|
relation = except(:group)
|
176
189
|
relation.group_values = group
|
@@ -181,12 +194,12 @@ module Switchman
|
|
181
194
|
def compact_grouped_calculation_rows(rows, opts)
|
182
195
|
result = ::ActiveSupport::OrderedHash.new
|
183
196
|
rows.each do |row|
|
184
|
-
key = opts[:group_columns].map { |aliaz,
|
197
|
+
key = opts[:group_columns].map { |aliaz, _column| row[aliaz] }
|
185
198
|
key = key.first if key.size == 1
|
186
199
|
value = row[opts[:aggregate_alias]]
|
187
200
|
|
188
201
|
if opts[:operation] == 'average'
|
189
|
-
if result.
|
202
|
+
if result.key?(key)
|
190
203
|
old_value, old_count = result[key]
|
191
204
|
new_count = old_count + row['count']
|
192
205
|
new_value = ((old_value * old_count) + (value * row['count'])) / new_count
|
@@ -194,30 +207,24 @@ module Switchman
|
|
194
207
|
else
|
195
208
|
result[key] = [value, row['count']]
|
196
209
|
end
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
result[key] = value if value > result[key]
|
206
|
-
end
|
207
|
-
else
|
208
|
-
result[key] = value
|
210
|
+
elsif result.key?(key)
|
211
|
+
case opts[:operation]
|
212
|
+
when 'count', 'sum'
|
213
|
+
result[key] += value
|
214
|
+
when 'minimum'
|
215
|
+
result[key] = value if value < result[key]
|
216
|
+
when 'maximum'
|
217
|
+
result[key] = value if value > result[key]
|
209
218
|
end
|
219
|
+
else
|
220
|
+
result[key] = value
|
210
221
|
end
|
211
222
|
end
|
212
223
|
|
213
|
-
if opts[:operation] == 'average'
|
214
|
-
result = Hash[result.map{|k, v| [k, v.first]}]
|
215
|
-
end
|
224
|
+
result.transform_values!(&:first) if opts[:operation] == 'average'
|
216
225
|
|
217
226
|
result
|
218
227
|
end
|
219
|
-
|
220
|
-
|
221
228
|
end
|
222
229
|
end
|
223
230
|
end
|
@@ -5,16 +5,7 @@ require 'switchman/errors'
|
|
5
5
|
module Switchman
|
6
6
|
module ActiveRecord
|
7
7
|
module ConnectionPool
|
8
|
-
def shard
|
9
|
-
Thread.current[tls_key] || Shard.default
|
10
|
-
end
|
11
|
-
|
12
|
-
def shard=(value)
|
13
|
-
Thread.current[tls_key] = value
|
14
|
-
end
|
15
|
-
|
16
8
|
def default_schema
|
17
|
-
raise "Not postgres!" unless self.spec.config[:adapter] == 'postgresql'
|
18
9
|
connection unless @schemas
|
19
10
|
# default shard will not switch databases immediately, so it won't be set yet
|
20
11
|
@schemas ||= connection.current_schemas
|
@@ -22,75 +13,37 @@ module Switchman
|
|
22
13
|
end
|
23
14
|
|
24
15
|
def checkout_new_connection
|
25
|
-
conn =
|
26
|
-
|
27
|
-
# without locking anything, but if spec returns not-the-object passed
|
28
|
-
# to initialize this pool, things break
|
29
|
-
spec.config[:shard_name] = self.shard.name
|
30
|
-
|
31
|
-
super
|
32
|
-
end
|
33
|
-
conn.shard = self.shard
|
16
|
+
conn = super
|
17
|
+
conn.shard = current_shard
|
34
18
|
conn
|
35
19
|
end
|
36
20
|
|
37
21
|
def connection(switch_shard: true)
|
38
22
|
conn = super()
|
39
|
-
raise NonExistentShardError if
|
40
|
-
|
23
|
+
raise NonExistentShardError if current_shard.new_record?
|
24
|
+
|
25
|
+
switch_database(conn) if conn.shard != current_shard && switch_shard
|
41
26
|
conn
|
42
27
|
end
|
43
28
|
|
44
29
|
def release_connection(with_id = Thread.current)
|
45
30
|
super(with_id)
|
46
31
|
|
47
|
-
|
48
|
-
clear_idle_connections!(Time.now - spec.config[:idle_timeout].to_i)
|
49
|
-
end
|
50
|
-
end
|
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
|
-
|
66
|
-
def clear_idle_connections!(since_when)
|
67
|
-
synchronize do
|
68
|
-
@connections.reject! do |conn|
|
69
|
-
if conn.last_query_at < since_when && !conn.in_use?
|
70
|
-
conn.disconnect!
|
71
|
-
true
|
72
|
-
else
|
73
|
-
false
|
74
|
-
end
|
75
|
-
end
|
76
|
-
@available.clear
|
77
|
-
@connections.each do |conn|
|
78
|
-
@available.add conn
|
79
|
-
end
|
80
|
-
end
|
32
|
+
flush
|
81
33
|
end
|
82
34
|
|
83
35
|
def switch_database(conn)
|
84
|
-
if !@schemas && conn.adapter_name == 'PostgreSQL' && !
|
85
|
-
@schemas = conn.current_schemas
|
86
|
-
end
|
36
|
+
@schemas = conn.current_schemas if !@schemas && conn.adapter_name == 'PostgreSQL' && !current_shard.database_server.config[:shard_name]
|
87
37
|
|
88
|
-
|
89
|
-
conn.shard = shard
|
38
|
+
conn.shard = current_shard
|
90
39
|
end
|
91
40
|
|
92
41
|
private
|
93
42
|
|
43
|
+
def current_shard
|
44
|
+
connection_klass.current_switchman_shard
|
45
|
+
end
|
46
|
+
|
94
47
|
def tls_key
|
95
48
|
"#{object_id}_shard".to_sym
|
96
49
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Switchman
|
4
|
+
module ActiveRecord
|
5
|
+
module DatabaseConfigurations
|
6
|
+
private
|
7
|
+
|
8
|
+
# key difference: assumes a hybrid two-tier structure; each third tier
|
9
|
+
# is implicitly named, and their config is constructing by merging into
|
10
|
+
# its parent
|
11
|
+
def build_configs(configs)
|
12
|
+
return configs.configurations if configs.is_a?(DatabaseConfigurations)
|
13
|
+
return configs if configs.is_a?(Array)
|
14
|
+
|
15
|
+
db_configs = configs.flat_map do |env_name, config|
|
16
|
+
roles = config.keys.select { |k| config[k].is_a?(Hash) }
|
17
|
+
base_config = config.except(*roles)
|
18
|
+
|
19
|
+
name = "#{env_name}/primary"
|
20
|
+
name = 'primary' if env_name == default_env
|
21
|
+
base_db = build_db_config_from_raw_config(env_name, name, base_config)
|
22
|
+
[base_db] + roles.map do |role|
|
23
|
+
build_db_config_from_raw_config(env_name, "#{env_name}/#{role}",
|
24
|
+
base_config.merge(config[role]).merge(replica: true))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
db_configs << environment_url_config(default_env, 'primary', {}) unless db_configs.find(&:for_current_env?)
|
29
|
+
|
30
|
+
merge_db_environment_variables(default_env, db_configs.compact)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|