active_record_shards 3.11.0 → 3.19.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,13 +1,19 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'active_record_shards/shard_support'
3
4
 
4
5
  module ActiveRecordShards
5
6
  module ConnectionSwitcher
6
- SHARD_NAMES_CONFIG_KEY = 'shard_names'.freeze
7
+ SHARD_NAMES_CONFIG_KEY = 'shard_names'
7
8
 
8
9
  def self.extended(base)
9
- base.singleton_class.send(:alias_method, :columns_without_default_shard, :columns)
10
- base.singleton_class.send(:alias_method, :columns, :columns_with_default_shard)
10
+ if ActiveRecord::VERSION::MAJOR >= 5
11
+ base.singleton_class.send(:alias_method, :load_schema_without_default_shard!, :load_schema!)
12
+ base.singleton_class.send(:alias_method, :load_schema!, :load_schema_with_default_shard!)
13
+ else
14
+ base.singleton_class.send(:alias_method, :columns_without_default_shard, :columns)
15
+ base.singleton_class.send(:alias_method, :columns, :columns_with_default_shard)
16
+ end
11
17
 
12
18
  base.singleton_class.send(:alias_method, :table_exists_without_default_shard?, :table_exists?)
13
19
  base.singleton_class.send(:alias_method, :table_exists?, :table_exists_with_default_shard?)
@@ -18,6 +24,10 @@ module ActiveRecordShards
18
24
  switch_connection(shard: new_default_shard)
19
25
  end
20
26
 
27
+ def on_primary_db(&block)
28
+ on_shard(nil, &block)
29
+ end
30
+
21
31
  def on_shard(shard)
22
32
  old_options = current_shard_selection.options
23
33
  switch_connection(shard: shard) if supports_sharding?
@@ -26,9 +36,9 @@ module ActiveRecordShards
26
36
  switch_connection(old_options)
27
37
  end
28
38
 
29
- def on_first_shard
39
+ def on_first_shard(&block)
30
40
  shard_name = shard_names.first
31
- on_shard(shard_name) { yield }
41
+ on_shard(shard_name, &block)
32
42
  end
33
43
 
34
44
  def shards
@@ -49,70 +59,79 @@ module ActiveRecordShards
49
59
  switch_connection(old_options)
50
60
  end
51
61
 
52
- def on_slave_if(condition, &block)
53
- condition ? on_slave(&block) : yield
62
+ def on_replica_if(condition, &block)
63
+ condition ? on_replica(&block) : yield
54
64
  end
65
+ alias_method :on_slave_if, :on_replica_if
55
66
 
56
- def on_slave_unless(condition, &block)
57
- on_slave_if(!condition, &block)
67
+ def on_replica_unless(condition, &block)
68
+ on_replica_if(!condition, &block)
58
69
  end
70
+ alias_method :on_slave_unless, :on_replica_unless
59
71
 
60
- def on_master_if(condition, &block)
61
- condition ? on_master(&block) : yield
72
+ def on_primary_if(condition, &block)
73
+ condition ? on_primary(&block) : yield
62
74
  end
75
+ alias_method :on_master_if, :on_primary_if
63
76
 
64
- def on_master_unless(condition, &block)
65
- on_master_if(!condition, &block)
77
+ def on_primary_unless(condition, &block)
78
+ on_primary_if(!condition, &block)
66
79
  end
80
+ alias_method :on_master_unless, :on_primary_unless
67
81
 
68
- def on_master_or_slave(which, &block)
82
+ def on_primary_or_replica(which, &block)
69
83
  if block_given?
70
84
  on_cx_switch_block(which, &block)
71
85
  else
72
- MasterSlaveProxy.new(self, which)
86
+ PrimaryReplicaProxy.new(self, which)
73
87
  end
74
88
  end
89
+ alias_method :on_master_or_slave, :on_primary_or_replica
75
90
 
76
- # Executes queries using the slave database. Fails over to master if no slave is found.
77
- # if you want to execute a block of code on the slave you can go:
78
- # Account.on_slave do
91
+ # Executes queries using the replica database. Fails over to primary if no replica is found.
92
+ # if you want to execute a block of code on the replica you can go:
93
+ # Account.on_replica do
79
94
  # Account.first
80
95
  # end
81
- # the first account will be found on the slave DB
96
+ # the first account will be found on the replica DB
82
97
  #
83
98
  # For one-liners you can simply do
