switchman 3.0.2 → 3.1.0

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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -1
  3. data/db/migrate/20180828183945_add_default_shard_index.rb +1 -1
  4. data/db/migrate/20180828192111_add_timestamps_to_shards.rb +1 -1
  5. data/lib/switchman/action_controller/caching.rb +2 -2
  6. data/lib/switchman/active_record/abstract_adapter.rb +2 -13
  7. data/lib/switchman/active_record/associations.rb +223 -0
  8. data/lib/switchman/active_record/attribute_methods.rb +144 -63
  9. data/lib/switchman/active_record/base.rb +100 -43
  10. data/lib/switchman/active_record/calculations.rb +12 -5
  11. data/lib/switchman/active_record/connection_pool.rb +9 -31
  12. data/lib/switchman/active_record/database_configurations.rb +18 -2
  13. data/lib/switchman/active_record/finder_methods.rb +2 -2
  14. data/lib/switchman/active_record/migration.rb +7 -4
  15. data/lib/switchman/active_record/model_schema.rb +1 -1
  16. data/lib/switchman/active_record/persistence.rb +7 -2
  17. data/lib/switchman/active_record/postgresql_adapter.rb +6 -2
  18. data/lib/switchman/active_record/predicate_builder.rb +1 -1
  19. data/lib/switchman/active_record/query_methods.rb +27 -14
  20. data/lib/switchman/active_record/reflection.rb +1 -1
  21. data/lib/switchman/active_record/relation.rb +25 -24
  22. data/lib/switchman/active_record/statement_cache.rb +2 -2
  23. data/lib/switchman/active_record/table_definition.rb +1 -1
  24. data/lib/switchman/active_record/test_fixtures.rb +43 -0
  25. data/lib/switchman/active_support/cache.rb +16 -0
  26. data/lib/switchman/arel.rb +28 -6
  27. data/lib/switchman/database_server.rb +71 -65
  28. data/lib/switchman/default_shard.rb +0 -2
  29. data/lib/switchman/engine.rb +67 -125
  30. data/lib/switchman/errors.rb +4 -2
  31. data/lib/switchman/guard_rail/relation.rb +6 -9
  32. data/lib/switchman/guard_rail.rb +5 -0
  33. data/lib/switchman/parallel.rb +68 -0
  34. data/lib/switchman/r_spec_helper.rb +5 -17
  35. data/lib/switchman/rails.rb +1 -4
  36. data/{app/models → lib}/switchman/shard.rb +61 -188
  37. data/lib/switchman/sharded_instrumenter.rb +1 -1
  38. data/lib/switchman/standard_error.rb +11 -12
  39. data/{app/models → lib}/switchman/unsharded_record.rb +1 -1
  40. data/lib/switchman/version.rb +1 -1
  41. data/lib/switchman.rb +22 -2
  42. data/lib/tasks/switchman.rake +24 -13
  43. metadata +24 -22
  44. data/lib/switchman/active_record/association.rb +0 -206
  45. data/lib/switchman/open4.rb +0 -80
@@ -8,101 +8,27 @@ 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]
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
+ if ::Rails.version < '7.0'
16
+ initialize_dependency_mechanism = ::Rails::Application::Bootstrap.initializers.find { |i| i.name == :initialize_dependency_mechanism }
17
+ initialize_dependency_mechanism.instance_variable_get(:@options)[:after] = :set_autoload_paths
67
18
  end
68
19
 
69
- initializer 'switchman.extend_ar', before: 'active_record.initialize_database' do
20
+ initializer 'switchman.active_record_patch',
21
+ before: 'active_record.initialize_database',
22
+ after: (::Rails.version < '7.0' ? :initialize_dependency_mechanism : :setup_once_autoloader) do
70
23
  ::ActiveSupport.on_load(:active_record) do
