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.
- 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
|