active_record_shards 4.0.0.beta8 → 5.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.
@@ -0,0 +1,237 @@
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
+ ruby2_keywords(:#{method}_with_default_replica#{punctuation}) if respond_to?(:ruby2_keywords, true)
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 transaction_with_replica_off(*args, &block)
33
+ if on_replica_by_default?
34
+ begin
35
+ old_val = Thread.current[:_active_record_shards_in_tx]
36
+ Thread.current[:_active_record_shards_in_tx] = true
37
+ transaction_without_replica_off(*args, &block)
38
+ ensure
39
+ Thread.current[:_active_record_shards_in_tx] = old_val
40
+ end
41
+ else
42
+ transaction_without_replica_off(*args, &block)
43
+ end
44
+ end
45
+ ruby2_keywords(:transaction_with_replica_off) if respond_to?(:ruby2_keywords, true)
46
+
47
+ module InstanceMethods
48
+ def on_replica_unless_tx
49
+ self.class.on_replica_unless_tx { yield }
50
+ end
51
+ end
52
+
53
+ CLASS_REPLICA_METHODS = [
54
+ :calculate,
55
+ :count_by_sql,
56
+ :exists?,
57
+ :find,
58
+ :find_by,
59
+ :find_by_sql,
60
+ :find_every,
61
+ :find_one,
62
+ :find_some,
63
+ :get_primary_key
64
+ ].freeze
65
+
66
+ CLASS_FORCE_REPLICA_METHODS = [
67
+ :replace_bind_variable,
68
+ :replace_bind_variables,
69
+ :sanitize_sql_array,
70
+ :sanitize_sql_hash_for_assignment,
71
+ :table_exists?
72
+ ].freeze
73
+
74
+ def self.extended(base)
75
+ CLASS_REPLICA_METHODS.each { |m| ActiveRecordShards::DefaultReplicaPatches.wrap_method_in_on_replica(true, base, m) }
76
+ CLASS_FORCE_REPLICA_METHODS.each { |m| ActiveRecordShards::DefaultReplicaPatches.wrap_method_in_on_replica(true, base, m, force_on_replica: true) }
77
+
78
+ ActiveRecordShards::DefaultReplicaPatches.wrap_method_in_on_replica(true, base, :load_schema!, force_on_replica: true)
79
+ ActiveRecordShards::DefaultReplicaPatches.wrap_method_in_on_replica(false, base, :reload)
80
+
81
+ base.class_eval do
82
+ include InstanceMethods
83
+
84
+ class << self
85
+ alias_method :transaction_without_replica_off, :transaction
86
+ alias_method :transaction, :transaction_with_replica_off
87
+ end
88
+ end
89
+ end
90
+
91
+ def on_replica_unless_tx(&block)
92
+ return yield if Thread.current[:_active_record_shards_in_migration]
93
+ return yield if Thread.current[:_active_record_shards_in_tx]
94
+
95
+ if on_replica_by_default?
96
+ on_replica(&block)
97
+ else
98
+ yield
99
+ end
100
+ end
101
+
102
+ def force_on_replica(&block)
103
+ return yield if Thread.current[:_active_record_shards_in_migration]
104
+
105
+ on_cx_switch_block(:replica, construct_ro_scope: false, force: true, &block)
106
+ end
107
+
108
+ module ActiveRelationPatches
109
+ def self.included(base)
110
+ [:calculate, :exists?, :pluck, :load].each do |m|
111
+ ActiveRecordShards::DefaultReplicaPatches.wrap_method_in_on_replica(false, base, m)
112
+ end
113
+
114
+ ActiveRecordShards::DefaultReplicaPatches.wrap_method_in_on_replica(false, base, :to_sql, force_on_replica: true)
115
+ end
116
+
117
+ def on_replica_unless_tx
118
+ @klass.on_replica_unless_tx { yield }
119
+ end
120
+ end
121
+
122
+ module Rails52RelationPatches
123
+ def connection
124
+ return super if Thread.current[:_active_record_shards_in_migration]
125
+ return super if Thread.current[:_active_record_shards_in_tx]
126
+
127
+ if @klass.on_replica_by_default?
128
+ @klass.on_replica.connection
129
+ else
130
+ super
131
+ end
132
+ end
133
+ end
134
+
135
+ # in rails 4.1+, they create a join class that's used to pull in records for HABTM.
136
+ # this simplifies the hell out of our existence, because all we have to do is inerit on-replica-by-default
137
+ # down from the parent now.
138
+ module Rails41HasAndBelongsToManyBuilderExtension
139
+ def self.included(base)
140
+ base.class_eval do
141
+ alias_method :through_model_without_inherit_default_replica_from_lhs, :through_model
142
+ alias_method :through_model, :through_model_with_inherit_default_replica_from_lhs
143
+ end
144
+ end
145
+
146
+ def through_model_with_inherit_default_replica_from_lhs
147
+ model = through_model_without_inherit_default_replica_from_lhs
148
+ def model.on_replica_by_default?
149
+ left_reflection.klass.on_replica_by_default?
150
+ end
151
+
152
+ # also transfer the sharded-ness of the left table to the join model
153
+ model.not_sharded unless model.left_reflection.klass.is_sharded?
154
+ model
155
+ end
156
+ end
157
+
158
+ module AssociationsAssociationAssociationScopePatch
159
+ def association_scope
160
+ if klass
161
+ on_replica_unless_tx { super }
162
+ else
163
+ super
164
+ end
165
+ end
166
+
167
+ def on_replica_unless_tx
168
+ klass.on_replica_unless_tx { yield }
169
+ end
170
+ end
171
+
172
+ module AssociationsAssociationFindTargetPatch
173
+ def find_target
174
+ if klass
175
+ on_replica_unless_tx { super }
176
+ else
177
+ super
178
+ end
179
+ end
180
+
181
+ def on_replica_unless_tx
182
+ klass.on_replica_unless_tx { yield }
183
+ end
184
+ end
185
+
186
+ module AssociationsPreloaderAssociationAssociatedRecordsByOwnerPatch
187
+ def associated_records_by_owner(preloader)
188
+ if klass
189
+ on_replica_unless_tx { super }
190
+ else
191
+ super
192
+ end
193
+ end
194
+
195
+ def on_replica_unless_tx
196
+ klass.on_replica_unless_tx { yield }
197
+ end
198
+ end
199
+
200
+ module AssociationsPreloaderAssociationLoadRecordsPatch
201
+ def load_records
202
+ if klass
203
+ on_replica_unless_tx { super }
204
+ else
205
+ super
206
+ end
207
+ end
208
+
209
+ def on_replica_unless_tx
210
+ klass.on_replica_unless_tx { yield }
211
+ end
212
+ end
213
+
214
+ module TypeCasterConnectionConnectionPatch
215
+ def connection
216
+ return super if Thread.current[:_active_record_shards_in_migration]
217
+ return super if Thread.current[:_active_record_shards_in_tx]
218
+
219
+ if @klass.on_replica_by_default?
220
+ @klass.on_replica.connection
221
+ else
222
+ super
223
+ end
224
+ end
225
+ end
226
+
227
+ module SchemaDefinePatch
228
+ def define(info, &block)
229
+ old_val = Thread.current[:_active_record_shards_in_migration]
230
+ Thread.current[:_active_record_shards_in_migration] = true
231
+ super
232
+ ensure
233
+ Thread.current[:_active_record_shards_in_migration] = old_val
234
+ end
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordShards
4
+ module DefaultShard
5
+ def default_shard=(new_default_shard)
6
+ if ars_shard_type?(new_default_shard)
7
+ ActiveRecordShards::ShardSelection.ars_default_shard = new_default_shard
8
+ switch_connection(shard: new_default_shard)
9
+ else
10
+ super
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def ars_shard_type?(shard)
17
+ return true if ActiveRecord.version < Gem::Version.new('6.1')
18
+ return true if shard.nil?
19
+ return true if shard == :_no_shard
20
+ return true if shard.is_a?(Integer)
21
+
22
+ false
23
+ end
24
+ end
25
+ end
26
+
27
+ ActiveRecord::Base.singleton_class.prepend(ActiveRecordShards::DefaultShard)
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ class Migrator
5
+ def self.shards_migration_context
6
+ if ActiveRecord::VERSION::MAJOR >= 6
7
+ ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths, ActiveRecord::SchemaMigration)
8
+ elsif ActiveRecord::VERSION::STRING >= '5.2.0'
9
+ ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths)
10
+ else
11
+ self
12
+ end
13
+ end
14
+
15
+ def initialize_with_sharding(*args)
16
+ initialize_without_sharding(*args)
17
+
18
+ # Rails creates the internal tables on the unsharded DB. We make them
19
+ # manually on the sharded DBs.
20
+ ActiveRecord::Base.on_all_shards do
21
+ ActiveRecord::SchemaMigration.create_table
22
+ ActiveRecord::InternalMetadata.create_table
23
+ end
24
+ end
25
+ ruby2_keywords(:initialize_with_sharding) if respond_to?(:ruby2_keywords, true)
26
+ alias_method :initialize_without_sharding, :initialize
27
+ alias_method :initialize, :initialize_with_sharding
28
+
29
+ def run_with_sharding
30
+ ActiveRecord::Base.on_shard(nil) { run_without_sharding }
31
+ ActiveRecord::Base.on_all_shards { run_without_sharding }
32
+ end
33
+ alias_method :run_without_sharding, :run
34
+ alias_method :run, :run_with_sharding
35
+
36
+ def migrate_with_sharding
37
+ ActiveRecord::Base.on_shard(nil) { migrate_without_sharding }
38
+ ActiveRecord::Base.on_all_shards { migrate_without_sharding }
39
+ end
40
+ alias_method :migrate_without_sharding, :migrate
41
+ alias_method :migrate, :migrate_with_sharding
42
+
43
+ # don't allow Migrator class to cache versions
44
+ undef migrated
45
+ def migrated
46
+ self.class.shards_migration_context.get_all_versions
47
+ end
48
+
49
+ # list of pending migrations is any migrations that haven't run on all shards.
50
+ undef pending_migrations
51
+ def pending_migrations
52
+ pending, _missing = self.class.shard_status(migrations.map(&:version))
53
+ pending = pending.values.flatten
54
+ migrations.select { |m| pending.include?(m.version) }
55
+ end
56
+
57
+ # public
58
+ # list of pending and missing versions per shard
59
+ # [{1 => [1234567]}, {1 => [2345678]}]
60
+ def self.shard_status(versions)
61
+ pending = {}
62
+ missing = {}
63
+
64
+ collect = lambda do |shard|
65
+ migrated = shards_migration_context.get_all_versions
66
+
67
+ p = versions - migrated
68
+ pending[shard] = p if p.any?
69
+
70
+ m = migrated - versions
71
+ missing[shard] = m if m.any?
72
+ end
73
+
74
+ ActiveRecord::Base.on_shard(nil) { collect.call(nil) }
75
+ ActiveRecord::Base.on_all_shards { |shard| collect.call(shard) }
76
+
77
+ [pending, missing]
78
+ end
79
+ end
80
+ end
81
+
82
+ module ActiveRecordShards
83
+ module MigrationClassExtension
84
+ attr_accessor :migration_shard
85
+
86
+ def shard(arg = nil)
87
+ self.migration_shard = arg
88
+ end
89
+ end
90
+
91
+ module ActualMigrationExtension
92
+ def migrate_with_forced_shard(direction)
93
+ if migration_shard.blank?
94
+ raise "#{name}: Can't run migrations without a shard spec: this may be :all, :none,
95
+ or a specific shard (for data-fixups). please call shard(arg) in your migration."
96
+ end
97
+
98
+ shard = ActiveRecord::Base.current_shard_selection.shard
99
+
100
+ if shard.nil?
101
+ return if migration_shard != :none
102
+ else
103
+ return if migration_shard == :none
104
+ return if migration_shard != :all && migration_shard.to_s != shard.to_s
105
+ end
106
+
107
+ migrate_without_forced_shard(direction)
108
+ end
109
+
110
+ def migration_shard
111
+ self.class.migration_shard
112
+ end
113
+ end
114
+ end
115
+
116
+ ActiveRecord::Migration.class_eval do
117
+ extend ActiveRecordShards::MigrationClassExtension
118
+ include ActiveRecordShards::ActualMigrationExtension
119
+
120
+ alias_method :migrate_without_forced_shard, :migrate
121
+ alias_method :migrate, :migrate_with_forced_shard
122
+ end
123
+
124
+ ActiveRecord::MigrationProxy.delegate :migration_shard, to: :migration
@@ -3,53 +3,68 @@
3
3
  module ActiveRecordShards