71
- require 'switchman/active_record/abstract_adapter'
72
- require 'switchman/active_record/association'
73
- require 'switchman/active_record/attribute_methods'
74
- require 'switchman/active_record/base'
75
- require 'switchman/active_record/calculations'
76
- require 'switchman/active_record/connection_pool'
77
- require 'switchman/active_record/database_configurations'
78
- require 'switchman/active_record/database_configurations/database_config'
79
- require 'switchman/active_record/finder_methods'
80
- require 'switchman/active_record/log_subscriber'
81
- require 'switchman/active_record/migration'
82
- require 'switchman/active_record/model_schema'
83
- require 'switchman/active_record/persistence'
84
- require 'switchman/active_record/predicate_builder'
85
- require 'switchman/active_record/query_cache'
86
- require 'switchman/active_record/query_methods'
87
- require 'switchman/active_record/reflection'
88
- require 'switchman/active_record/relation'
89
- require 'switchman/active_record/spawn_methods'
90
- require 'switchman/active_record/statement_cache'
91
- require 'switchman/active_record/tasks/database_tasks'
92
- require 'switchman/active_record/type_caster'
93
- require 'switchman/arel'
94
- require 'switchman/call_super'
95
- require 'switchman/rails'
96
- require 'switchman/guard_rail/relation'
97
- require 'switchman/standard_error'
98
-
99
- ::StandardError.include(StandardError)
24
+ # Switchman requires postgres, so just always load the pg adapter
25
+ require 'active_record/connection_adapters/postgresql_adapter'
100
26
 
101
27
  self.default_shard = ::Rails.env.to_sym
102
28
  self.default_role = :primary
103
29
 
104
- include ActiveRecord::Base
105
- include ActiveRecord::AttributeMethods
30
+ prepend ActiveRecord::Base
31
+ prepend ActiveRecord::AttributeMethods
106
32
  include ActiveRecord::Persistence
107
33
  singleton_class.prepend ActiveRecord::ModelSchema::ClassMethods
108
34
 
@@ -111,22 +37,24 @@ module Switchman
111
37
  ::ActiveRecord::StatementCache::BindMap.prepend(ActiveRecord::StatementCache::BindMap)
112
38
  ::ActiveRecord::StatementCache::Substitute.send(:attr_accessor, :primary, :sharded)
113
39
 
114
- ::ActiveRecord::Associations::CollectionAssociation.prepend(ActiveRecord::CollectionAssociation)
115
- ::ActiveRecord::Associations::HasOneAssociation.prepend(ActiveRecord::ForeignAssociation)
116
- ::ActiveRecord::Associations::HasManyAssociation.prepend(ActiveRecord::ForeignAssociation)
40
+ ::ActiveRecord::Associations::CollectionAssociation.prepend(ActiveRecord::Associations::CollectionAssociation)
41
+ ::ActiveRecord::Associations::HasOneAssociation.prepend(ActiveRecord::Associations::ForeignAssociation)
42
+ ::ActiveRecord::Associations::HasManyAssociation.prepend(ActiveRecord::Associations::ForeignAssociation)
117
43
 
118
44
  ::ActiveRecord::PredicateBuilder.singleton_class.prepend(ActiveRecord::PredicateBuilder)
119
45
 
120
- prepend(ActiveRecord::AutosaveAssociation)
46
+ prepend(ActiveRecord::Associations::AutosaveAssociation)
121
47
 
122
- ::ActiveRecord::Associations::Association.prepend(ActiveRecord::Association)
123
- ::ActiveRecord::Associations::BelongsToAssociation.prepend(ActiveRecord::BelongsToAssociation)
124
- ::ActiveRecord::Associations::CollectionProxy.include(ActiveRecord::CollectionProxy)
48
+ ::ActiveRecord::Associations::Association.prepend(ActiveRecord::Associations::Association)
49
+ ::ActiveRecord::Associations::BelongsToAssociation.prepend(ActiveRecord::Associations::BelongsToAssociation)
50
+ ::ActiveRecord::Associations::CollectionProxy.include(ActiveRecord::Associations::CollectionProxy)
125
51
 
