switchman 3.0.19 → 3.0.22
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- 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 +23 -2
- 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 +17 -8
- data/lib/switchman/default_shard.rb +0 -2
- data/lib/switchman/engine.rb +51 -123
- data/lib/switchman/errors.rb +4 -2
- data/lib/switchman/rails.rb +2 -5
- data/{app/models → lib}/switchman/shard.rb +4 -19
- 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: f6ed57a128cb7da74d0df280b924f4369dcd224039343f4476796ae09d8573f5
|
4
|
+
data.tar.gz: 732fbee6c445f210f9b97b3921f30356a2eb7f9df1c925631dccce5e91612904
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8fdbbdc4309ebac36a0c779aefe89163f16419581752307223a367c3e47197d96a64daa8f8c4182b1b94512cf871891e531ea5c99616b0fc2e808c1b698d4f24
|
7
|
+
data.tar.gz: 93bd4a98e64cba28e28a80f4b33e8c93ef00ef3ba1c467b81c2ac93665ae0678acd2715d3781566c4e60df29749a353f08546d7aff9809fef2e53b5eb7f81d43
|
@@ -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
|
@@ -14,8 +14,6 @@ module Switchman
|
|
14
14
|
def sharded_model
|
15
15
|
self.abstract_class = true
|
16
16
|
|
17
|
-
return if self == UnshardedRecord
|
18
|
-
|
19
17
|
Shard.send(:add_sharded_model, self)
|
20
18
|
end
|
21
19
|
|
@@ -64,6 +62,29 @@ module Switchman
|
|
64
62
|
current_role != current_role(without_overrides: true)
|
65
63
|
end
|
66
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)
|
78
|
+
end
|
79
|
+
|
80
|
+
def connected_to_stack
|
81
|
+
return super if Thread.current.thread_variable?(:ar_connected_to_stack)
|
82
|
+
|
83
|
+
ret = super
|
84
|
+
DatabaseServer.guard_servers
|
85
|
+
ret
|
86
|
+
end
|
87
|
+
|
67
88
|
# significant change: Allow per-shard roles
|
68
89
|
def current_role(without_overrides: false)
|
69
90
|
return super() if without_overrides
|
@@ -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
|
|
@@ -49,31 +46,42 @@ module Switchman
|
|
49
46
|
servers[rand(servers.length)]
|
50
47
|
end
|
51
48
|
|
49
|
+
def guard_servers
|
50
|
+
all.each { |db| db.guard! if db.config[:prefer_secondary] }
|
51
|
+
end
|
52
|
+
|
52
53
|
private
|
53
54
|
|
54
55
|
def reference_role(role)
|
55
56
|
return if all_roles.include?(role)
|
56
57
|
|
57
58
|
@all_roles << role
|
58
|
-
Shard.send(:
|
59
|
+
Shard.send(:configure_connects_to)
|
59
60
|
end
|
60
61
|
|
61
62
|
def database_servers
|
62
63
|
if !@database_servers || @database_servers.empty?
|
63
64
|
@database_servers = {}.with_indifferent_access
|
65
|
+
roles = []
|
64
66
|
::ActiveRecord::Base.configurations.configurations.each do |config|
|
65
67
|
if config.name.include?('/')
|
66
68
|
name, role = config.name.split('/')
|
67
69
|
else
|
68
70
|
name, role = config.env_name, config.name
|
69
71
|
end
|
72
|
+
role = role.to_sym
|
70
73
|
|
71
|
-
|
74
|
+
roles << role
|
75
|
+
if role == :primary
|
72
76
|
@database_servers[name] = DatabaseServer.new(config.env_name, config.configuration_hash)
|
73
77
|
else
|
74
|
-
@database_servers[name].roles << role
|
78
|
+
@database_servers[name].roles << role
|
75
79
|
end
|
76
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)
|
77
85
|
end
|
78
86
|
@database_servers
|
79
87
|
end
|
@@ -133,6 +141,7 @@ module Switchman
|
|
133
141
|
# when doing writes (then it falls back to the current
|
134
142
|
# value of GuardRail.environment)
|
135
143
|
def guard!(environment = :secondary)
|
144
|
+
DatabaseServer.send(:reference_role, environment)
|
136
145
|
::ActiveRecord::Base.connected_to_stack << { shard_roles: { id.to_sym => environment }, klasses: [::ActiveRecord::Base] }
|
137
146
|
end
|
138
147
|
|
data/lib/switchman/engine.rb
CHANGED
@@ -8,101 +8,13 @@ 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
|
-
result = {}
|
15
|
-
cache_store_config.each do |key, value|
|
16
|
-
next if value.is_a?(String)
|
17
|
-
|
18
|
-
result[key] = ::ActiveSupport::Cache.lookup_store(value)
|
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
|
13
|
+
initializer 'switchman.active_record_patch', before: 'active_record.initialize_database' do
|
70
14
|
::ActiveSupport.on_load(:active_record) do
|
71
15
|
# Switchman requires postgres, so just always load the pg adapter
|
72
16
|
require 'active_record/connection_adapters/postgresql_adapter'
|
73
17
|
|
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
18
|
self.default_shard = ::Rails.env.to_sym
|
107
19
|
self.default_role = :primary
|
108
20
|
|
@@ -116,19 +28,19 @@ module Switchman
|
|
116
28
|
::ActiveRecord::StatementCache::BindMap.prepend(ActiveRecord::StatementCache::BindMap)
|
117
29
|
::ActiveRecord::StatementCache::Substitute.send(:attr_accessor, :primary, :sharded)
|
118
30
|
|
119
|
-
::ActiveRecord::Associations::CollectionAssociation.prepend(ActiveRecord::CollectionAssociation)
|
120
|
-
::ActiveRecord::Associations::HasOneAssociation.prepend(ActiveRecord::ForeignAssociation)
|
121
|
-
::ActiveRecord::Associations::HasManyAssociation.prepend(ActiveRecord::ForeignAssociation)
|
31
|
+
::ActiveRecord::Associations::CollectionAssociation.prepend(ActiveRecord::Associations::CollectionAssociation)
|
32
|
+
::ActiveRecord::Associations::HasOneAssociation.prepend(ActiveRecord::Associations::ForeignAssociation)
|
33
|
+
::ActiveRecord::Associations::HasManyAssociation.prepend(ActiveRecord::Associations::ForeignAssociation)
|
122
34
|
|
123
35
|
::ActiveRecord::PredicateBuilder.singleton_class.prepend(ActiveRecord::PredicateBuilder)
|
124
36
|
|
125
|
-
prepend(ActiveRecord::AutosaveAssociation)
|
37
|
+
prepend(ActiveRecord::Associations::AutosaveAssociation)
|
126
38
|
|
127
|
-
::ActiveRecord::Associations::Association.prepend(ActiveRecord::Association)
|
128
|
-
::ActiveRecord::Associations::BelongsToAssociation.prepend(ActiveRecord::BelongsToAssociation)
|
129
|
-
::ActiveRecord::Associations::CollectionProxy.include(ActiveRecord::CollectionProxy)
|
39
|
+
::ActiveRecord::Associations::Association.prepend(ActiveRecord::Associations::Association)
|
40
|
+
::ActiveRecord::Associations::BelongsToAssociation.prepend(ActiveRecord::Associations::BelongsToAssociation)
|
41
|
+
::ActiveRecord::Associations::CollectionProxy.include(ActiveRecord::Associations::CollectionProxy)
|
130
42
|
|
131
|
-
::ActiveRecord::Associations::Preloader::Association.prepend(ActiveRecord::Preloader::Association)
|
43
|
+
::ActiveRecord::Associations::Preloader::Association.prepend(ActiveRecord::Associations::Preloader::Association)
|
132
44
|
::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(ActiveRecord::AbstractAdapter)
|
133
45
|
::ActiveRecord::ConnectionAdapters::ConnectionPool.prepend(ActiveRecord::ConnectionPool)
|
134
46
|
::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(ActiveRecord::QueryCache)
|
@@ -165,49 +77,65 @@ module Switchman
|
|
165
77
|
::ActiveRecord::TypeCaster::Map.include(ActiveRecord::TypeCaster::Map)
|
166
78
|
::ActiveRecord::TypeCaster::Connection.include(ActiveRecord::TypeCaster::Connection)
|
167
79
|
|
168
|
-
::Rails.singleton_class.prepend(Rails::ClassMethods)
|
169
|
-
|
170
80
|
::Arel::Table.prepend(Arel::Table)
|
171
81
|
::Arel::Visitors::ToSql.prepend(Arel::Visitors::ToSql)
|
172
|
-
end
|
173
|
-
end
|
174
|
-
|
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
82
|
|
179
|
-
initializer 'switchman.extend_connection_adapters', after: 'active_record.initialize_database' do
|
180
|
-
::ActiveSupport.on_load(:active_record) do
|
181
83
|
::ActiveRecord::ConnectionAdapters::AbstractAdapter.descendants.each do |klass|
|
182
84
|
klass.prepend(ActiveRecord::AbstractAdapter::ForeignKeyCheck)
|
183
85
|
end
|
184
86
|
|
185
|
-
require 'switchman/active_record/table_definition'
|
186
87
|
::ActiveRecord::ConnectionAdapters::TableDefinition.prepend(ActiveRecord::TableDefinition)
|
187
|
-
|
188
|
-
Shard.send(:initialize_sharding)
|
189
88
|
end
|
89
|
+
# Ensure that ActiveRecord::Base is always loaded before any app-level initializers can go try to load Switchman::Shard or we get a loop
|
90
|
+
::ActiveRecord::Base
|
190
91
|
end
|
191
92
|
|
192
|
-
initializer 'switchman.
|
193
|
-
::ActiveSupport.on_load(:
|
194
|
-
|
195
|
-
require 'active_record/base'
|
93
|
+
initializer 'switchman.error_patch', after: 'active_record.initialize_database' do
|
94
|
+
::ActiveSupport.on_load(:active_record) do
|
95
|
+
::StandardError.include(StandardError)
|
196
96
|
end
|
197
97
|
end
|
198
98
|
|
199
|
-
initializer 'switchman.
|
200
|
-
::ActiveSupport.
|
201
|
-
require 'switchman/guard_rail'
|
99
|
+
initializer 'switchman.initialize_cache', before: 'initialize_cache', after: 'switchman.active_record_patch' do
|
100
|
+
::ActiveSupport::Cache.singleton_class.prepend(ActiveSupport::Cache::ClassMethods)
|
202
101
|
|
203
|
-
|
102
|
+
# if we haven't already setup our cache map out-of-band, set it up from
|
103
|
+
# config.cache_store now. behaves similarly to Rails' default
|
104
|
+
# initialize_cache initializer, but for each value in the map, rather
|
105
|
+
# than just Rails.cache. if config.cache_store is a flat value, uses it
|
106
|
+
# to fill just the Rails.env entry in the cache map.
|
107
|
+
unless Switchman.config[:cache_map].present?
|
108
|
+
cache_store_config = ::Rails.configuration.cache_store
|
109
|
+
cache_store_config = { ::Rails.env => cache_store_config } unless cache_store_config.is_a?(Hash)
|
110
|
+
|
111
|
+
Switchman.config[:cache_map] = ::ActiveSupport::Cache.lookup_stores(cache_store_config)
|
204
112
|
end
|
205
|
-
end
|
206
113
|
|
207
|
-
|
208
|
-
|
209
|
-
|
114
|
+
# if the configured cache map (either from before, or as populated from
|
115
|
+
# config.cache_store) didn't have an entry for Rails.env, add one using
|
116
|
+
# lookup_store(nil); matches the behavior of Rails' default
|
117
|
+
# initialize_cache initializer when config.cache_store is nil.
|
118
|
+
unless Switchman.config[:cache_map].key?(::Rails.env)
|
119
|
+
value = ::ActiveSupport::Cache.lookup_store(nil)
|
120
|
+
Switchman.config[:cache_map][::Rails.env] = value
|
121
|
+
end
|
210
122
|
|
123
|
+
middlewares = Switchman.config[:cache_map].values.map do |store|
|
124
|
+
store.middleware if store.respond_to?(:middleware)
|
125
|
+
end.compact.uniq
|
126
|
+
middlewares.each do |middleware|
|
127
|
+
config.middleware.insert_before('Rack::Runtime', middleware)
|
128
|
+
end
|
129
|
+
|
130
|
+
# prevent :initialize_cache from trying to (or needing to) set
|
131
|
+
# Rails.cache. once our switchman.extend_ar initializer (below) runs
|
132
|
+
# Rails.cache will be overridden to pull appropriate values from the
|
133
|
+
# cache map, but between now and then, Rails.cache should return the
|
134
|
+
# Rails.env entry in the cache map.
|
135
|
+
::Rails.cache = Switchman.config[:cache_map][::Rails.env]
|
136
|
+
::Rails.singleton_class.prepend(Rails::ClassMethods)
|
137
|
+
|
138
|
+
::ActiveSupport.on_load(:action_controller) do
|
211
139
|
::ActionController::Base.include(ActionController::Caching)
|
212
140
|
end
|
213
141
|
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
|
@@ -376,16 +368,12 @@ module Switchman
|
|
376
368
|
|
377
369
|
private
|
378
370
|
|
379
|
-
def sharding_initialized
|
380
|
-
@sharding_initialized ||= false
|
381
|
-
end
|
382
|
-
|
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,9 +395,6 @@ module Switchman
|
|
407
395
|
|
408
396
|
klass.connects_to shards: connects_to_hash
|
409
397
|
end
|
410
|
-
DatabaseServer.all.each { |db| db.guard! if db.config[:prefer_secondary] } unless @sharding_initialized
|
411
|
-
|
412
|
-
@sharding_initialized = true
|
413
398
|
end
|
414
399
|
|
415
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.22
|
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-07 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
|