4
4
  module Model
5
5
  def not_sharded
6
- message = "Calling not_sharded is deprecated. "\
7
- "Please ensure to still read from the "\
8
- "account db slave after removing the "\
9
- "call."
10
- if ActiveRecord::Base.logger
11
- ActiveRecord::Base.logger.warn(message)
12
- else
13
- Kernel.warn(message)
6
+ if self != ActiveRecord::Base && self != base_class
7
+ raise "You should only call not_sharded on direct descendants of ActiveRecord::Base"
14
8
  end
9
+
10
+ self.sharded = false
15
11
  end
16
12
 
17
13
  def is_sharded? # rubocop:disable Naming/PredicateName
18
- false
14
+ if self == ActiveRecord::Base
15
+ sharded != false && supports_sharding?
16
+ elsif self == base_class
17
+ if sharded.nil?
18
+ ActiveRecord::Base.is_sharded?
19
+ else
20
+ sharded != false
21
+ end
22
+ else
23
+ base_class.is_sharded?
24
+ end
19
25
  end
20
26
 
21
- def on_slave_by_default?
27
+ def on_replica_by_default?
22
28
  if self == ActiveRecord::Base
23
29
  false
24
30
  else
25
31
  base = base_class
26
- if base.instance_variable_defined?(:@on_slave_by_default)
27
- base.instance_variable_get(:@on_slave_by_default)
32
+ if base.instance_variable_defined?(:@on_replica_by_default)
33
+ base.instance_variable_get(:@on_replica_by_default)
28
34
  end