126
- ::ActiveRecord::Associations::Preloader::Association.prepend(ActiveRecord::Preloader::Association)
52
+ ::ActiveRecord::Associations::Preloader::Association.prepend(ActiveRecord::Associations::Preloader::Association)
53
+ ::ActiveRecord::Associations::Preloader::Association::LoaderQuery.prepend(ActiveRecord::Associations::Preloader::Association::LoaderQuery) unless ::Rails.version < '7.0'
127
54
  ::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(ActiveRecord::AbstractAdapter)
128
55
  ::ActiveRecord::ConnectionAdapters::ConnectionPool.prepend(ActiveRecord::ConnectionPool)
129
56
  ::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(ActiveRecord::QueryCache)
57
+ ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(ActiveRecord::PostgreSQLAdapter)
130
58
 
131
59
  ::ActiveRecord::DatabaseConfigurations.prepend(ActiveRecord::DatabaseConfigurations)
132
60
  ::ActiveRecord::DatabaseConfigurations::DatabaseConfig.prepend(ActiveRecord::DatabaseConfigurations::DatabaseConfig)
@@ -150,60 +78,74 @@ module Switchman
150
78
  ::ActiveRecord::Relation.include(CallSuper)
151
79
 
152
80
  ::ActiveRecord::PredicateBuilder::AssociationQueryValue.prepend(ActiveRecord::PredicateBuilder::AssociationQueryValue)
81
+ ::ActiveRecord::PredicateBuilder::PolymorphicArrayValue.prepend(ActiveRecord::PredicateBuilder::AssociationQueryValue)
153
82
 
154
83
  ::ActiveRecord::Tasks::DatabaseTasks.singleton_class.prepend(ActiveRecord::Tasks::DatabaseTasks)
155
84
 
85
+ ::ActiveRecord::TestFixtures.prepend(ActiveRecord::TestFixtures)
86
+
156
87
  ::ActiveRecord::TypeCaster::Map.include(ActiveRecord::TypeCaster::Map)
157
88
  ::ActiveRecord::TypeCaster::Connection.include(ActiveRecord::TypeCaster::Connection)
158
89
 
159
- ::Rails.singleton_class.prepend(Rails::ClassMethods)
160
-
161
90
  ::Arel::Table.prepend(Arel::Table)
162
91
  ::Arel::Visitors::ToSql.prepend(Arel::Visitors::ToSql)
163
- end
164
- end
165
-
166
- def self.foreign_key_check(name, type, limit: nil)
167
- 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
168
- end
169
92
 
170
- initializer 'switchman.extend_connection_adapters', after: 'active_record.initialize_database' do
171
- ::ActiveSupport.on_load(:active_record) do
172
93
  ::ActiveRecord::ConnectionAdapters::AbstractAdapter.descendants.each do |klass|
173
94
  klass.prepend(ActiveRecord::AbstractAdapter::ForeignKeyCheck)
174
95
  end
175
96
 
176
- require 'switchman/active_record/table_definition'
177
97
  ::ActiveRecord::ConnectionAdapters::TableDefinition.prepend(ActiveRecord::TableDefinition)
