active_record_shards 4.0.0.beta9 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,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 :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.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