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