84
- # Account.on_slave.first
85
- def on_slave(&block)
86
- on_master_or_slave(:slave, &block)
99
+ # Account.on_replica.first
100
+ def on_replica(&block)
101
+ on_primary_or_replica(:replica, &block)
87
102
  end
103
+ alias_method :on_slave, :on_replica
88
104
 
89
- def on_master(&block)
90
- on_master_or_slave(:master, &block)
105
+ def on_primary(&block)
106
+ on_primary_or_replica(:primary, &block)
91
107
  end
108
+ alias_method :on_master, :on_primary
92
109
 
93
110
  # just to ease the transition from replica to active_record_shards
94
- alias_method :with_slave, :on_slave
95
- alias_method :with_slave_if, :on_slave_if
96
- alias_method :with_slave_unless, :on_slave_unless
111
+ alias_method :with_slave, :on_replica
112
+ alias_method :with_slave_if, :on_replica_if
113
+ alias_method :with_slave_unless, :on_replica_unless
97
114
 
98
115
  def on_cx_switch_block(which, force: false, construct_ro_scope: nil, &block)
99
- @disallow_slave ||= 0
100
- @disallow_slave += 1 if which == :master
116
+ @disallow_replica ||= 0
117
+ @disallow_replica += 1 if [:primary, :master].include?(which)
101
118
 
102
- switch_to_slave = force || @disallow_slave.zero?
119
+ ActiveRecordShards::Deprecation.warn('the `:master` option should be replaced with `:primary`!') if which == :master
120
+
121
+ switch_to_replica = force || @disallow_replica.zero?
103
122
  old_options = current_shard_selection.options
104
123
 
105
- switch_connection(slave: switch_to_slave)
124
+ switch_connection(replica: switch_to_replica)
106
125
 
107
126
  # we avoid_readonly_scope to prevent some stack overflow problems, like when
108
127
  # .columns calls .with_scope which calls .columns and onward, endlessly.
109
- if self == ActiveRecord::Base || !switch_to_slave || construct_ro_scope == false
128
+ if self == ActiveRecord::Base || !switch_to_replica || construct_ro_scope == false
110
129
  yield
111
130
  else
112
131
  readonly.scoping(&block)
113
132
  end
114
133
  ensure
115
- @disallow_slave -= 1 if which == :master
134
+ @disallow_replica -= 1 if [:primary, :master].include?(which)
116
135
  switch_connection(old_options) if old_options
117
136
  end
118
137
 
@@ -120,9 +139,10 @@ module ActiveRecordShards
120
139
  shard_names.any?
121
140
  end
122
141
 
123
- def on_slave?
124
- current_shard_selection.on_slave?
142
+ def on_replica?
143
+ current_shard_selection.on_replica?
125
144
  end
145
+ alias_method :on_slave?, :on_replica?
126
146
 
127
147
  def current_shard_selection
128
148
  Thread.current[:shard_selection] ||= ShardSelection.new
@@ -134,8 +154,12 @@ module ActiveRecordShards
134
154
 
135
155
  def shard_names
136
156
  unless config = configurations[shard_env]
137
- raise "Did not find #{shard_env} in configurations, did you forget to add it to your database.yml ? (configurations: #{configurations.inspect})"
157
+ raise "Did not find #{shard_env} in configurations, did you forget to add it to your database config? (configurations: #{configurations.keys.inspect})"
158
+ end
159
+ unless config.fetch(SHARD_NAMES_CONFIG_KEY, []).all? { |shard_name| shard_name.is_a?(Integer) }
160
+ raise "All shard names must be integers: #{config[SHARD_NAMES_CONFIG_KEY].inspect}."
138
161
  end
162
+
139
163
  config[SHARD_NAMES_CONFIG_KEY] || []
140
164
  end
141
165
 
@@ -143,11 +167,15 @@ module ActiveRecordShards
143
167
 
144
168
  def switch_connection(options)
145
169
  if options.any?
146
- if options.key?(:slave)
147
- current_shard_selection.on_slave = options[:slave]
170
+ if options.key?(:replica)
171
+ current_shard_selection.on_replica = options[:replica]
148
172
  end
149
173
 
150
174
  if options.key?(:shard)
175
+ unless configurations[shard_env]
176
+ raise "Did not find #{shard_env} in configurations, did you forget to add it to your database config? (configurations: #{configurations.keys.inspect})"
177
+ end
178
+
151
179
  current_shard_selection.shard = options[:shard]
152
180
  end
153
181
 
@@ -159,51 +187,51 @@ module ActiveRecordShards
159
187
  ActiveRecordShards.rails_env