29
35
  end
30
36
  end
31
37
 
32
- def on_slave_by_default=(value)
38
+ def on_replica_by_default=(value)
33
39
  if self == ActiveRecord::Base
34
- raise ArgumentError, "Cannot set on_slave_by_default on ActiveRecord::Base"
40
+ raise ArgumentError, "Cannot set on_replica_by_default on ActiveRecord::Base"
35
41
  else
36
- base_class.instance_variable_set(:@on_slave_by_default, value)
42
+ base_class.instance_variable_set(:@on_replica_by_default, value)
37
43
  end
38
44
  end
39
45
 
40
46
  module InstanceMethods
41
- def initialize_slave
42
- @from_slave = !!self.class.current_slave_selection
47
+ def initialize_shard_and_replica
48
+ @from_replica = !!self.class.current_shard_selection.options[:replica]
49
+ @from_shard = self.class.current_shard_selection.options[:shard]
50
+ end
51
+
52
+ def from_replica?
53
+ @from_replica
43
54
  end
44
55
 
45
- def from_slave?
46
- @from_slave
56
+ def from_shard
57
+ @from_shard
47
58
  end
48
59
  end
49
60
 
50
61
  def self.extended(base)
51
- base.include(InstanceMethods)
52
- base.after_initialize :initialize_slave
62
+ base.send(:include, InstanceMethods)
63
+ base.after_initialize :initialize_shard_and_replica
53
64
  end
