switchman 3.0.21 → 3.0.24

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df4352d56024adc191b904c1786c43dff9a685be0c7e82e5813339a107ac6df3
4
- data.tar.gz: 7f6ce97fdadcf3dff63b64065e94b2c4de5a481b234ec6f36429ff7eb796eccc
3
+ metadata.gz: 469ef6bca62776e9ba974a5c6d2a6220fc6ddcf1649956f43247465cbfb24f37
4
+ data.tar.gz: 9fefef5095835cf28a6c72197ed74f6de9974d7c9c9019ad4ea87209d98c37e9
5
5
  SHA512:
6
- metadata.gz: 26b862b5b2d44c3aebf24d2ce469e332ff3af117b75e05559d1343b399a01d36db1a321f6fb2f48e535d3e0f99517c56c0dd549911d47b9ef024e8652b5f54b3
7
- data.tar.gz: 229c8277f8df802f2f5f29221d66afbaba873ff70b0f97ca0ff9356a99b58d8b1dc08bd6fb07c75800275dfc4ef63ec8296451bdfed1204860fbfc1cd764d291
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=(_cache)
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
- Engine.foreign_key_check(name, type, limit: limit)
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 current_role_overriden?
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, current_shard)
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,7 +4,7 @@ module Switchman
4
4
  module ActiveRecord
5
5
  module TableDefinition
6
6
  def column(name, type, limit: nil, **)
7
- Engine.foreign_key_check(name, type, limit: limit)
7
+ Switchman.foreign_key_check(name, type, limit: limit)
8
8
  super
9
9
  end
10
10
  end
@@ -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(:initialize_sharding)
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(:initialize_sharding)
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
- if role == 'primary'
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.to_sym
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.current_role_overriden?
153
+ return yield unless ::ActiveRecord::Base.role_overriden?(id.to_sym)
150
154
 
151
155
  begin
152
156
  unguard!
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'switchman/database_server'
4
-
5
3
  module Switchman
6
4
  class DefaultShard
7
5
  def id
@@ -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
- config.autoload_once_paths << File.expand_path('app/models', config.paths.path)
11
+ ::GuardRail.singleton_class.prepend(GuardRail::ClassMethods)
12
12
 
13
- def self.lookup_stores(cache_store_config)
14
- result = {}
15
- cache_store_config.each do |key, value|
16
- next if value.is_a?(String)
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
- 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
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.eager_load' do
193
- ::ActiveSupport.on_load(:before_eager_load) do
194
- # This needs to be loaded before Switchman::Shard, otherwise it won't autoload it correctly
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.extend_guard_rail', before: 'switchman.extend_ar' do
200
- ::ActiveSupport.on_load(:active_record) do
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
- ::GuardRail.singleton_class.prepend(GuardRail::ClassMethods)
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
- initializer 'switchman.extend_controller', after: 'guard_rail.extend_ar' do
208
- ::ActiveSupport.on_load(:action_controller) do
209
- require 'switchman/action_controller/caching'
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
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Switchman
4
- class NonExistentShardError < RuntimeError; end
4
+ module Errors
5
+ class NonExistentShardError < RuntimeError; end
5
6
 
6
- class ParallelShardExecError < RuntimeError; end
7
+ class ParallelShardExecError < RuntimeError; end
8
+ end
7
9
  end
@@ -4,16 +4,13 @@ module Switchman
4
4
  module Rails
5
5
  module ClassMethods
6
6
  def self.prepended(klass)
7
- # in Rails 4+, the Rails.cache= method was used during bootstrap to set
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 ? Switchman::Shard.current.database_server.cache_store : super
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
- sharding_initialized ? default : nil
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
- initialize_sharding
373
+ configure_connects_to
386
374
  end
387
375
 
388
- def initialize_sharding
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
- begin
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Switchman
4
4
  class UnshardedRecord < ::ActiveRecord::Base
5
- sharded_model
5
+ self.abstract_class = true
6
6
  end
7
7
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Switchman
4
- VERSION = '3.0.21'
4
+ VERSION = '3.0.24'
5
5
  end
data/lib/switchman.rb CHANGED
@@ -1,8 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'guard_rail'
4
- require 'switchman/parallel'
5
- require 'switchman/engine'
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.21
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-06 00:00:00.000000000 Z
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/association.rb
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