160
188
  end
161
189
 
162
- if ActiveRecord::VERSION::MAJOR >= 4
163
- def with_default_shard
164
- if is_sharded? && current_shard_id.nil? && table_name != ActiveRecord::SchemaMigration.table_name
165
- on_first_shard { yield }
166
- else
167
- yield
168
- end
169
- end
170
- else
171
- def with_default_shard
172
- if is_sharded? && current_shard_id.nil? && table_name != ActiveRecord::Migrator.schema_migrations_table_name
173
- on_first_shard { yield }
174
- else
175
- yield
176
- end
190
+ # Make these few schema related methods available before having switched to
191
+ # a shard.
192
+ def with_default_shard(&block)
193
+ if is_sharded? && current_shard_id.nil? && table_name != ActiveRecord::SchemaMigration.table_name
194
+ on_first_shard(&block)
195
+ else
196
+ yield
177
197
  end
178
198
  end
179
199
 
180
- def columns_with_default_shard
181
- with_default_shard { columns_without_default_shard }
200
+ if ActiveRecord::VERSION::MAJOR >= 5
201
+ def load_schema_with_default_shard!
202
+ with_default_shard { load_schema_without_default_shard! }
203
+ end
204
+ else
205
+ def columns_with_default_shard
206
+ with_default_shard { columns_without_default_shard }
207
+ end
182
208
  end
183
209
 
184
210
  def table_exists_with_default_shard?
185
211
  with_default_shard { table_exists_without_default_shard? }
186
212
  end
187
213
 
188
- class MasterSlaveProxy
214
+ class PrimaryReplicaProxy
189
215
  def initialize(target, which)
190
216
  @target = target
191
217
  @which = which
192
218
  end
193
219
 
194
- def method_missing(method, *args, &block) # rubocop:disable Style/MethodMissing
195
- @target.on_master_or_slave(@which) { @target.send(method, *args, &block) }
220
+ def method_missing(method, *args, &block) # rubocop:disable Style/MethodMissingSuper, Style/MissingRespondToMissing
221
+ @target.on_primary_or_replica(@which) { @target.send(method, *args, &block) }
196
222
  end
197
223
  end
224
+
225
+ MasterSlaveProxy = PrimaryReplicaProxy
198
226
  end
199
227
  end
200
228
 
201
229
  case "#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"
202
- when '3.2', '4.2'
203
- require 'active_record_shards/connection_switcher-4-0'
230
+ when '4.2'
231
+ require 'active_record_shards/connection_switcher-4-2'
204
232
  when '5.0'
205
233
  require 'active_record_shards/connection_switcher-5-0'
206
- when '5.1'
234
+ when '5.1', '5.2', '6.0'
207
235
  require 'active_record_shards/connection_switcher-5-1'
208
236
  else
209
237
  raise "ActiveRecordShards is not compatible with #{ActiveRecord::VERSION::STRING}"