65
+
66
+ private
67
+
68
+ attr_accessor :sharded
54
69
  end
55
70
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordShards
4
+ module SchemaDumperExtension
5
+ def dump(stream)
6
+ stream = super(stream)
7
+ original_connection = @connection
8
+
9
+ if ActiveRecord::Base.supports_sharding?
10
+ ActiveRecord::Base.on_first_shard do
11
+ @connection = ActiveRecord::Base.connection
12
+ shard_header(stream)
13
+ extensions(stream)
14
+ tables(stream)
15
+ shard_trailer(stream)
16
+ end
17
+ end
18
+
19
+ stream
20
+ ensure
21
+ @connection = original_connection
22
+ end
23
+
24
+ def shard_header(stream)
25
+ define_params = @version ? "version: #{@version}" : ""
26
+
27
+ stream.puts <<~HEADER
28
+
29
+
30
+ # This section generated by active_record_shards
31
+
32
+ ActiveRecord::Base.on_all_shards do
33
+ ActiveRecord::Schema.define(#{define_params}) do
34
+
35
+ HEADER
36
+ end
37
+
38
+ def shard_trailer(stream)
39
+ stream.puts "end\nend"
40
+ end
41
+ end
42
+ end
@@ -1,18 +1,58 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module ActiveRecordShards
3
4
  class ShardSelection
4
- attr_reader :shard
5
+ NO_SHARD = :_no_shard
6
+ cattr_accessor :ars_default_shard
7
+
8
+ def initialize
9
+ @on_replica = false
10
+ @shard = nil
11
+ end
12
+
13
+ def shard
14
+ if @shard.nil? || @shard == NO_SHARD
15
+ nil
16
+ else
17
+ @shard || self.class.ars_default_shard
18
+ end
19
+ end
20
+
21
+ PRIMARY = "primary"
22
+ def resolve_connection_name(sharded:, configurations:)
23
+ resolved_shard = sharded ? shard : nil
24
+ env = ActiveRecordShards.app_env
25
+
26
+ @connection_names ||= {}
27
+ @connection_names[env] ||= {}
28
+ @connection_names[env][resolved_shard] ||= {}
29
+ @connection_names[env][resolved_shard][@on_replica] ||= begin
30
+ name = env.dup
31
+ name << "_shard_#{resolved_shard}" if resolved_shard
32
+ if @on_replica && configurations["#{name}_replica"]
33
+ "#{name}_replica"
34
+ else
35
+ # ActiveRecord always names its default connection pool 'primary'
36
+ # while everything else is named by the configuration name
37
+ resolved_shard ? name : PRIMARY
38
+ end
39
+ end
40
+ end
41
+
42
+ def shard=(new_shard)
43
+ @shard = (new_shard || NO_SHARD)
44
+ end
5
45
 
6
- def initialize(shard)
7
- @shard = Integer(shard)
46
+ def on_replica?
47
+ @on_replica
8
48
  end
9
49
 
10
- def connection_config
11
- { shard: shard }
50
+ def on_replica=(new_replica)
51
+ @on_replica = (new_replica == true)
12
52
  end
13
53
 
14
- def on_shard?
15
- true
54
+ def options
55
+ { shard: @shard, replica: @on_replica }
16
56
  end
17
57
  end
18
58
  end
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module ActiveRecordShards
3
4
  class ShardSupport
4
5
  class ShardEnumerator
5
6
  include Enumerable
6
7
 
7
8
  def each(&block)
8
- ActiveRecordShards::ShardedModel.on_all_shards(&block)
9
+ ActiveRecord::Base.on_all_shards(&block)
9
10
  end
10
11
  end
11
12
 
@@ -22,15 +23,14 @@ module ActiveRecordShards
22
23
 
23
24
  exception = nil
24
25
  enum.each do
25
- begin
26
- record = @scope.find(*find_args)
27
- return record if record
28
- rescue ActiveRecord::RecordNotFound => e
29
- exception = e
30
- end
26
+ record = @scope.find(*find_args)
27
+ return record if record
28
+ rescue ActiveRecord::RecordNotFound => e
29
+ exception = e
31
30
  end
32
31
  raise exception
33
32
  end
33
+ ruby2_keywords(:find) if respond_to?(:ruby2_keywords, true)
34
34
 
35
35
  def count
36
36
  enum.inject(0) { |accum, _shard| @scope.clone.count + accum }
@@ -1,16 +1,23 @@
1
- # show which connection was picked to debug master/slave slowness when both servers are the same
1
+ # show which connection was picked to debug primary/replica slowness when both servers are the same
2
2
  module ActiveRecordShards
3
3
  module SqlComments
4
4
  module Methods
5
5
  def execute(query, name = nil)
6
- slave = ActiveRecord::Base.current_slave_selection
7
- query += " /* #{slave ? 'slave' : 'master'} */"
6
+ shard = ActiveRecord::Base.current_shard_selection.shard
7
+ shard_text = shard ? "shard #{shard}" : 'unsharded'
8
+ replica = ActiveRecord::Base.current_shard_selection.on_replica?
9
+ replica_text = replica ? 'replica' : 'primary'
10
+ query = "/* #{shard_text} #{replica_text} */ " + query
8
11
  super(query, name)
9
12
  end
10
13
  end
11
14
 
12
15
  def self.enable
13
- ActiveRecord::Base.connection.class.prepend(Methods)
16
+ ActiveRecord::Base.on_replica do
17
+ ActiveRecord::Base.on_shard(nil) do
18
+ ActiveRecord::Base.connection.class.prepend(Methods)
19
+ end
20
+ end
14
21
  end
15
22
  end
16
23
  end