switchman 3.0.21 → 3.0.24
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/lib/switchman/action_controller/caching.rb +2 -2
- data/lib/switchman/active_record/abstract_adapter.rb +1 -3
- data/lib/switchman/active_record/associations.rb +208 -0
- data/lib/switchman/active_record/base.rb +19 -8
- data/lib/switchman/active_record/connection_pool.rb +1 -3
- data/lib/switchman/active_record/table_definition.rb +1 -1
- data/lib/switchman/active_support/cache.rb +16 -0
- data/lib/switchman/database_server.rb +13 -9
- data/lib/switchman/default_shard.rb +0 -2
- data/lib/switchman/engine.rb +55 -122
- data/lib/switchman/errors.rb +4 -2
- data/lib/switchman/rails.rb +2 -5
- data/{app/models → lib}/switchman/shard.rb +4 -23
- data/lib/switchman/standard_error.rb +1 -5
- data/{app/models → lib}/switchman/unsharded_record.rb +1 -1
- data/lib/switchman/version.rb +1 -1
- data/lib/switchman.rb +22 -2
- metadata +5 -5
- data/lib/switchman/active_record/association.rb +0 -206
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 469ef6bca62776e9ba974a5c6d2a6220fc6ddcf1649956f43247465cbfb24f37
|
4
|
+
data.tar.gz: 9fefef5095835cf28a6c72197ed74f6de9974d7c9c9019ad4ea87209d98c37e9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 96d01726ff4e0e560e63964c5607417b7272202109b6e7e718769e9ba2cc04f82000f442c70aef8d6140e6dac00925197f0a95c70b790d47fbc2355d9021ae39
|
7
|
+
data.tar.gz: 4b2edb3c0400f3280ba579bfaac61fce263ea5dd0881e5eeb77e675e0d2c1dfe04168e059f7bb91ac601ce2ef210c50c252945258fe2166ee6cf21c458defac0
|
@@ -13,8 +13,8 @@ module Switchman
|
|
13
13
|
# disallow assigning to ActionController::Base.cache_store or
|
14
14
|
# ActionController::Base#cache_store for the same reasons we disallow
|
15
15
|
# assigning to Rails.cache
|
16
|
-
def cache_store=(
|
17
|
-
raise NoMethodError
|
16
|
+
def cache_store=(cache)
|
17
|
+
raise NoMethodError unless cache == ::Rails.cache
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
@@ -1,13 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'switchman/sharded_instrumenter'
|
4
|
-
|
5
3
|
module Switchman
|
6
4
|
module ActiveRecord
|
7
5
|
module AbstractAdapter
|
8
6
|
module ForeignKeyCheck
|
9
7
|
def add_column(table, name, type, limit: nil, **)
|
10
|
-
|
8
|
+
Switchman.foreign_key_check(name, type, limit: limit)
|
11
9
|
super
|
12
10
|
end
|
13
11
|
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Switchman
|
4
|
+
module ActiveRecord
|
5
|
+
module Associations
|
6
|
+
module Association
|
7
|
+
def shard
|
8
|
+
reflection.shard(owner)
|
9
|
+
end
|
10
|
+
|
11
|
+
def build_record(*args)
|
12
|
+
shard.activate { super }
|
13
|
+
end
|
14
|
+
|
15
|
+
def load_target
|
16
|
+
shard.activate { super }
|
17
|
+
end
|
18
|
+
|
19
|
+
def scope
|
20
|
+
shard_value = @reflection.options[:multishard] ? @owner : shard
|
21
|
+
@owner.shard.activate { super.shard(shard_value, :association) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module CollectionAssociation
|
26
|
+
def find_target
|
27
|
+
shards = reflection.options[:multishard] && owner.respond_to?(:associated_shards) ? owner.associated_shards : [shard]
|
28
|
+
# activate both the owner and the target's shard category, so that Reflection#join_id_for,
|
29
|
+
# when called for the owner, will be returned relative to shard the query will execute on
|
30
|
+
Shard.with_each_shard(shards, [klass.connection_classes, owner.class.connection_classes].uniq) do
|
31
|
+
super
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def _create_record(*)
|
36
|
+
shard.activate { super }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module BelongsToAssociation
|
41
|
+
def replace_keys(record, force: false)
|
42
|
+
if record&.class&.sharded_column?(reflection.association_primary_key(record.class))
|
43
|
+
foreign_id = record[reflection.association_primary_key(record.class)]
|
44
|
+
owner[reflection.foreign_key] = Shard.relative_id_for(foreign_id, record.shard, owner.shard)
|
45
|
+
else
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def shard
|
51
|
+
if @owner.class.sharded_column?(@reflection.foreign_key) &&
|
52
|
+
(foreign_id = @owner[@reflection.foreign_key])
|
53
|
+
Shard.shard_for(foreign_id, @owner.shard)
|
54
|
+
else
|
55
|
+
super
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
module ForeignAssociation
|
61
|
+
# significant change:
|
62
|
+
# * transpose the key to the correct shard
|
63
|
+
def set_owner_attributes(record) # rubocop:disable Naming/AccessorMethodName
|
64
|
+
return if options[:through]
|
65
|
+
|
66
|
+
key = owner._read_attribute(reflection.join_foreign_key)
|
67
|
+
key = Shard.relative_id_for(key, owner.shard, shard)
|
68
|
+
record._write_attribute(reflection.join_primary_key, key)
|
69
|
+
|
70
|
+
record._write_attribute(reflection.type, owner.class.polymorphic_name) if reflection.type
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
module Extension
|
75
|
+
def self.build(_model, _reflection); end
|
76
|
+
|
77
|
+
def self.valid_options
|
78
|
+
[:multishard]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
::ActiveRecord::Associations::Builder::Association.extensions << Extension
|
83
|
+
|
84
|
+
module Preloader
|
85
|
+
module Association
|
86
|
+
# Copypasta from Activerecord but with added global_id_for goodness.
|
87
|
+
def records_for(ids)
|
88
|
+
scope.where(association_key_name => ids).load do |record|
|
89
|
+
global_key = if model.connection_classes == UnshardedRecord
|
90
|
+
convert_key(record[association_key_name])
|
91
|
+
else
|
92
|
+
Shard.global_id_for(record[association_key_name], record.shard)
|
93
|
+
end
|
94
|
+
owner = owners_by_key[convert_key(global_key)].first
|
95
|
+
association = owner.association(reflection.name)
|
96
|
+
association.set_inverse_instance(record)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# significant changes:
|
101
|
+
# * partition_by_shard the records_for call
|
102
|
+
# * re-globalize the fetched owner id before looking up in the map
|
103
|
+
def load_records
|
104
|
+
# owners can be duplicated when a relation has a collection association join
|
105
|
+
# #compare_by_identity makes such owners different hash keys
|
106
|
+
@records_by_owner = {}.compare_by_identity
|
107
|
+
|
108
|
+
if owner_keys.empty?
|
109
|
+
raw_records = []
|
110
|
+
else
|
111
|
+
# determine the shard to search for each owner
|
112
|
+
if reflection.macro == :belongs_to
|
113
|
+
# for belongs_to, it's the shard of the foreign_key
|
114
|
+
partition_proc = lambda do |owner|
|
115
|
+
if owner.class.sharded_column?(owner_key_name)
|
116
|
+
Shard.shard_for(owner[owner_key_name], owner.shard)
|
117
|
+
else
|
118
|
+
Shard.current
|
119
|
+
end
|
120
|
+
end
|
121
|
+
elsif !reflection.options[:multishard]
|
122
|
+
# for non-multishard associations, it's *just* the owner's shard
|
123
|
+
partition_proc = ->(owner) { owner.shard }
|
124
|
+
end
|
125
|
+
|
126
|
+
raw_records = Shard.partition_by_shard(owners, partition_proc) do |partitioned_owners|
|
127
|
+
relative_owner_keys = partitioned_owners.map do |owner|
|
128
|
+
key = owner[owner_key_name]
|
129
|
+
if key && owner.class.sharded_column?(owner_key_name)
|
130
|
+
key = Shard.relative_id_for(key, owner.shard,
|
131
|
+
Shard.current(klass.connection_classes))
|
132
|
+
end
|
133
|
+
convert_key(key)
|
134
|
+
end
|
135
|
+
relative_owner_keys.compact!
|
136
|
+
relative_owner_keys.uniq!
|
137
|
+
records_for(relative_owner_keys)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
@preloaded_records = raw_records.select do |record|
|
142
|
+
assignments = false
|
143
|
+
|
144
|
+
owner_key = record[association_key_name]
|
145
|
+
if owner_key && record.class.sharded_column?(association_key_name)
|
146
|
+
owner_key = Shard.global_id_for(owner_key,
|
147
|
+
record.shard)
|
148
|
+
end
|
149
|
+
|
150
|
+
owners_by_key[convert_key(owner_key)].each do |owner|
|
151
|
+
entries = (@records_by_owner[owner] ||= [])
|
152
|
+
|
153
|
+
if reflection.collection? || entries.empty?
|
154
|
+
entries << record
|
155
|
+
assignments = true
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
assignments
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# significant change: globalize keys on sharded columns
|
164
|
+
def owners_by_key
|
165
|
+
@owners_by_key ||= owners.each_with_object({}) do |owner, result|
|
166
|
+
key = owner[owner_key_name]
|
167
|
+
key = Shard.global_id_for(key, owner.shard) if key && owner.class.sharded_column?(owner_key_name)
|
168
|
+
key = convert_key(key)
|
169
|
+
(result[key] ||= []) << owner if key
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# significant change: don't cache scope (since it could be for different shards)
|
174
|
+
def scope
|
175
|
+
build_scope
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
module CollectionProxy
|
181
|
+
def initialize(*args)
|
182
|
+
super
|
183
|
+
self.shard_value = scope.shard_value
|
184
|
+
self.shard_source_value = :association
|
185
|
+
end
|
186
|
+
|
187
|
+
def shard(*args)
|
188
|
+
scope.shard(*args)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
module AutosaveAssociation
|
193
|
+
def record_changed?(reflection, record, key)
|
194
|
+
record.new_record? ||
|
195
|
+
(record.has_attribute?(reflection.foreign_key) && record.send(reflection.foreign_key) != key) || # have to use send instead of [] because sharding
|
196
|
+
record.attribute_changed?(reflection.foreign_key)
|
197
|
+
end
|
198
|
+
|
199
|
+
def save_belongs_to_association(reflection)
|
200
|
+
# this seems counter-intuitive, but the autosave code will assign to attribute bypassing switchman,
|
201
|
+
# after reading the id attribute _without_ bypassing switchman. So we need Shard.current for the
|
202
|
+
# category of the associated record to match Shard.current for the category of self
|
203
|
+
shard.activate(connection_classes_for_reflection(reflection)) { super }
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'switchman/database_server'
|
4
|
-
|
5
3
|
module Switchman
|
6
4
|
module ActiveRecord
|
7
5
|
module Base
|
@@ -16,8 +14,6 @@ module Switchman
|
|
16
14
|
def sharded_model
|
17
15
|
self.abstract_class = true
|
18
16
|
|
19
|
-
return if self == UnshardedRecord
|
20
|
-
|
21
17
|
Shard.send(:add_sharded_model, self)
|
22
18
|
end
|
23
19
|
|
@@ -62,8 +58,23 @@ module Switchman
|
|
62
58
|
end
|
63
59
|
end
|
64
60
|
|
65
|
-
def
|
66
|
-
current_role != current_role(without_overrides: true)
|
61
|
+
def role_overriden?(shard_id)
|
62
|
+
current_role(target_shard: shard_id) != current_role(without_overrides: true)
|
63
|
+
end
|
64
|
+
|
65
|
+
def establish_connection(config_or_env = nil)
|
66
|
+
raise ArgumentError, 'establish connection cannot be used on the non-current shard/role' if config_or_env.is_a?(Symbol) && config_or_env != ::Rails.env.to_sym
|
67
|
+
|
68
|
+
# Ensure we don't randomly surprise change the connection parms associated with a shard/role
|
69
|
+
config_or_env = nil if config_or_env == ::Rails.env.to_sym
|
70
|
+
|
71
|
+
config_or_env ||= if current_shard == ::Rails.env.to_sym && current_role == :primary
|
72
|
+
:primary
|
73
|
+
else
|
74
|
+
"#{current_shard}/#{current_role}".to_sym
|
75
|
+
end
|
76
|
+
|
77
|
+
super(config_or_env)
|
67
78
|
end
|
68
79
|
|
69
80
|
def connected_to_stack
|
@@ -75,12 +86,12 @@ module Switchman
|
|
75
86
|
end
|
76
87
|
|
77
88
|
# significant change: Allow per-shard roles
|
78
|
-
def current_role(without_overrides: false)
|
89
|
+
def current_role(without_overrides: false, target_shard: current_shard)
|
79
90
|
return super() if without_overrides
|
80
91
|
|
81
92
|
sharded_role = nil
|
82
93
|
connected_to_stack.reverse_each do |hash|
|
83
|
-
shard_role = hash.dig(:shard_roles,
|
94
|
+
shard_role = hash.dig(:shard_roles, target_shard)
|
84
95
|
if shard_role && (hash[:klasses].include?(::ActiveRecord::Base) || hash[:klasses].include?(connection_classes))
|
85
96
|
sharded_role = shard_role
|
86
97
|
break
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'switchman/errors'
|
4
|
-
|
5
3
|
module Switchman
|
6
4
|
module ActiveRecord
|
7
5
|
module ConnectionPool
|
@@ -20,7 +18,7 @@ module Switchman
|
|
20
18
|
|
21
19
|
def connection(switch_shard: true)
|
22
20
|
conn = super()
|
23
|
-
raise NonExistentShardError if current_shard.new_record?
|
21
|
+
raise Errors::NonExistentShardError if current_shard.new_record?
|
24
22
|
|
25
23
|
switch_database(conn) if conn.shard != current_shard && switch_shard
|
26
24
|
conn
|
@@ -4,6 +4,22 @@ module Switchman
|
|
4
4
|
module ActiveSupport
|
5
5
|
module Cache
|
6
6
|
module ClassMethods
|
7
|
+
def lookup_stores(cache_store_config)
|
8
|
+
result = {}
|
9
|
+
cache_store_config.each do |key, value|
|
10
|
+
next if value.is_a?(String)
|
11
|
+
|
12
|
+
result[key] = ::ActiveSupport::Cache.lookup_store(value)
|
13
|
+
end
|
14
|
+
|
15
|
+
cache_store_config.each do |key, value| # rubocop:disable Style/CombinableLoops
|
16
|
+
next unless value.is_a?(String)
|
17
|
+
|
18
|
+
result[key] = result[value]
|
19
|
+
end
|
20
|
+
result
|
21
|
+
end
|
22
|
+
|
7
23
|
def lookup_store(*store_options)
|
8
24
|
store = super
|
9
25
|
# can't use defined?, because it's a _ruby_ autoloaded constant,
|
@@ -8,15 +8,12 @@ module Switchman
|
|
8
8
|
|
9
9
|
class << self
|
10
10
|
attr_accessor :creating_new_shard
|
11
|
+
attr_reader :all_roles
|
11
12
|
|
12
13
|
def all
|
13
14
|
database_servers.values
|
14
15
|
end
|
15
16
|
|
16
|
-
def all_roles
|
17
|
-
@all_roles ||= all.map(&:roles).flatten.uniq
|
18
|
-
end
|
19
|
-
|
20
17
|
def find(id_or_all)
|
21
18
|
return all if id_or_all == :all
|
22
19
|
return id_or_all.map { |id| database_servers[id || ::Rails.env] }.compact.uniq if id_or_all.is_a?(Array)
|
@@ -38,7 +35,7 @@ module Switchman
|
|
38
35
|
database_servers[server.id] = server
|
39
36
|
::ActiveRecord::Base.configurations.configurations <<
|
40
37
|
::ActiveRecord::DatabaseConfigurations::HashConfig.new(::Rails.env, "#{server.id}/primary", settings)
|
41
|
-
Shard.send(:
|
38
|
+
Shard.send(:configure_connects_to)
|
42
39
|
server
|
43
40
|
end
|
44
41
|
|
@@ -59,25 +56,32 @@ module Switchman
|
|
59
56
|
return if all_roles.include?(role)
|
60
57
|
|
61
58
|
@all_roles << role
|
62
|
-
Shard.send(:
|
59
|
+
Shard.send(:configure_connects_to)
|
63
60
|
end
|
64
61
|
|
65
62
|
def database_servers
|
66
63
|
if !@database_servers || @database_servers.empty?
|
67
64
|
@database_servers = {}.with_indifferent_access
|
65
|
+
roles = []
|
68
66
|
::ActiveRecord::Base.configurations.configurations.each do |config|
|
69
67
|
if config.name.include?('/')
|
70
68
|
name, role = config.name.split('/')
|
71
69
|
else
|
72
70
|
name, role = config.env_name, config.name
|
73
71
|
end
|
72
|
+
role = role.to_sym
|
74
73
|
|
75
|
-
|
74
|
+
roles << role
|
75
|
+
if role == :primary
|
76
76
|
@database_servers[name] = DatabaseServer.new(config.env_name, config.configuration_hash)
|
77
77
|
else
|
78
|
-
@database_servers[name].roles << role
|
78
|
+
@database_servers[name].roles << role
|
79
79
|
end
|
80
80
|
end
|
81
|
+
# Do this after so that all database servers for all roles are established and we won't prematurely
|
82
|
+
# configure a connection for the wrong role
|
83
|
+
@all_roles = roles.uniq
|
84
|
+
Shard.send(:configure_connects_to)
|
81
85
|
end
|
82
86
|
@database_servers
|
83
87
|
end
|
@@ -146,7 +150,7 @@ module Switchman
|
|
146
150
|
end
|
147
151
|
|
148
152
|
def unguard
|
149
|
-
return yield unless ::ActiveRecord::Base.
|
153
|
+
return yield unless ::ActiveRecord::Base.role_overriden?(id.to_sym)
|
150
154
|
|
151
155
|
begin
|
152
156
|
unguard!
|
data/lib/switchman/engine.rb
CHANGED
@@ -8,101 +8,18 @@ module Switchman
|
|
8
8
|
config.active_record.legacy_connection_handling = false
|
9
9
|
config.active_record.writing_role = :primary
|
10
10
|
|
11
|
-
|
11
|
+
::GuardRail.singleton_class.prepend(GuardRail::ClassMethods)
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
# after :initialize_dependency_mechanism to ensure autoloading is configured for any downstream initializers that care
|
14
|
+
# In rails 7.0 we should be able to just use an explicit after on configuring the once autoloaders and not need to go monkey around with initializer order
|
15
|
+
initialize_dependency_mechanism = ::Rails::Application::Bootstrap.initializers.find { |i| i.name == :initialize_dependency_mechanism }
|
16
|
+
initialize_dependency_mechanism.instance_variable_get(:@options)[:after] = :set_autoload_paths
|
17
17
|
|
18
|
-
|
19
|
-
end
|
20
|
-
|
21
|
-
cache_store_config.each do |key, value| # rubocop:disable Style/CombinableLoops
|
22
|
-
next unless value.is_a?(String)
|
23
|
-
|
24
|
-
result[key] = result[value]
|
25
|
-
end
|
26
|
-
result
|
27
|
-
end
|
28
|
-
|
29
|
-
initializer 'switchman.initialize_cache', before: 'initialize_cache' do
|
30
|
-
require 'switchman/active_support/cache'
|
31
|
-
::ActiveSupport::Cache.singleton_class.prepend(ActiveSupport::Cache::ClassMethods)
|
32
|
-
|
33
|
-
# if we haven't already setup our cache map out-of-band, set it up from
|
34
|
-
# config.cache_store now. behaves similarly to Rails' default
|
35
|
-
# initialize_cache initializer, but for each value in the map, rather
|
36
|
-
# than just Rails.cache. if config.cache_store is a flat value, uses it
|
37
|
-
# to fill just the Rails.env entry in the cache map.
|
38
|
-
unless Switchman.config[:cache_map].present?
|
39
|
-
cache_store_config = ::Rails.configuration.cache_store
|
40
|
-
cache_store_config = { ::Rails.env => cache_store_config } unless cache_store_config.is_a?(Hash)
|
41
|
-
|
42
|
-
Switchman.config[:cache_map] = Engine.lookup_stores(cache_store_config)
|
43
|
-
end
|
44
|
-
|
45
|
-
# if the configured cache map (either from before, or as populated from
|
46
|
-
# config.cache_store) didn't have an entry for Rails.env, add one using
|
47
|
-
# lookup_store(nil); matches the behavior of Rails' default
|
48
|
-
# initialize_cache initializer when config.cache_store is nil.
|
49
|
-
unless Switchman.config[:cache_map].key?(::Rails.env)
|
50
|
-
value = ::ActiveSupport::Cache.lookup_store(nil)
|
51
|
-
Switchman.config[:cache_map][::Rails.env] = value
|
52
|
-
end
|
53
|
-
|
54
|
-
middlewares = Switchman.config[:cache_map].values.map do |store|
|
55
|
-
store.middleware if store.respond_to?(:middleware)
|
56
|
-
end.compact.uniq
|
57
|
-
middlewares.each do |middleware|
|
58
|
-
config.middleware.insert_before('Rack::Runtime', middleware)
|
59
|
-
end
|
60
|
-
|
61
|
-
# prevent :initialize_cache from trying to (or needing to) set
|
62
|
-
# Rails.cache. once our switchman.extend_ar initializer (below) runs
|
63
|
-
# Rails.cache will be overridden to pull appropriate values from the
|
64
|
-
# cache map, but between now and then, Rails.cache should return the
|
65
|
-
# Rails.env entry in the cache map.
|
66
|
-
::Rails.cache = Switchman.config[:cache_map][::Rails.env]
|
67
|
-
end
|
68
|
-
|
69
|
-
initializer 'switchman.extend_ar', before: 'active_record.initialize_database' do
|
18
|
+
initializer 'switchman.active_record_patch', before: 'active_record.initialize_database', after: :initialize_dependency_mechanism do
|
70
19
|
::ActiveSupport.on_load(:active_record) do
|
71
20
|
# Switchman requires postgres, so just always load the pg adapter
|
72
21
|
require 'active_record/connection_adapters/postgresql_adapter'
|
73
22
|
|
74
|
-
require 'switchman/active_record/abstract_adapter'
|
75
|
-
require 'switchman/active_record/association'
|
76
|
-
require 'switchman/active_record/attribute_methods'
|
77
|
-
require 'switchman/active_record/base'
|
78
|
-
require 'switchman/active_record/calculations'
|
79
|
-
require 'switchman/active_record/connection_pool'
|
80
|
-
require 'switchman/active_record/database_configurations'
|
81
|
-
require 'switchman/active_record/database_configurations/database_config'
|
82
|
-
require 'switchman/active_record/finder_methods'
|
83
|
-
require 'switchman/active_record/log_subscriber'
|
84
|
-
require 'switchman/active_record/migration'
|
85
|
-
require 'switchman/active_record/model_schema'
|
86
|
-
require 'switchman/active_record/persistence'
|
87
|
-
require 'switchman/active_record/postgresql_adapter'
|
88
|
-
require 'switchman/active_record/predicate_builder'
|
89
|
-
require 'switchman/active_record/query_cache'
|
90
|
-
require 'switchman/active_record/query_methods'
|
91
|
-
require 'switchman/active_record/reflection'
|
92
|
-
require 'switchman/active_record/relation'
|
93
|
-
require 'switchman/active_record/spawn_methods'
|
94
|
-
require 'switchman/active_record/statement_cache'
|
95
|
-
require 'switchman/active_record/tasks/database_tasks'
|
96
|
-
require 'switchman/active_record/type_caster'
|
97
|
-
require 'switchman/active_record/test_fixtures'
|
98
|
-
require 'switchman/arel'
|
99
|
-
require 'switchman/call_super'
|
100
|
-
require 'switchman/rails'
|
101
|
-
require 'switchman/guard_rail/relation'
|
102
|
-
require 'switchman/standard_error'
|
103
|
-
|
104
|
-
::StandardError.include(StandardError)
|
105
|
-
|
106
23
|
self.default_shard = ::Rails.env.to_sym
|
107
24
|
self.default_role = :primary
|
108
25
|
|
@@ -116,19 +33,19 @@ module Switchman
|
|
116
33
|
::ActiveRecord::StatementCache::BindMap.prepend(ActiveRecord::StatementCache::BindMap)
|
117
34
|
::ActiveRecord::StatementCache::Substitute.send(:attr_accessor, :primary, :sharded)
|
118
35
|
|
119
|
-
::ActiveRecord::Associations::CollectionAssociation.prepend(ActiveRecord::CollectionAssociation)
|
120
|
-
::ActiveRecord::Associations::HasOneAssociation.prepend(ActiveRecord::ForeignAssociation)
|
121
|
-
::ActiveRecord::Associations::HasManyAssociation.prepend(ActiveRecord::ForeignAssociation)
|
36
|
+
::ActiveRecord::Associations::CollectionAssociation.prepend(ActiveRecord::Associations::CollectionAssociation)
|
37
|
+
::ActiveRecord::Associations::HasOneAssociation.prepend(ActiveRecord::Associations::ForeignAssociation)
|
38
|
+
::ActiveRecord::Associations::HasManyAssociation.prepend(ActiveRecord::Associations::ForeignAssociation)
|
122
39
|
|
123
40
|
::ActiveRecord::PredicateBuilder.singleton_class.prepend(ActiveRecord::PredicateBuilder)
|
124
41
|
|
125
|
-
prepend(ActiveRecord::AutosaveAssociation)
|
42
|
+
prepend(ActiveRecord::Associations::AutosaveAssociation)
|
126
43
|
|
127
|
-
::ActiveRecord::Associations::Association.prepend(ActiveRecord::Association)
|
128
|
-
::ActiveRecord::Associations::BelongsToAssociation.prepend(ActiveRecord::BelongsToAssociation)
|
129
|
-
::ActiveRecord::Associations::CollectionProxy.include(ActiveRecord::CollectionProxy)
|
44
|
+
::ActiveRecord::Associations::Association.prepend(ActiveRecord::Associations::Association)
|
45
|
+
::ActiveRecord::Associations::BelongsToAssociation.prepend(ActiveRecord::Associations::BelongsToAssociation)
|
46
|
+
::ActiveRecord::Associations::CollectionProxy.include(ActiveRecord::Associations::CollectionProxy)
|
130
47
|
|
131
|
-
::ActiveRecord::Associations::Preloader::Association.prepend(ActiveRecord::Preloader::Association)
|
48
|
+
::ActiveRecord::Associations::Preloader::Association.prepend(ActiveRecord::Associations::Preloader::Association)
|
132
49
|
::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(ActiveRecord::AbstractAdapter)
|
133
50
|
::ActiveRecord::ConnectionAdapters::ConnectionPool.prepend(ActiveRecord::ConnectionPool)
|
134
51
|
::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(ActiveRecord::QueryCache)
|
@@ -165,49 +82,65 @@ module Switchman
|
|
165
82
|
::ActiveRecord::TypeCaster::Map.include(ActiveRecord::TypeCaster::Map)
|
166
83
|
::ActiveRecord::TypeCaster::Connection.include(ActiveRecord::TypeCaster::Connection)
|
167
84
|
|
168
|
-
::Rails.singleton_class.prepend(Rails::ClassMethods)
|
169
|
-
|
170
85
|
::Arel::Table.prepend(Arel::Table)
|
171
86
|
::Arel::Visitors::ToSql.prepend(Arel::Visitors::ToSql)
|
172
|
-
end
|
173
|
-
end
|
174
87
|
|
175
|
-
def self.foreign_key_check(name, type, limit: nil)
|
176
|
-
puts "WARNING: All foreign keys need to be 8-byte integers. #{name} looks like a foreign key. If so, please add the option: `:limit => 8`" if name.to_s =~ /_id\z/ && type.to_s == 'integer' && limit.to_i < 8
|
177
|
-
end
|
178
|
-
|
179
|
-
initializer 'switchman.extend_connection_adapters', after: 'active_record.initialize_database' do
|
180
|
-
::ActiveSupport.on_load(:active_record) do
|
181
88
|
::ActiveRecord::ConnectionAdapters::AbstractAdapter.descendants.each do |klass|
|
182
89
|
klass.prepend(ActiveRecord::AbstractAdapter::ForeignKeyCheck)
|
183
90
|
end
|
184
91
|
|
185
|
-
require 'switchman/active_record/table_definition'
|
186
92
|
::ActiveRecord::ConnectionAdapters::TableDefinition.prepend(ActiveRecord::TableDefinition)
|
187
|
-
|
188
|
-
Shard.send(:initialize_sharding)
|
189
93
|
end
|
94
|
+
# Ensure that ActiveRecord::Base is always loaded before any app-level initializers can go try to load Switchman::Shard or we get a loop
|
95
|
+
::ActiveRecord::Base
|
190
96
|
end
|
191
97
|
|
192
|
-
initializer 'switchman.
|
193
|
-
::ActiveSupport.on_load(:
|
194
|
-
|
195
|
-
require 'active_record/base'
|
98
|
+
initializer 'switchman.error_patch', after: 'active_record.initialize_database' do
|
99
|
+
::ActiveSupport.on_load(:active_record) do
|
100
|
+
::StandardError.include(StandardError)
|
196
101
|
end
|
197
102
|
end
|
198
103
|
|
199
|
-
initializer 'switchman.
|
200
|
-
::ActiveSupport.
|
201
|
-
require 'switchman/guard_rail'
|
104
|
+
initializer 'switchman.initialize_cache', before: :initialize_cache, after: 'active_record.initialize_database' do
|
105
|
+
::ActiveSupport::Cache.singleton_class.prepend(ActiveSupport::Cache::ClassMethods)
|
202
106
|
|
203
|
-
|
107
|
+
# if we haven't already setup our cache map out-of-band, set it up from
|
108
|
+
# config.cache_store now. behaves similarly to Rails' default
|
109
|
+
# initialize_cache initializer, but for each value in the map, rather
|
110
|
+
# than just Rails.cache. if config.cache_store is a flat value, uses it
|
111
|
+
# to fill just the Rails.env entry in the cache map.
|
112
|
+
unless Switchman.config[:cache_map].present?
|
113
|
+
cache_store_config = ::Rails.configuration.cache_store
|
114
|
+
cache_store_config = { ::Rails.env => cache_store_config } unless cache_store_config.is_a?(Hash)
|
115
|
+
|
116
|
+
Switchman.config[:cache_map] = ::ActiveSupport::Cache.lookup_stores(cache_store_config)
|
204
117
|
end
|
205
|
-
end
|
206
118
|
|
207
|
-
|
208
|
-
|
209
|
-
|
119
|
+
# if the configured cache map (either from before, or as populated from
|
120
|
+
# config.cache_store) didn't have an entry for Rails.env, add one using
|
121
|
+
# lookup_store(nil); matches the behavior of Rails' default
|
122
|
+
# initialize_cache initializer when config.cache_store is nil.
|
123
|
+
unless Switchman.config[:cache_map].key?(::Rails.env)
|
124
|
+
value = ::ActiveSupport::Cache.lookup_store(nil)
|
125
|
+
Switchman.config[:cache_map][::Rails.env] = value
|
126
|
+
end
|
127
|
+
|
128
|
+
middlewares = Switchman.config[:cache_map].values.map do |store|
|
129
|
+
store.middleware if store.respond_to?(:middleware)
|
130
|
+
end.compact.uniq
|
131
|
+
middlewares.each do |middleware|
|
132
|
+
config.middleware.insert_before('Rack::Runtime', middleware)
|
133
|
+
end
|
210
134
|
|
135
|
+
# prevent :initialize_cache from trying to (or needing to) set
|
136
|
+
# Rails.cache. once our switchman.extend_ar initializer (below) runs
|
137
|
+
# Rails.cache will be overridden to pull appropriate values from the
|
138
|
+
# cache map, but between now and then, Rails.cache should return the
|
139
|
+
# Rails.env entry in the cache map.
|
140
|
+
::Rails.cache = Switchman.config[:cache_map][::Rails.env]
|
141
|
+
::Rails.singleton_class.prepend(Rails::ClassMethods)
|
142
|
+
|
143
|
+
::ActiveSupport.on_load(:action_controller) do
|
211
144
|
::ActionController::Base.include(ActionController::Caching)
|
212
145
|
end
|
213
146
|
end
|
data/lib/switchman/errors.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Switchman
|
4
|
-
|
4
|
+
module Errors
|
5
|
+
class NonExistentShardError < RuntimeError; end
|
5
6
|
|
6
|
-
|
7
|
+
class ParallelShardExecError < RuntimeError; end
|
8
|
+
end
|
7
9
|
end
|
data/lib/switchman/rails.rb
CHANGED
@@ -4,16 +4,13 @@ module Switchman
|
|
4
4
|
module Rails
|
5
5
|
module ClassMethods
|
6
6
|
def self.prepended(klass)
|
7
|
-
#
|
8
|
-
# Rails.cache(_without_sharding) to the value from the config file. but now
|
9
|
-
# that that's done (the bootstrap happened before this module is included
|
10
|
-
# into Rails), we want to make sure no one tries to assign to Rails.cache,
|
7
|
+
# we want to make sure no one tries to assign to Rails.cache,
|
11
8
|
# because it would be wrong w.r.t. sharding.
|
12
9
|
klass.send(:remove_method, :cache=)
|
13
10
|
end
|
14
11
|
|
15
12
|
def cache
|
16
|
-
Switchman::Shard.current
|
13
|
+
Switchman::Shard.current.database_server.cache_store
|
17
14
|
end
|
18
15
|
end
|
19
16
|
end
|
@@ -1,10 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'switchman/database_server'
|
4
|
-
require 'switchman/default_shard'
|
5
|
-
require 'switchman/environment'
|
6
|
-
require 'switchman/errors'
|
7
|
-
|
8
3
|
module Switchman
|
9
4
|
class Shard < UnshardedRecord
|
10
5
|
# ten trillion possible ids per shard. yup.
|
@@ -42,12 +37,9 @@ module Switchman
|
|
42
37
|
# Now find the actual record, if it exists
|
43
38
|
@default = begin
|
44
39
|
find_cached('default_shard') { Shard.where(default: true).take } || default
|
45
|
-
# If we are *super* early in boot, the connection pool won't exist; we don't want to fill in the default shard yet
|
46
|
-
# Otherwise, rescue the fake default if the table doesn't exist
|
47
40
|
rescue
|
48
|
-
|
41
|
+
default
|
49
42
|
end
|
50
|
-
return default unless @default
|
51
43
|
|
52
44
|
# make sure this is not erroneously cached
|
53
45
|
@default.database_server.remove_instance_variable(:@primary_shard) if @default.database_server.instance_variable_defined?(:@primary_shard)
|
@@ -193,7 +185,7 @@ module Switchman
|
|
193
185
|
unless errors.empty?
|
194
186
|
raise errors.first.exception if errors.length == 1
|
195
187
|
|
196
|
-
raise ParallelShardExecError,
|
188
|
+
raise Errors::ParallelShardExecError,
|
197
189
|
"The following database server(s) did not finish processing cleanly: #{errors.map(&:name).sort.join(', ')}",
|
198
190
|
cause: errors.first.exception
|
199
191
|
end
|
@@ -374,18 +366,14 @@ module Switchman
|
|
374
366
|
shard || source_shard || Shard.current
|
375
367
|
end
|
376
368
|
|
377
|
-
def sharding_initialized
|
378
|
-
@sharding_initialized ||= false
|
379
|
-
end
|
380
|
-
|
381
369
|
private
|
382
370
|
|
383
371
|
def add_sharded_model(klass)
|
384
372
|
@sharded_models = (sharded_models + [klass]).freeze
|
385
|
-
|
373
|
+
configure_connects_to
|
386
374
|
end
|
387
375
|
|
388
|
-
def
|
376
|
+
def configure_connects_to
|
389
377
|
full_connects_to_hash = DatabaseServer.all.to_h { |db| [db.id.to_sym, db.connects_to_hash] }
|
390
378
|
sharded_models.each do |klass|
|
391
379
|
connects_to_hash = full_connects_to_hash.deep_dup
|
@@ -407,13 +395,6 @@ module Switchman
|
|
407
395
|
|
408
396
|
klass.connects_to shards: connects_to_hash
|
409
397
|
end
|
410
|
-
|
411
|
-
return if @sharding_initialized
|
412
|
-
|
413
|
-
# If we hadn't initialized sharding yet, the servers won't be guarded
|
414
|
-
# The order matters here or guard_servers will be a noop
|
415
|
-
@sharding_initialized = true
|
416
|
-
DatabaseServer.guard_servers
|
417
398
|
end
|
418
399
|
|
419
400
|
# in-process caching
|
@@ -12,11 +12,7 @@ module Switchman
|
|
12
12
|
begin
|
13
13
|
Thread.current[:switchman_error_handler] = true
|
14
14
|
|
15
|
-
|
16
|
-
@active_shards ||= Shard.active_shards if defined?(Shard)
|
17
|
-
rescue
|
18
|
-
# If we hit an error really early in boot, activerecord may not be initialized yet
|
19
|
-
end
|
15
|
+
@active_shards ||= Shard.active_shards
|
20
16
|
ensure
|
21
17
|
Thread.current[:switchman_error_handler] = nil
|
22
18
|
end
|
data/lib/switchman/version.rb
CHANGED
data/lib/switchman.rb
CHANGED
@@ -1,8 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'guard_rail'
|
4
|
-
require '
|
5
|
-
|
4
|
+
require 'zeitwerk'
|
5
|
+
|
6
|
+
class SwitchmanInflector < Zeitwerk::GemInflector
|
7
|
+
def camelize(basename, abspath)
|
8
|
+
if basename =~ /\Apostgresql_(.*)/
|
9
|
+
'PostgreSQL' + super($1, abspath)
|
10
|
+
else
|
11
|
+
super
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
loader = Zeitwerk::Loader.for_gem
|
17
|
+
loader.inflector = SwitchmanInflector.new(__FILE__)
|
18
|
+
loader.setup
|
6
19
|
|
7
20
|
module Switchman
|
8
21
|
def self.config
|
@@ -18,5 +31,12 @@ module Switchman
|
|
18
31
|
@cache = cache
|
19
32
|
end
|
20
33
|
|
34
|
+
def self.foreign_key_check(name, type, limit: nil)
|
35
|
+
puts "WARNING: All foreign keys need to be 8-byte integers. #{name} looks like a foreign key. If so, please add the option: `:limit => 8`" if name.to_s =~ /_id\z/ && type.to_s == 'integer' && limit.to_i < 8
|
36
|
+
end
|
37
|
+
|
21
38
|
class OrderOnMultiShardQuery < RuntimeError; end
|
22
39
|
end
|
40
|
+
|
41
|
+
# Load the engine and everything associated at gem load time
|
42
|
+
Switchman::Engine
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: switchman
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.0.
|
4
|
+
version: 3.0.24
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cody Cutrer
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2022-04-
|
13
|
+
date: 2022-04-28 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activerecord
|
@@ -242,8 +242,6 @@ extensions: []
|
|
242
242
|
extra_rdoc_files: []
|
243
243
|
files:
|
244
244
|
- Rakefile
|
245
|
-
- app/models/switchman/shard.rb
|
246
|
-
- app/models/switchman/unsharded_record.rb
|
247
245
|
- db/migrate/20130328212039_create_switchman_shards.rb
|
248
246
|
- db/migrate/20130328224244_create_default_shard.rb
|
249
247
|
- db/migrate/20161206323434_add_back_default_string_limits_switchman.rb
|
@@ -253,7 +251,7 @@ files:
|
|
253
251
|
- lib/switchman.rb
|
254
252
|
- lib/switchman/action_controller/caching.rb
|
255
253
|
- lib/switchman/active_record/abstract_adapter.rb
|
256
|
-
- lib/switchman/active_record/
|
254
|
+
- lib/switchman/active_record/associations.rb
|
257
255
|
- lib/switchman/active_record/attribute_methods.rb
|
258
256
|
- lib/switchman/active_record/base.rb
|
259
257
|
- lib/switchman/active_record/calculations.rb
|
@@ -290,9 +288,11 @@ files:
|
|
290
288
|
- lib/switchman/parallel.rb
|
291
289
|
- lib/switchman/r_spec_helper.rb
|
292
290
|
- lib/switchman/rails.rb
|
291
|
+
- lib/switchman/shard.rb
|
293
292
|
- lib/switchman/sharded_instrumenter.rb
|
294
293
|
- lib/switchman/standard_error.rb
|
295
294
|
- lib/switchman/test_helper.rb
|
295
|
+
- lib/switchman/unsharded_record.rb
|
296
296
|
- lib/switchman/version.rb
|
297
297
|
- lib/tasks/switchman.rake
|
298
298
|
homepage: http://www.instructure.com/
|
@@ -1,206 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Switchman
|
4
|
-
module ActiveRecord
|
5
|
-
module Association
|
6
|
-
def shard
|
7
|
-
reflection.shard(owner)
|
8
|
-
end
|
9
|
-
|
10
|
-
def build_record(*args)
|
11
|
-
shard.activate { super }
|
12
|
-
end
|
13
|
-
|
14
|
-
def load_target
|
15
|
-
shard.activate { super }
|
16
|
-
end
|
17
|
-
|
18
|
-
def scope
|
19
|
-
shard_value = @reflection.options[:multishard] ? @owner : shard
|
20
|
-
@owner.shard.activate { super.shard(shard_value, :association) }
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
module CollectionAssociation
|
25
|
-
def find_target
|
26
|
-
shards = reflection.options[:multishard] && owner.respond_to?(:associated_shards) ? owner.associated_shards : [shard]
|
27
|
-
# activate both the owner and the target's shard category, so that Reflection#join_id_for,
|
28
|
-
# when called for the owner, will be returned relative to shard the query will execute on
|
29
|
-
Shard.with_each_shard(shards, [klass.connection_classes, owner.class.connection_classes].uniq) do
|
30
|
-
super
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
def _create_record(*)
|
35
|
-
shard.activate { super }
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
module BelongsToAssociation
|
40
|
-
def replace_keys(record, force: false)
|
41
|
-
if record&.class&.sharded_column?(reflection.association_primary_key(record.class))
|
42
|
-
foreign_id = record[reflection.association_primary_key(record.class)]
|
43
|
-
owner[reflection.foreign_key] = Shard.relative_id_for(foreign_id, record.shard, owner.shard)
|
44
|
-
else
|
45
|
-
super
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
def shard
|
50
|
-
if @owner.class.sharded_column?(@reflection.foreign_key) &&
|
51
|
-
(foreign_id = @owner[@reflection.foreign_key])
|
52
|
-
Shard.shard_for(foreign_id, @owner.shard)
|
53
|
-
else
|
54
|
-
super
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
module ForeignAssociation
|
60
|
-
# significant change:
|
61
|
-
# * transpose the key to the correct shard
|
62
|
-
def set_owner_attributes(record) # rubocop:disable Naming/AccessorMethodName
|
63
|
-
return if options[:through]
|
64
|
-
|
65
|
-
key = owner._read_attribute(reflection.join_foreign_key)
|
66
|
-
key = Shard.relative_id_for(key, owner.shard, shard)
|
67
|
-
record._write_attribute(reflection.join_primary_key, key)
|
68
|
-
|
69
|
-
record._write_attribute(reflection.type, owner.class.polymorphic_name) if reflection.type
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
module Extension
|
74
|
-
def self.build(_model, _reflection); end
|
75
|
-
|
76
|
-
def self.valid_options
|
77
|
-
[:multishard]
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
::ActiveRecord::Associations::Builder::Association.extensions << Extension
|
82
|
-
|
83
|
-
module Preloader
|
84
|
-
module Association
|
85
|
-
# Copypasta from Activerecord but with added global_id_for goodness.
|
86
|
-
def records_for(ids)
|
87
|
-
scope.where(association_key_name => ids).load do |record|
|
88
|
-
global_key = if model.connection_classes == UnshardedRecord
|
89
|
-
convert_key(record[association_key_name])
|
90
|
-
else
|
91
|
-
Shard.global_id_for(record[association_key_name], record.shard)
|
92
|
-
end
|
93
|
-
owner = owners_by_key[convert_key(global_key)].first
|
94
|
-
association = owner.association(reflection.name)
|
95
|
-
association.set_inverse_instance(record)
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
# significant changes:
|
100
|
-
# * partition_by_shard the records_for call
|
101
|
-
# * re-globalize the fetched owner id before looking up in the map
|
102
|
-
def load_records
|
103
|
-
# owners can be duplicated when a relation has a collection association join
|
104
|
-
# #compare_by_identity makes such owners different hash keys
|
105
|
-
@records_by_owner = {}.compare_by_identity
|
106
|
-
|
107
|
-
if owner_keys.empty?
|
108
|
-
raw_records = []
|
109
|
-
else
|
110
|
-
# determine the shard to search for each owner
|
111
|
-
if reflection.macro == :belongs_to
|
112
|
-
# for belongs_to, it's the shard of the foreign_key
|
113
|
-
partition_proc = lambda do |owner|
|
114
|
-
if owner.class.sharded_column?(owner_key_name)
|
115
|
-
Shard.shard_for(owner[owner_key_name], owner.shard)
|
116
|
-
else
|
117
|
-
Shard.current
|
118
|
-
end
|
119
|
-
end
|
120
|
-
elsif !reflection.options[:multishard]
|
121
|
-
# for non-multishard associations, it's *just* the owner's shard
|
122
|
-
partition_proc = ->(owner) { owner.shard }
|
123
|
-
end
|
124
|
-
|
125
|
-
raw_records = Shard.partition_by_shard(owners, partition_proc) do |partitioned_owners|
|
126
|
-
relative_owner_keys = partitioned_owners.map do |owner|
|
127
|
-
key = owner[owner_key_name]
|
128
|
-
if key && owner.class.sharded_column?(owner_key_name)
|
129
|
-
key = Shard.relative_id_for(key, owner.shard,
|
130
|
-
Shard.current(klass.connection_classes))
|
131
|
-
end
|
132
|
-
convert_key(key)
|
133
|
-
end
|
134
|
-
relative_owner_keys.compact!
|
135
|
-
relative_owner_keys.uniq!
|
136
|
-
records_for(relative_owner_keys)
|
137
|
-
end
|
138
|
-
end
|
139
|
-
|
140
|
-
@preloaded_records = raw_records.select do |record|
|
141
|
-
assignments = false
|
142
|
-
|
143
|
-
owner_key = record[association_key_name]
|
144
|
-
if owner_key && record.class.sharded_column?(association_key_name)
|
145
|
-
owner_key = Shard.global_id_for(owner_key,
|
146
|
-
record.shard)
|
147
|
-
end
|
148
|
-
|
149
|
-
owners_by_key[convert_key(owner_key)].each do |owner|
|
150
|
-
entries = (@records_by_owner[owner] ||= [])
|
151
|
-
|
152
|
-
if reflection.collection? || entries.empty?
|
153
|
-
entries << record
|
154
|
-
assignments = true
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
assignments
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
# significant change: globalize keys on sharded columns
|
163
|
-
def owners_by_key
|
164
|
-
@owners_by_key ||= owners.each_with_object({}) do |owner, result|
|
165
|
-
key = owner[owner_key_name]
|
166
|
-
key = Shard.global_id_for(key, owner.shard) if key && owner.class.sharded_column?(owner_key_name)
|
167
|
-
key = convert_key(key)
|
168
|
-
(result[key] ||= []) << owner if key
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
|
-
# significant change: don't cache scope (since it could be for different shards)
|
173
|
-
def scope
|
174
|
-
build_scope
|
175
|
-
end
|
176
|
-
end
|
177
|
-
end
|
178
|
-
|
179
|
-
module CollectionProxy
|
180
|
-
def initialize(*args)
|
181
|
-
super
|
182
|
-
self.shard_value = scope.shard_value
|
183
|
-
self.shard_source_value = :association
|
184
|
-
end
|
185
|
-
|
186
|
-
def shard(*args)
|
187
|
-
scope.shard(*args)
|
188
|
-
end
|
189
|
-
end
|
190
|
-
|
191
|
-
module AutosaveAssociation
|
192
|
-
def record_changed?(reflection, record, key)
|
193
|
-
record.new_record? ||
|
194
|
-
(record.has_attribute?(reflection.foreign_key) && record.send(reflection.foreign_key) != key) || # have to use send instead of [] because sharding
|
195
|
-
record.attribute_changed?(reflection.foreign_key)
|
196
|
-
end
|
197
|
-
|
198
|
-
def save_belongs_to_association(reflection)
|
199
|
-
# this seems counter-intuitive, but the autosave code will assign to attribute bypassing switchman,
|
200
|
-
# after reading the id attribute _without_ bypassing switchman. So we need Shard.current for the
|
201
|
-
# category of the associated record to match Shard.current for the category of self
|
202
|
-
shard.activate(connection_classes_for_reflection(reflection)) { super }
|
203
|
-
end
|
204
|
-
end
|
205
|
-
end
|
206
|
-
end
|