@@ -0,0 +1,278 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordShards
4
+ module DefaultReplicaPatches
5
+ def self.wrap_method_in_on_replica(class_method, base, method, force_on_replica: false)
6
+ base_methods =
7
+ if class_method
8
+ base.methods + base.private_methods
9
+ else
10
+ base.instance_methods + base.private_instance_methods
11
+ end
12
+
13
+ return unless base_methods.include?(method)
14
+
15
+ _, method, punctuation = method.to_s.match(/^(.*?)([\?\!]?)$/).to_a
16
+ # _ALWAYS_ on replica, or only for on `on_replica_by_default = true` models?
17
+ wrapper = force_on_replica ? 'force_on_replica' : 'on_replica_unless_tx'
18
+ base.class_eval <<-RUBY, __FILE__, __LINE__ + 1
19
+ #{class_method ? 'class << self' : ''}
20
+ def #{method}_with_default_replica#{punctuation}(*args, &block)
21
+ #{wrapper} do
22
+ #{method}_without_default_replica#{punctuation}(*args, &block)
23
+ end
24
+ end
25
+
26
+ alias_method :#{method}_without_default_replica#{punctuation}, :#{method}#{punctuation}
27
+ alias_method :#{method}#{punctuation}, :#{method}_with_default_replica#{punctuation}
28
+ #{class_method ? 'end' : ''}
29
+ RUBY
30
+ end
31
+
32
+ def self.wrap_method_in_on_slave(*args)
33
+ ActiveRecordShards::Deprecation.deprecation_warning(
34
+ :'self.wrap_method_in_on_slave',
35
+ :'self.wrap_method_in_on_replica'
36
+ )
37
+ wrap_method_in_on_replica(*args)
38
+ end
39
+
40
+ def transaction_with_replica_off(*args, &block)
41
+ if on_replica_by_default?
42
+ begin
43
+ old_val = Thread.current[:_active_record_shards_in_tx]
44
+ Thread.current[:_active_record_shards_in_tx] = true
45
+ transaction_without_replica_off(*args, &block)
46
+ ensure
47
+ Thread.current[:_active_record_shards_in_tx] = old_val
48
+ end
49
+ else
50
+ transaction_without_replica_off(*args, &block)
51
+ end
52
+ end
53
+ alias_method :transaction_with_slave_off, :transaction_with_replica_off
54
+
55
+ module InstanceMethods
56
+ # fix ActiveRecord to do the right thing, and use our aliased quote_value
57
+ def quote_value(*args, &block)
58
+ self.class.quote_value(*args, &block)
59
+ end
60
+
61
+ def on_replica_unless_tx
62
+ self.class.on_replica_unless_tx { yield }
63
+ end
64
+ end
65
+
66
+ CLASS_REPLICA_METHODS = [
67
+ :calculate,
68
+ :count_by_sql,
69
+ :exists?,
70
+ :find,
71
+ :find_by,
72
+ :find_by_sql,
73
+ :find_every,
74
+ :find_one,
75
+ :find_some,
76
+ :get_primary_key
77
+ ].freeze
78
+
79
+ CLASS_FORCE_REPLICA_METHODS = [
80
+ :replace_bind_variable,
81
+ :replace_bind_variables,
82
+ :sanitize_sql_array,
83
+ :sanitize_sql_hash_for_assignment,
84
+ :table_exists?
85
+ ].freeze
86
+
87
+ CLASS_SLAVE_METHODS = CLASS_REPLICA_METHODS
88
+ CLASS_FORCE_SLAVE_METHODS = CLASS_FORCE_REPLICA_METHODS
89
+
90
+ def self.extended(base)
91
+ CLASS_REPLICA_METHODS.each { |m| ActiveRecordShards::DefaultReplicaPatches.wrap_method_in_on_replica(true, base, m) }
92
+ CLASS_FORCE_REPLICA_METHODS.each { |m| ActiveRecordShards::DefaultReplicaPatches.wrap_method_in_on_replica(true, base, m, force_on_replica: true) }
93
+
94
+ if ActiveRecord::VERSION::MAJOR >= 5
95
+ ActiveRecordShards::DefaultReplicaPatches.wrap_method_in_on_replica(true, base, :load_schema!, force_on_replica: true)
96
+ else
97
+ ActiveRecordShards::DefaultReplicaPatches.wrap_method_in_on_replica(true, base, :columns, force_on_replica: true)
98
+ end
99
+
100
+ ActiveRecordShards::DefaultReplicaPatches.wrap_method_in_on_replica(false, base, :reload)
101
+
102
+ base.class_eval do
103
+ include InstanceMethods
104
+
105
+ class << self
106
+ alias_method :transaction_without_replica_off, :transaction
107
+ alias_method :transaction, :transaction_with_replica_off
108
+ end
109
+ end
110
+ end
111
+
112
+ def on_replica_unless_tx(&block)
113
+ return yield if Thread.current[:_active_record_shards_in_migration]
114
+ return yield if Thread.current[:_active_record_shards_in_tx]
115
+
116
+ if on_replica_by_default?
117
+ on_replica(&block)
118
+ else
119
+ yield
120
+ end
121
+ end
122
+ alias_method :on_slave_unless_tx, :on_replica_unless_tx
123
+
124
+ def force_on_replica(&block)
125
+ return yield if Thread.current[:_active_record_shards_in_migration]
126
+
127
+ on_cx_switch_block(:replica, construct_ro_scope: false, force: true, &block)
128
+ end
129
+
130
+ module ActiveRelationPatches
131
+ def self.included(base)
132
+ [:calculate, :exists?, :pluck, :load].each do |m|
133
+ ActiveRecordShards::DefaultReplicaPatches.wrap_method_in_on_replica(false, base, m)
134
+ end
135
+
136
+ if ActiveRecord::VERSION::MAJOR == 4
137
+ # `where` and `having` clauses call `create_binds`, which will use the primary connection
138
+ ActiveRecordShards::DefaultReplicaPatches.wrap_method_in_on_replica(false, base, :create_binds, force_on_replica: true)
139
+ end
140
+
141
+ ActiveRecordShards::DefaultReplicaPatches.wrap_method_in_on_replica(false, base, :to_sql, force_on_replica: true)
142
+ end
143
+
144
+ def on_replica_unless_tx
145
+ @klass.on_replica_unless_tx { yield }
146
+ end
147
+ end
148
+
149
+ module Rails52RelationPatches
150
+ def connection
151
+ return super if Thread.current[:_active_record_shards_in_migration]
152
+ return super if Thread.current[:_active_record_shards_in_tx]
153
+
154
+ if @klass.on_replica_by_default?
155
+ @klass.on_replica.connection
156
+ else
157
+ super
158
+ end
159
+ end
160
+ end
161
+
162
+ # in rails 4.1+, they create a join class that's used to pull in records for HABTM.
163
+ # this simplifies the hell out of our existence, because all we have to do is inerit on-replica-by-default
164
+ # down from the parent now.
165
+ module Rails41HasAndBelongsToManyBuilderExtension
166
+ def self.included(base)
167
+ base.class_eval do
168
+ alias_method :through_model_without_inherit_default_replica_from_lhs, :through_model
169
+ alias_method :through_model, :through_model_with_inherit_default_replica_from_lhs
170
+ end
171
+ end
172
+
173
+ def through_model_with_inherit_default_replica_from_lhs
174
+ model = through_model_without_inherit_default_replica_from_lhs
175
+ def model.on_replica_by_default?
176
+ left_reflection.klass.on_replica_by_default?
177
+ end
178
+
179
+ # also transfer the sharded-ness of the left table to the join model
180
+ model.not_sharded unless model.left_reflection.klass.is_sharded?
181
+ model
182
+ end
183
+ end
184
+
185
+ module AssociationsAssociationAssociationScopePatch
186
+ def association_scope
187
+ if klass
188
+ on_replica_unless_tx { super }
189
+ else
190
+ super
191
+ end
192
+ end
193
+
194
+ def on_replica_unless_tx
195
+ klass.on_replica_unless_tx { yield }
196
+ end
197
+ end
198
+
199
+ module AssociationsAssociationFindTargetPatch
200
+ def find_target
201
+ if klass
202
+ on_replica_unless_tx { super }
203
+ else
204
+ super
205
+ end
206
+ end
207
+
208
+ def on_replica_unless_tx
209
+ klass.on_replica_unless_tx { yield }
210
+ end
211
+ end
212
+
213
+ module AssociationsAssociationGetRecordsPatch
214
+ def get_records # rubocop:disable Naming/AccessorMethodName
215
+ if klass
216
+ on_replica_unless_tx { super }
217
+ else
218
+ super
219
+ end
220
+ end
221
+
222
+ def on_replica_unless_tx
223
+ klass.on_replica_unless_tx { yield }
224
+ end
225
+ end
226
+
227
+ module AssociationsPreloaderAssociationAssociatedRecordsByOwnerPatch
228
+ def associated_records_by_owner(preloader)
229
+ if klass
230
+ on_replica_unless_tx { super }
231
+ else
232
+ super
233
+ end
234
+ end
235
+
236
+ def on_replica_unless_tx
237
+ klass.on_replica_unless_tx { yield }
238
+ end
239
+ end
240
+
241
+ module AssociationsPreloaderAssociationLoadRecordsPatch
242
+ def load_records
243
+ if klass
244
+ on_replica_unless_tx { super }
245
+ else
246
+ super
247
+ end
248
+ end
249
+
250
+ def on_replica_unless_tx
251
+ klass.on_replica_unless_tx { yield }
252
+ end
253
+ end
254
+
255
+ module TypeCasterConnectionConnectionPatch
256
+ def connection
257
+ return super if Thread.current[:_active_record_shards_in_migration]
258
+ return super if Thread.current[:_active_record_shards_in_tx]
259
+
260
+ if @klass.on_replica_by_default?
261
+ @klass.on_replica.connection
262
+ else
263
+ super
264
+ end
265
+ end
266
+ end
267
+
268
+ module SchemaDefinePatch
269
+ def define(info, &block)
270
+ old_val = Thread.current[:_active_record_shards_in_migration]
271
+ Thread.current[:_active_record_shards_in_migration] = true
272
+ super
273
+ ensure
274
+ Thread.current[:_active_record_shards_in_migration] = old_val
275
+ end
276
+ end
277
+ end
278
+ end