switchman 2.0.8 → 2.0.13

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 697ce16e90d84de2535fdbf6dacc40640323dddffa4becb4a3d5e8c3b4d1a67f
4
- data.tar.gz: b94f2c678d8dd4d03c7dd089c441ced5a75923778c972a86a7cc3f20c126bc97
3
+ metadata.gz: de9d4ea9f0368bf1bfe15fdb8dd6bef040a8ec107aed386eaa81f8fa444513df
4
+ data.tar.gz: 71b7905b40d59a4767576552c8f5d16a2b3bdaec6d910f81f18d803a9c916256
5
5
  SHA512:
6
- metadata.gz: 82dff35e613f3f9147e7575f1865402df57375dd8f8396ae6f61ad86cf82ec401098b4210eecabb8596c9d7dbba3a3a3ba87c030d7b17b74aae1605cfc3efac9
7
- data.tar.gz: 8678d9c29a485baf8bc483d3c6717d4e2c88d2de14e51e4356a656b579bd0c65579274e0afd70a94d2fb1bc8c617b44a47d73bb04cd8a0b41f1ad2a378ff7e38
6
+ metadata.gz: bac07aa4f15521f123a1d95f676448d649c71eddd86fdfbbb57504f59d0c71f9ce69618aa7d1e25e93fdc278708cb2a7385d111417f23a8fc0eb1e9b6f6eefb3
7
+ data.tar.gz: 1c091615da88fe2f54de51de1e740336007fd13d66d0cac0b977d0e2770a6dfacd6a85bd12b689a0c610b1de5daeab4512743f0c50199e0f610806cc2f4dd629
@@ -718,6 +718,7 @@ module Switchman
718
718
  Switchman.cache.delete(['shard', id].join('/'))
719
719
  Switchman.cache.delete("default_shard") if default?
720
720
  end
721
+ self.class.clear_cache
721
722
  end
722
723
 
723
724
  def default_name
@@ -10,6 +10,6 @@ class AddDefaultShardIndex < ActiveRecord::Migration[4.2]
10
10
  else
11
11
  {}
12
12
  end
13
- add_index :switchman_shards, :default, options
13
+ add_index :switchman_shards, :default, **options
14
14
  end
15
15
  end
data/lib/switchman.rb CHANGED
@@ -17,4 +17,6 @@ module Switchman
17
17
  def self.cache=(cache)
18
18
  @cache = cache
19
19
  end
20
+
21
+ class OrderOnMultiShardQuery < RuntimeError; end
20
22
  end
@@ -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 record.class.shard_category == :unsharded
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 = reorder(nil)
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")]
@@ -135,7 +135,7 @@ module Switchman
135
135
  primary_pool = retrieve_connection_pool("primary")
136
136
  if primary_pool.is_a?(ConnectionPoolProxy)
137
137
  pool = ConnectionPoolProxy.new(spec_name.to_sym, primary_pool.default_pool, @shard_connection_pools)
138
- pool.schema_cache.copy_values(primary_pool.schema_cache)
138
+ pool.schema_cache.copy_references(primary_pool.schema_cache)
139
139
  pool
140
140
  else
141
141
  primary_pool
@@ -30,10 +30,42 @@ module Switchman
30
30
  end
31
31
 
32
32
  module Migrator
33
+ # significant change: hash shard id, not database name
33
34
  def generate_migrator_advisory_lock_id
34
- shard_name_hash = Zlib.crc32("#{Shard.current.id}:#{Shard.current.name}")
35
+ shard_name_hash = Zlib.crc32(Shard.current.name)
35
36
  ::ActiveRecord::Migrator::MIGRATOR_SALT * shard_name_hash
36
37
  end