178
-
179
- if defined?(::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
180
- require 'switchman/active_record/postgresql_adapter'
181
- ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(ActiveRecord::PostgreSQLAdapter)
182
- end
183
-
184
- Shard.send(:initialize_sharding)
185
98
  end
99
+ # Ensure that ActiveRecord::Base is always loaded before any app-level initializers can go try to load Switchman::Shard or we get a loop
100
+ ::ActiveRecord::Base
186
101
  end
187
102
 
188
- initializer 'switchman.eager_load' do
189
- ::ActiveSupport.on_load(:before_eager_load) do
190
- # This needs to be loaded before Switchman::Shard, otherwise it won't autoload it correctly
191
- require 'active_record/base'
103
+ initializer 'switchman.error_patch', after: 'active_record.initialize_database' do
104
+ ::ActiveSupport.on_load(:active_record) do
105
+ ::StandardError.include(StandardError)
192
106
  end
193
107
  end
194
108
 
195
- initializer 'switchman.extend_guard_rail', before: 'switchman.extend_ar' do
196
- ::ActiveSupport.on_load(:active_record) do
197
- require 'switchman/guard_rail'
109
+ initializer 'switchman.initialize_cache', before: :initialize_cache, after: 'active_record.initialize_database' do
110
+ ::ActiveSupport::Cache.singleton_class.prepend(ActiveSupport::Cache::ClassMethods)
198
111
 
199
- ::GuardRail.singleton_class.prepend(GuardRail::ClassMethods)
112
+ # if we haven't already setup our cache map out-of-band, set it up from
113
+ # config.cache_store now. behaves similarly to Rails' default
114
+ # initialize_cache initializer, but for each value in the map, rather
115
+ # than just Rails.cache. if config.cache_store is a flat value, uses it
116
+ # to fill just the Rails.env entry in the cache map.
117
+ unless Switchman.config[:cache_map].present?
118
+ cache_store_config = ::Rails.configuration.cache_store
119
+ cache_store_config = { ::Rails.env => cache_store_config } unless cache_store_config.is_a?(Hash)
120
+
121
+ Switchman.config[:cache_map] = ::ActiveSupport::Cache.lookup_stores(cache_store_config)
200
122
  end
201
- end
202
123
 
203
- initializer 'switchman.extend_controller', after: 'guard_rail.extend_ar' do
204
- ::ActiveSupport.on_load(:action_controller) do
205
- require 'switchman/action_controller/caching'
124
+ # if the configured cache map (either from before, or as populated from
125
+ # config.cache_store) didn't have an entry for Rails.env, add one using
126
+ # lookup_store(nil); matches the behavior of Rails' default
127
+ # initialize_cache initializer when config.cache_store is nil.
128
+ unless Switchman.config[:cache_map].key?(::Rails.env)
129
+ value = ::ActiveSupport::Cache.lookup_store(nil)
130
+ Switchman.config[:cache_map][::Rails.env] = value
131
+ end
206
132
 
133
+ middlewares = Switchman.config[:cache_map].values.map do |store|
134
+ store.middleware if store.respond_to?(:middleware)
135
+ end.compact.uniq
136
+ middlewares.each do |middleware|
137
+ config.middleware.insert_before('Rack::Runtime', middleware)
138
+ end
139
+
140
+ # prevent :initialize_cache from trying to (or needing to) set
141
+ # Rails.cache. once our switchman.extend_ar initializer (below) runs
142
+ # Rails.cache will be overridden to pull appropriate values from the
143
+ # cache map, but between now and then, Rails.cache should return the
144
+ # Rails.env entry in the cache map.
145
+ ::Rails.cache = Switchman.config[:cache_map][::Rails.env]
146
+ ::Rails.singleton_class.prepend(Rails::ClassMethods)
147
+
148
+ ::ActiveSupport.on_load(:action_controller) do
207
149
  ::ActionController::Base.include(ActionController::Caching)
208
150
  end
209
151
  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
@@ -5,21 +5,18 @@ module Switchman
5
5
  module Relation
6
6
  def exec_queries(*args)
7
7
  if lock_value
8
- db = Shard.current(connection_classes).database_server
9
- return db.unguard { super } if ::GuardRail.environment != db.guard_rail_environment
8
+ db = Shard.current(connection_class_for_self).database_server
9
+ db.unguard { super }
10
+ else
11
+ super
10
12
  end
11
- super
12
13
  end
13
14
 
14
15
  %w[update_all delete_all].each do |method|
15
16
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
16
17
  def #{method}(*args)
17
- db = Shard.current(connection_classes).database_server
18
- if ::GuardRail.environment != db.guard_rail_environment
19
- db.unguard { super }
20
- else
21
- super
22
- end
18
+ db = Shard.current(connection_class_for_self).database_server
19
+ db.unguard { super }
23
20
  end
24
21
  RUBY
25
22
  end
@@ -3,6 +3,11 @@
3
3
  module Switchman
4
4
  module GuardRail
5
5
  module ClassMethods
6
+ def environment
7
+ # no overrides so we get the global role, not the role for the default shard
8
+ ::ActiveRecord::Base.current_role(without_overrides: true)
9
+ end
10
+
6
11
  def activate(role)
7
12
  DatabaseServer.send(:reference_role, role)
8
13
  super
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parallel'
4
+
5
+ module Switchman
6
+ module Parallel
7
+ module UndumpableException
8
+ def initialize(original)
9
+ super
10
+ @active_shards = original.instance_variable_get(:@active_shards)
11
+ current_shard
12
+ end
13
+ end
14
+
15
+ class QuietExceptionWrapper
16
+ attr_accessor :name
17
+
18
+ def initialize(name, wrapper)
19
+ @name = name
20
+ @wrapper = wrapper
21
+ end
22
+
23
+ def exception
24
+ @wrapper.exception
25
+ end
26
+ end
27
+
28
+ class UndumpableResult
29
+ attr_reader :name
30
+
31
+ def initialize(result)
32
+ @name = result.inspect
33
+ end
34
+
35
+ def inspect
36
+ "#<UndumpableResult:#{name}>"
37
+ end
38
+ end
39
+
40
+ class ResultWrapper
41
+ attr_reader :result
42
+
43
+ def initialize(result)
44
+ @result =
45
+ begin
46
+ Marshal.dump(result) && result
47
+ rescue
48
+ UndumpableResult.new(result)
49
+ end
50
+ end
51
+ end
52
+
53
+ class PrefixingIO
54
+ delegate_missing_to :@original_io
55
+
56
+ def initialize(prefix, original_io)
57
+ @prefix = prefix
58
+ @original_io = original_io
59
+ end
60
+
61
+ def puts(*args)
62
+ args.flatten.each { |arg| @original_io.puts "#{@prefix}: #{arg}" }
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ ::Parallel::UndumpableException.prepend(::Switchman::Parallel::UndumpableException)
@@ -70,7 +70,10 @@ module Switchman
70
70
  Shard.default(reload: true)
71
71
  puts 'Done!'
72
72
 
73
+ main_pid = Process.pid
73
74
  at_exit do
75
+ next unless main_pid == Process.pid
76
+
74
77
  # preserve rspec's exit status
75
78
  status = $!.is_a?(::SystemExit) ? $!.status : nil
76
79
  puts 'Tearing down sharding for all specs'
@@ -111,28 +114,12 @@ module Switchman
111
114
  Shard.default(reload: true)
112
115
  @shard1 = Shard.find(@shard1.id)
113
116
  @shard2 = Shard.find(@shard2.id)
114
- shards = [@shard2]
115
- shards << @shard1 unless @shard1.database_server == Shard.default.database_server
116
- shards.each do |shard|
117
- shard.activate do
118
- ::ActiveRecord::Base.connection.begin_transaction joinable: false
119
- end
120
- end
121
117
  end
122
118
  end
123
119
 
124
120
  klass.after do
125
121
  next if @@sharding_failed
126
122
 
127
- if use_transactional_tests
128
- shards = [@shard2]
129
- shards << @shard1 unless @shard1.database_server == Shard.default.database_server
130
- shards.each do |shard|
131
- shard.activate do
132
- ::ActiveRecord::Base.connection.rollback_transaction if ::ActiveRecord::Base.connection.transaction_open?
133
- end
134
- end
135
- end
136
123
  # clean up after specs
137
124
  DatabaseServer.all.each do |ds|
138
125
  if ds.fake? && ds != @shard2.database_server
@@ -143,7 +130,8 @@ module Switchman
143
130
  end
144
131
 
145
132
  klass.after(:all) do
146
- Shard.connection.update("TRUNCATE #{Shard.quoted_table_name} CASCADE")
133
+ # Don't truncate because that can create some fun cross-connection lock contention
134
+ Shard.delete_all
147
135
  Switchman.cache.delete('default_shard')
148
136
  Shard.default(reload: true)
149
137
  end
@@ -4,10 +4,7 @@ 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