switchman 2.0.9 → 2.1.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 +14 -3
- data/lib/switchman.rb +2 -0
- data/lib/switchman/active_record/association.rb +1 -1
- data/lib/switchman/active_record/calculations.rb +1 -1
- data/lib/switchman/active_record/connection_handler.rb +23 -5
- data/lib/switchman/active_record/postgresql_adapter.rb +4 -0
- data/lib/switchman/active_record/predicate_builder.rb +1 -1
- data/lib/switchman/active_record/query_methods.rb +19 -3
- data/lib/switchman/active_record/reflection.rb +19 -8
- data/lib/switchman/active_record/relation.rb +59 -5
- data/lib/switchman/engine.rb +1 -0
- data/lib/switchman/test_helper.rb +1 -7
- data/lib/switchman/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a4ef7325514ffe64ab4fd53de062392b9edba2e7089ff35a8bbebfa475338a9d
|
4
|
+
data.tar.gz: cb74519796fee752ec5dc5e5469e39db240fbf819b38ea8d1ff5ae75c939601a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6583450d651aa95d06ad5d53a294bb2247ceee6fca42659a666da13378422fdaeeced89a772622de5671812fd7990531f470fd3d630870e6306d53e3e1ebc873
|
7
|
+
data.tar.gz: d1c21d744f5b926620f32540ca5c5c68b86ba97355b1fb8548ea829ccb05702173228eb40bf25e729a773be210436cfeb369ac2918a8aacea4da63d59b294404
|
@@ -18,7 +18,7 @@ module Switchman
|
|
18
18
|
:unsharded => [Shard]
|
19
19
|
}
|
20
20
|
private_constant :CATEGORIES
|
21
|
-
@connection_specification_name =
|
21
|
+
@connection_specification_name = 'unsharded'
|
22
22
|
|
23
23
|
if defined?(::ProtectedAttributes)
|
24
24
|
attr_accessible :default, :name, :database_server
|
@@ -283,17 +283,28 @@ module Switchman
|
|
283
283
|
|
284
284
|
with_each_shard(subscope, categories, options) { yield }
|
285
285
|
exception_pipe.last.close
|
286
|
-
rescue => e
|
286
|
+
rescue Exception => e
|
287
287
|
begin
|
288
288
|
dumped = Marshal.dump(e)
|
289
|
+
dumped = nil if dumped.length > 64 * 1024
|
289
290
|
rescue
|
291
|
+
dumped = nil
|
292
|
+
end
|
293
|
+
|
294
|
+
if dumped.nil?
|
290
295
|
# couldn't dump the exception; create a copy with just
|
291
296
|
# the message and the backtrace
|
292
297
|
e2 = e.class.new(e.message)
|
293
|
-
|
298
|
+
backtrace = e.backtrace
|
299
|
+
# truncate excessively long backtraces
|
300
|
+
if backtrace.length > 50
|
301
|
+
backtrace = backtrace[0...25] + ['...'] + backtrace[-25..-1]
|
302
|
+
end
|
303
|
+
e2.set_backtrace(backtrace)
|
294
304
|
e2.instance_variable_set(:@active_shards, e.instance_variable_get(:@active_shards))
|
295
305
|
dumped = Marshal.dump(e2)
|
296
306
|
end
|
307
|
+
|
297
308
|
exception_pipe.last.set_encoding(dumped.encoding)
|
298
309
|
exception_pipe.last.write(dumped)
|
299
310
|
exception_pipe.last.flush
|
data/lib/switchman.rb
CHANGED
@@ -87,7 +87,7 @@ module Switchman
|
|
87
87
|
# Copypasta from Activerecord but with added global_id_for goodness.
|
88
88
|
def records_for(ids)
|
89
89
|
scope.where(association_key_name => ids).load do |record|
|
90
|
-
global_key = if
|
90
|
+
global_key = if model.shard_category == :unsharded
|
91
91
|
convert_key(record[association_key_name])
|
92
92
|
else
|
93
93
|
Shard.global_id_for(record[association_key_name], record.shard)
|
@@ -51,7 +51,7 @@ module Switchman
|
|
51
51
|
|
52
52
|
def calculate_simple_average(column_name, distinct)
|
53
53
|
# See activerecord#execute_simple_calculation
|
54
|
-
relation =
|
54
|
+
relation = except(:order)
|
55
55
|
column = aggregate_column(column_name)
|
56
56
|
relation.select_values = [operation_over_aggregate_column(column, "average", distinct).as("average"),
|
57
57
|
operation_over_aggregate_column(column, "count", distinct).as("count")]
|
@@ -112,8 +112,20 @@ module Switchman
|
|
112
112
|
end
|
113
113
|
|
114
114
|
def remove_connection(spec_name)
|
115
|
+
# also remove pools based on the same spec name that are for shard category purposes
|
116
|
+
# can't just use delete_if, because it's a Concurrent::Map, not a Hash
|
117
|
+
owner_to_pool.keys.each do |k|
|
118
|
+
next if k == spec_name
|
119
|
+
|
120
|
+
v = owner_to_pool[k]
|
121
|
+
owner_to_pool.delete(k) if v.is_a?(ConnectionPoolProxy) && v.spec.name == spec_name
|
122
|
+
end
|
123
|
+
|
124
|
+
# unwrap the pool from inside a ConnectionPoolProxy
|
115
125
|
pool = owner_to_pool[spec_name]
|
116
126
|
owner_to_pool[spec_name] = pool.default_pool if pool.is_a?(ConnectionPoolProxy)
|
127
|
+
|
128
|
+
# now let Rails do its thing with the data type it expects
|
117
129
|
super
|
118
130
|
end
|
119
131
|
|
@@ -128,15 +140,21 @@ module Switchman
|
|
128
140
|
else
|
129
141
|
ancestor_pool.spec
|
130
142
|
end
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
143
|
+
# avoid copying "duplicate" pools that implement shard categories.
|
144
|
+
# they'll have a spec.name of primary, but a spec_name of something else, like unsharded
|
145
|
+
if spec.name == spec_name
|
146
|
+
pool = establish_connection(spec.to_hash)
|
147
|
+
pool.instance_variable_set(:@schema_cache, ancestor_pool.schema_cache) if ancestor_pool.schema_cache
|
148
|
+
next pool
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
if spec_name != "primary"
|
135
153
|
primary_pool = retrieve_connection_pool("primary")
|
136
154
|
if primary_pool.is_a?(ConnectionPoolProxy)
|
137
155
|
pool = ConnectionPoolProxy.new(spec_name.to_sym, primary_pool.default_pool, @shard_connection_pools)
|
138
156
|
pool.schema_cache.copy_references(primary_pool.schema_cache)
|
139
|
-
pool
|
157
|
+
owner_to_pool[spec_name] = pool
|
140
158
|
else
|
141
159
|
primary_pool
|
142
160
|
end
|
@@ -215,6 +215,10 @@ module Switchman
|
|
215
215
|
name.quoted
|
216
216
|
end
|
217
217
|
|
218
|
+
def with_global_table_name(&block)
|
219
|
+
with_local_table_name(false, &block)
|
220
|
+
end
|
221
|
+
|
218
222
|
def with_local_table_name(enable = true)
|
219
223
|
old_value = @use_local_table_name
|
220
224
|
@use_local_table_name = enable
|
@@ -233,6 +233,10 @@ module Switchman
|
|
233
233
|
connection.with_local_table_name { super }
|
234
234
|
end
|
235
235
|
|
236
|
+
def table_name_matches?(from)
|
237
|
+
connection.with_global_table_name { super }
|
238
|
+
end
|
239
|
+
|
236
240
|
# semi-private
|
237
241
|
public
|
238
242
|
def transpose_predicates(predicates,
|
@@ -242,6 +246,18 @@ module Switchman
|
|
242
246
|
binds: nil,
|
243
247
|
dup_binds_on_mutation: false)
|
244
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
|
+
|
245
261
|
next predicate unless predicate.is_a?(::Arel::Nodes::Binary)
|
246
262
|
next predicate unless predicate.left.is_a?(::Arel::Attributes::Attribute)
|
247
263
|
relation, column = relation_and_column(predicate.left)
|
@@ -250,7 +266,7 @@ module Switchman
|
|
250
266
|
remove = true if type == :primary &&
|
251
267
|
remove_nonlocal_primary_keys &&
|
252
268
|
predicate.left.relation.model == klass &&
|
253
|
-
predicate.is_a?(::Arel::Nodes::Equality)
|
269
|
+
(predicate.is_a?(::Arel::Nodes::Equality) || predicate.is_a?(::Arel::Nodes::In))
|
254
270
|
|
255
271
|
current_source_shard =
|
256
272
|
if source_shard
|
@@ -265,7 +281,7 @@ module Switchman
|
|
265
281
|
new_right_value =
|
266
282
|
case predicate.right
|
267
283
|
when Array
|
268
|
-
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
|
269
285
|
else
|
270
286
|
transpose_predicate_value(predicate.right, current_source_shard, target_shard, type, remove)
|
271
287
|
end
|
@@ -340,7 +356,7 @@ module Switchman
|
|
340
356
|
value
|
341
357
|
else
|
342
358
|
local_id = Shard.relative_id_for(current_id, current_shard, target_shard) || current_id
|
343
|
-
|
359
|
+
return nil if remove_non_local_ids && local_id.is_a?(Integer) && local_id > Shard::IDS_PER_SHARD
|
344
360
|
if current_id != local_id
|
345
361
|
# make a new bind param
|
346
362
|
::Arel::Nodes::BindParam.new(query_att.class.new(query_att.name, local_id, query_att.type))
|
@@ -28,15 +28,26 @@ module Switchman
|
|
28
28
|
# this technically belongs on AssociationReflection, but we put it on
|
29
29
|
# ThroughReflection as well, instead of delegating to its internal
|
30
30
|
# HasManyAssociation, losing its proper `klass`
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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)
|
35
50
|
end
|
36
|
-
key = [key, shard(owner).id].flatten
|
37
|
-
@association_scope_cache[key] ||= @scope_lock.synchronize {
|
38
|
-
@association_scope_cache[key] ||= (::Rails.version >= "5.2" ? ::ActiveRecord::StatementCache.create(conn, &block) : block.call)
|
39
|
-
}
|
40
51
|
end
|
41
52
|
end
|
42
53
|
|
@@ -62,7 +62,7 @@ module Switchman
|
|
62
62
|
%I{update_all delete_all}.each do |method|
|
63
63
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
64
64
|
def #{method}(*args)
|
65
|
-
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) }
|
66
66
|
result = result.sum if result.is_a?(Array)
|
67
67
|
result
|
68
68
|
end
|
@@ -97,7 +97,7 @@ module Switchman
|
|
97
97
|
end
|
98
98
|
end
|
99
99
|
|
100
|
-
def activate(&block)
|
100
|
+
def activate(unordered: false, &block)
|
101
101
|
shards = all_shards
|
102
102
|
if (Array === shards && shards.length == 1)
|
103
103
|
if shards.first == DefaultShard || shards.first == Shard.current(klass.shard_category)
|
@@ -106,10 +106,64 @@ module Switchman
|
|
106
106
|
shards.first.activate(klass.shard_category) { yield(self, shards.first) }
|
107
107
|
end
|
108
108
|
else
|
109
|
-
|
110
|
-
|
111
|
-
|
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?
|
112
165
|
end
|
166
|
+
result
|
113
167
|
end
|
114
168
|
end
|
115
169
|
end
|
data/lib/switchman/engine.rb
CHANGED
@@ -142,6 +142,7 @@ module Switchman
|
|
142
142
|
|
143
143
|
::ActiveRecord::Relation::WhereClauseFactory.prepend(ActiveRecord::WhereClauseFactory)
|
144
144
|
::ActiveRecord::PredicateBuilder::AssociationQueryValue.prepend(ActiveRecord::PredicateBuilder::AssociationQueryValue)
|
145
|
+
::ActiveRecord::PredicateBuilder::PolymorphicArrayValue.prepend(ActiveRecord::PredicateBuilder::AssociationQueryValue)
|
145
146
|
::ActiveRecord::TypeCaster::Map.include(ActiveRecord::TypeCaster::Map)
|
146
147
|
::ActiveRecord::TypeCaster::Connection.include(ActiveRecord::TypeCaster::Connection)
|
147
148
|
|
@@ -17,13 +17,7 @@ module Switchman
|
|
17
17
|
Shard.default(reload: true)
|
18
18
|
end
|
19
19
|
|
20
|
-
|
21
|
-
# default shard is split across multiple db servers
|
22
|
-
if ::ActiveRecord::Base.connection_handler.connection_pool_list.length > 1
|
23
|
-
server1 = DatabaseServer.create(Shard.default.database_server.config)
|
24
|
-
else
|
25
|
-
server1 = Shard.default.database_server
|
26
|
-
end
|
20
|
+
server1 = Shard.default.database_server
|
27
21
|
server2 = DatabaseServer.create(Shard.default.database_server.config.merge(server2: true))
|
28
22
|
|
29
23
|
if server1 == Shard.default.database_server && server1.config[:shard1] && server1.config[:shard2]
|
data/lib/switchman/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: switchman
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cody Cutrer
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2021-
|
13
|
+
date: 2021-07-12 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: railties
|