38
+
39
+ if ::Rails.version >= '6.0'
40
+ # copy/paste from Rails 6.1
41
+ def with_advisory_lock
42
+ lock_id = generate_migrator_advisory_lock_id
43
+
44
+ with_advisory_lock_connection do |connection|
45
+ got_lock = connection.get_advisory_lock(lock_id)
46
+ raise ::ActiveRecord::ConcurrentMigrationError unless got_lock
47
+ load_migrated # reload schema_migrations to be sure it wasn't changed by another process before we got the lock
48
+ yield
49
+ ensure
50
+ if got_lock && !connection.release_advisory_lock(lock_id)
51
+ raise ::ActiveRecord::ConcurrentMigrationError.new(
52
+ ::ActiveRecord::ConcurrentMigrationError::RELEASE_LOCK_FAILED_MESSAGE
53
+ )
54
+ end
55
+ end
56
+ end
57
+
58
+ # significant change: strip out prefer_secondary from config
59
+ def with_advisory_lock_connection
60
+ pool = ::ActiveRecord::ConnectionAdapters::ConnectionHandler.new.establish_connection(
61
+ ::ActiveRecord::Base.connection_config.except(:prefer_secondary)
62
+ )
63
+
64
+ pool.with_connection { |connection| yield(connection) }
65
+ ensure
66
+ pool&.disconnect!
67
+ end
68
+ end
37
69
  end
38
70
 
39
71
  module MigrationContext
@@ -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
@@ -15,7 +15,7 @@ module Switchman
15
15
  def convert_to_id(value)
16
16
  case value
17
17
  when ::ActiveRecord::Base
18
- value.send(primary_key)
18
+ value.id
19
19
  else
20
20
  super
21
21
  end
@@ -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
- local_id = [] if remove_non_local_ids && local_id.is_a?(Integer) && local_id > Shard::IDS_PER_SHARD
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))
@@ -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
- # TODO: implement local limit to avoid querying extra shards
110
- Shard.with_each_shard(shards, [klass.shard_category]) do
111
- shard(Shard.current(klass.shard_category), :to_a).activate(&block)
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
@@ -84,6 +84,10 @@ module Switchman
84
84
  @schema_cache
85
85
  end
86
86
 
87
+ def set_schema_cache(cache)
88
+ @schema_cache.copy_values(cache)
89
+ end
90
+
87
91
  %w{release_connection
88
92
  disconnect!
89
93
  flush!
@@ -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
 
@@ -5,14 +5,22 @@ module Switchman
5
5
  delegate :connection, to: :pool
6
6
  attr_reader :pool
7
7
 
8
+ SHARED_IVS = %i{@columns @columns_hash @primary_keys @data_sources @indexes}.freeze
9
+
8
10
  def initialize(pool)
9
11
  @pool = pool
10
12
  super(nil)
11
13
  end
12
14
 
13
15
  def copy_values(other_cache)
16
+ SHARED_IVS.each do |iv|
17
+ instance_variable_get(iv).replace(other_cache.instance_variable_get(iv))
18
+ end
19
+ end
20
+
21
+ def copy_references(other_cache)
14
22
  # use the same cached values but still fall back to the correct pool
15
- [:@columns, :@columns_hash, :@primary_keys, :@data_sources].each do |iv|
23
+ SHARED_IVS.each do |iv|
16
24
  instance_variable_set(iv, other_cache.instance_variable_get(iv))
17
25
  end
18
26
  end
@@ -24,7 +24,7 @@ module Switchman
24
24
  else
25
25
  server1 = Shard.default.database_server
26
26
  end
27
- server2 = DatabaseServer.create(Shard.default.database_server.config)
27
+ server2 = DatabaseServer.create(Shard.default.database_server.config.merge(server2: true))
28
28
 
29
29
  if server1 == Shard.default.database_server && server1.config[:shard1] && server1.config[:shard2]
30
30
  # look for the shards in the db already
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Switchman
4
- VERSION = "2.0.8"
4
+ VERSION = "2.0.13"
5
5
  end
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: switchman
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.8
4
+ version: 2.0.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer
8
8
  - James Williams
9
9
  - Jacob Fugal
10
- autorequire:
10
+ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2021-04-01 00:00:00.000000000 Z
13
+ date: 2021-06-11 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: railties
@@ -257,7 +257,7 @@ homepage: http://www.instructure.com/
257
257
  licenses:
258
258
  - MIT
259
259
  metadata: {}
260
- post_install_message:
260
+ post_install_message:
261
261
  rdoc_options: []
262
262
  require_paths:
263
263
  - lib
@@ -272,8 +272,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
272
272
  - !ruby/object:Gem::Version
273
273
  version: '0'
274
274
  requirements: []
275
- rubygems_version: 3.0.3
276
- signing_key:
275
+ rubygems_version: 3.2.15
276
+ signing_key:
277
277
  specification_version: 4
278
278
  summary: Rails sharding magic
279
279
  test_files: []