active_record_shards 4.0.0.beta8 → 5.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +144 -57
- data/lib/active_record_shards/association_collection_connection_selection.rb +16 -14
- data/lib/active_record_shards/configuration_parser.rb +6 -5
- data/lib/active_record_shards/{connection_switcher_5_1.rb → connection_switcher-5-1.rb} +11 -1
- data/lib/active_record_shards/connection_switcher-6-0.rb +30 -0
- data/lib/active_record_shards/connection_switcher.rb +130 -46
- data/lib/active_record_shards/default_replica_patches.rb +237 -0
- data/lib/active_record_shards/default_shard.rb +27 -0
- data/lib/active_record_shards/migration.rb +124 -0
- data/lib/active_record_shards/model.rb +36 -21
- data/lib/active_record_shards/schema_dumper_extension.rb +42 -0
- data/lib/active_record_shards/shard_selection.rb +47 -7
- data/lib/active_record_shards/shard_support.rb +7 -7
- data/lib/active_record_shards/sql_comments.rb +11 -4
- data/lib/active_record_shards/tasks.rb +53 -29
- data/lib/active_record_shards.rb +60 -12
- metadata +65 -52
- data/lib/active_record_shards/base_config.rb +0 -26
- data/lib/active_record_shards/connection_resolver.rb +0 -31
- data/lib/active_record_shards/connection_switcher_5_0.rb +0 -19
- data/lib/active_record_shards/default_slave_patches.rb +0 -136
- data/lib/active_record_shards/no_shard_selection.rb +0 -18
- data/lib/active_record_shards/sharded_model.rb +0 -31
- data/lib/active_record_shards/slave_db.rb +0 -105
@@ -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
|
-
|
7
|
-
"
|
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
|
-
|
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
|
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?(:@
|
27
|
-
base.instance_variable_get(:@
|
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
|
38
|
+
def on_replica_by_default=(value)
|
33
39
|
if self == ActiveRecord::Base
|
34
|
-
raise ArgumentError, "Cannot set
|
40
|
+
raise ArgumentError, "Cannot set on_replica_by_default on ActiveRecord::Base"
|
35
41
|
else
|
36
|
-
base_class.instance_variable_set(:@
|
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
|
42
|
-
@
|
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
|
46
|
-
@
|
56
|
+
def from_shard
|
57
|
+
@from_shard
|
47
58
|
end
|
48
59
|
end
|
49
60
|
|
50
61
|
def self.extended(base)
|
51
|
-
base.include
|
52
|
-
base.after_initialize :
|
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
|
-
|
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
|
7
|
-
@
|
46
|
+
def on_replica?
|
47
|
+
@on_replica
|
8
48
|
end
|
9
49
|
|
10
|
-
def
|
11
|
-
|
50
|
+
def on_replica=(new_replica)
|
51
|
+
@on_replica = (new_replica == true)
|
12
52
|
end
|
13
53
|
|
14
|
-
def
|
15
|
-
|
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
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
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
|
-
|
7
|
-
|
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.
|
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
|