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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eea092610f18550e6ab0d24baa9a2746e8452341116c0b5e3a94c15b1bcb9be0
4
- data.tar.gz: 53e0c2f7735838283311e57017de6bf020aae61f38dd394821a5bab5fdda869e
3
+ metadata.gz: f6ed57a128cb7da74d0df280b924f4369dcd224039343f4476796ae09d8573f5
4
+ data.tar.gz: 732fbee6c445f210f9b97b3921f30356a2eb7f9df1c925631dccce5e91612904
5
5
  SHA512:
6
- metadata.gz: da4096ca84676fecda1bee5e99a9e7e34c1d4cc2233e4a32f391d68ccd9d93a8d641b5d4a5ecd330e5df340daca8611bad0226709ba9f9fc6653282821216e0a
7
- data.tar.gz: d8a6f2245ac58e5ab64398245f7b14188cdb4675dea430fc2dd256b5aea73f68a81a30f5bcd3cd626634c492932bf85f3becab5dfa0a6e773abc6639615ab247
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
- 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
@@ -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,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
 
@@ -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(:initialize_sharding)
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
- if role == 'primary'
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.to_sym
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
 
@@ -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,13 @@ 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)
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.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'
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.extend_guard_rail', before: 'switchman.extend_ar' do
200
- ::ActiveSupport.on_load(:active_record) do
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
- ::GuardRail.singleton_class.prepend(GuardRail::ClassMethods)
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
- initializer 'switchman.extend_controller', after: 'guard_rail.extend_ar' do
208
- ::ActiveSupport.on_load(:action_controller) do
209
- require 'switchman/action_controller/caching'
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
@@ -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
@@ -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
- 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,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
- 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.19'
4
+ VERSION = '3.0.22'
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.19
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-06 00:00:00.000000000 Z
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/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