switchman 3.0.14 → 3.5.20

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +16 -15
  3. data/db/migrate/20180828183945_add_default_shard_index.rb +1 -1
  4. data/db/migrate/20190114212900_add_unique_name_indexes.rb +10 -4
  5. data/lib/switchman/action_controller/caching.rb +2 -2
  6. data/lib/switchman/active_record/abstract_adapter.rb +6 -6
  7. data/lib/switchman/active_record/associations.rb +365 -0
  8. data/lib/switchman/active_record/attribute_methods.rb +188 -99
  9. data/lib/switchman/active_record/base.rb +185 -40
  10. data/lib/switchman/active_record/calculations.rb +64 -40
  11. data/lib/switchman/active_record/connection_handler.rb +18 -0
  12. data/lib/switchman/active_record/connection_pool.rb +24 -5
  13. data/lib/switchman/active_record/database_configurations.rb +37 -13
  14. data/lib/switchman/active_record/finder_methods.rb +46 -16
  15. data/lib/switchman/active_record/log_subscriber.rb +11 -5
  16. data/lib/switchman/active_record/migration.rb +52 -8
  17. data/lib/switchman/active_record/model_schema.rb +1 -1
  18. data/lib/switchman/active_record/persistence.rb +31 -3
  19. data/lib/switchman/active_record/postgresql_adapter.rb +11 -10
  20. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  21. data/lib/switchman/active_record/query_cache.rb +49 -20
  22. data/lib/switchman/active_record/query_methods.rb +187 -136
  23. data/lib/switchman/active_record/reflection.rb +1 -1
  24. data/lib/switchman/active_record/relation.rb +33 -26
  25. data/lib/switchman/active_record/spawn_methods.rb +2 -2
  26. data/lib/switchman/active_record/statement_cache.rb +11 -7
  27. data/lib/switchman/active_record/table_definition.rb +1 -1
  28. data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
  29. data/lib/switchman/active_record/test_fixtures.rb +26 -16
  30. data/lib/switchman/active_support/cache.rb +20 -1
  31. data/lib/switchman/arel.rb +34 -18
  32. data/lib/switchman/call_super.rb +8 -2
  33. data/lib/switchman/database_server.rb +91 -45
  34. data/lib/switchman/default_shard.rb +14 -5
  35. data/lib/switchman/engine.rb +79 -126
  36. data/lib/switchman/environment.rb +2 -2
  37. data/lib/switchman/errors.rb +17 -2
  38. data/lib/switchman/guard_rail/relation.rb +8 -10
  39. data/lib/switchman/guard_rail.rb +5 -0
  40. data/lib/switchman/parallel.rb +68 -0
  41. data/lib/switchman/r_spec_helper.rb +14 -11
  42. data/lib/switchman/rails.rb +2 -5
  43. data/{app/models → lib}/switchman/shard.rb +186 -189
  44. data/lib/switchman/sharded_instrumenter.rb +5 -1
  45. data/lib/switchman/shared_schema_cache.rb +11 -0
  46. data/lib/switchman/standard_error.rb +6 -5
  47. data/lib/switchman/test_helper.rb +2 -2
  48. data/{app/models → lib}/switchman/unsharded_record.rb +1 -1
  49. data/lib/switchman/version.rb +1 -1
  50. data/lib/switchman.rb +44 -12
  51. data/lib/tasks/switchman.rake +74 -53
  52. metadata +42 -53
  53. data/lib/switchman/active_record/association.rb +0 -206
  54. data/lib/switchman/open4.rb +0 -80
@@ -7,9 +7,14 @@ module Switchman
7
7
  def drop(*)
8
8
  super
9
9
  # no really, it's gone
10
- Switchman.cache.delete('default_shard')
10
+ Switchman.cache.delete("default_shard")
11
11
  Shard.default(reload: true)
12
12
  end
13
+
14
+ def raise_for_multi_db(*)
15
+ # ignore; Switchman doesn't use namespaced tasks for multiple shards; it uses
16
+ # environment variables to filter which shards you want to target
17
+ end
13
18
  end
14
19
  end
15
20
  end
@@ -12,31 +12,41 @@ module Switchman
12
12
  # Replace the one that activerecord natively uses with a switchman-optimized one
13
13
  ::ActiveSupport::Notifications.unsubscribe(@connection_subscriber)
14
14
  # Code adapted from the code in rails proper
15
- @connection_subscriber = ::ActiveSupport::Notifications.subscribe('!connection.active_record') do |_, _, _, _, payload|
16
- spec_name = payload[:spec_name] if payload.key?(:spec_name)
17
- shard = payload[:shard] if payload.key?(:shard)
18
- setup_shared_connection_pool
15
+ @connection_subscriber =
16
+ ::ActiveSupport::Notifications.subscribe("!connection.active_record") do |_, _, _, _, payload|
17
+ spec_name = if ::Rails.version < "7.1"
18
+ payload[:spec_name] if payload.key?(:spec_name)
19
+ elsif payload.key?(:connection_name)
20
+ payload[:connection_name]
21
+ end
22
+ shard = payload[:shard] if payload.key?(:shard)
19
23
 
20
- if spec_name && !FORBIDDEN_DB_ENVS.include?(shard)
21
- begin
22
- connection = ::ActiveRecord::Base.connection_handler.retrieve_connection(spec_name, shard: shard)
23
- rescue ::ActiveRecord::ConnectionNotEstablished, ::ActiveRecord::NoDatabaseError
24
- connection = nil
25
- end
24
+ if spec_name && !FORBIDDEN_DB_ENVS.include?(shard)
25
+ begin
26
+ connection = ::ActiveRecord::Base.connection_handler.retrieve_connection(spec_name, shard: shard)
27
+ connection.connect! if ::Rails.version >= "7.1" # eagerly validate the connection
28
+ rescue ::ActiveRecord::ConnectionNotEstablished, ::ActiveRecord::NoDatabaseError
29
+ connection = nil
30
+ end
26
31
 
27
- if connection && !@fixture_connections.include?(connection)
28
- connection.begin_transaction joinable: false, _lazy: false
29
- connection.pool.lock_thread = true if lock_threads
30
- @fixture_connections << connection
32
+ if connection
33
+ setup_shared_connection_pool
34
+ unless @fixture_connections.include?(connection)
35
+ connection.begin_transaction joinable: false, _lazy: false
36
+ connection.pool.lock_thread = true if lock_threads
37
+ @fixture_connections << connection
38
+ end
39
+ end
31
40
  end
32
41
  end
33
- end
34
42
  end
35
43
 
36
44
  def enlist_fixture_connections
37
45
  setup_shared_connection_pool
38
46
 
39
- ::ActiveRecord::Base.connection_handler.connection_pool_list.reject { |cp| FORBIDDEN_DB_ENVS.include?(cp.db_config.env_name.to_sym) }.map(&:connection)
47
+ ::ActiveRecord::Base.connection_handler.connection_pool_list(:primary).reject do |cp|
48
+ FORBIDDEN_DB_ENVS.include?(cp.db_config.env_name.to_sym)
49
+ end.map(&:connection)
40
50
  end
41
51
  end
42
52
  end
@@ -4,11 +4,30 @@ 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,
10
26
  # so just checking that will cause it to get required
11
- ::ActiveSupport::Cache::RedisCacheStore.prepend(RedisCacheStore) if store.instance_of?(ActiveSupport::Cache::RedisCacheStore) && !::ActiveSupport::Cache::RedisCacheStore.ancestors.include?(RedisCacheStore)
27
+ if store.instance_of?(ActiveSupport::Cache::RedisCacheStore) &&
28
+ !::ActiveSupport::Cache::RedisCacheStore <= RedisCacheStore
29
+ ::ActiveSupport::Cache::RedisCacheStore.prepend(RedisCacheStore)
30
+ end
12
31
  store.options[:namespace] ||= -> { Shard.current.default? ? nil : "shard_#{Shard.current.id}" }
13
32
  store
14
33
  end
@@ -13,38 +13,54 @@ module Switchman
13
13
  # rubocop:disable Naming/MethodName
14
14
  # rubocop:disable Naming/MethodParameterName
15
15
 
16
+ def visit_Arel_Nodes_Cte(o, collector)
17
+ collector << quote_local_table_name(o.name)
18
+ collector << " AS "
19
+
20
+ case o.materialized
21
+ when true
22
+ collector << "MATERIALIZED "
23
+ when false
24
+ collector << "NOT MATERIALIZED "
25
+ end
26
+
27
+ visit o.relation, collector
28
+ end
29
+
16
30
  def visit_Arel_Nodes_TableAlias(o, collector)
17
31
  collector = visit o.relation, collector
18
- collector << ' '
32
+ collector << " "
19
33
  collector << quote_local_table_name(o.name)
20
34
  end
21
35
 
22
36
  def visit_Arel_Attributes_Attribute(o, collector)
23
37
  join_name = o.relation.table_alias || o.relation.name
24
- collector << quote_local_table_name(join_name) << '.' << quote_column_name(o.name)
38
+ collector << quote_local_table_name(join_name) << "." << quote_column_name(o.name)
25
39
  end
26
40
 
27
- def visit_Arel_Nodes_HomogeneousIn(o, collector)
28
- collector.preparable = false
41
+ if ::Rails.version < "7.1"
42
+ def visit_Arel_Nodes_HomogeneousIn(o, collector)
43
+ collector.preparable = false
29
44
 
30
- collector << quote_local_table_name(o.table_name) << '.' << quote_column_name(o.column_name)
45
+ collector << quote_local_table_name(o.table_name) << "." << quote_column_name(o.column_name)
31
46
 
32
- collector << if o.type == :in
33
- ' IN ('
34
- else
35
- ' NOT IN ('
36
- end
47
+ collector << if o.type == :in
48
+ " IN ("
49
+ else
50
+ " NOT IN ("
51
+ end
37
52
 
38
- values = o.casted_values
53
+ values = o.casted_values
39
54
 
40
- if values.empty?
41
- collector << @connection.quote(nil)
42
- else
43
- collector.add_binds(values, o.proc_for_binds, &bind_block)
44
- end
55
+ if values.empty?
56
+ collector << @connection.quote(nil)
57
+ else
58
+ collector.add_binds(values, o.proc_for_binds, &bind_block)
59
+ end
45
60
 
46
- collector << ')'
47
- collector
61
+ collector << ")"
62
+ collector
63
+ end
48
64
  end
49
65
 
50
66
  # rubocop:enable Naming/MethodName
@@ -12,8 +12,14 @@ module Switchman
12
12
  method.super_method
13
13
  end
14
14
 
15
- def call_super(method, above_module, *args, &block)
16
- super_method_above(method, above_module).call(*args, &block)
15
+ if RUBY_VERSION <= "2.8"
16
+ def call_super(method, above_module, *args, &block)
17
+ super_method_above(method, above_module).call(*args, &block)
18
+ end
19
+ else
20
+ def call_super(method, above_module, *args, **kwargs, &block)
21
+ super_method_above(method, above_module).call(*args, **kwargs, &block)
22
+ end
17
23
  end
18
24
  end
19
25
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'securerandom'
3
+ require "securerandom"
4
4
 
5
5
  module Switchman
6
6
  class DatabaseServer
@@ -8,24 +8,25 @@ module Switchman
8
8
 
9
9
  class << self
10
10
  attr_accessor :creating_new_shard
11
+ attr_reader :all_roles
12
+
13
+ include Enumerable
14
+
15
+ delegate :each, to: :all
11
16
 
12
17
  def all
13
18
  database_servers.values
14
19
  end
15
20
 
16
- def all_roles
17
- @all_roles ||= all.map(&:roles).flatten.uniq
18
- end
19
-
20
21
  def find(id_or_all)
21
22
  return all if id_or_all == :all
22
- return id_or_all.map { |id| database_servers[id || ::Rails.env] }.compact.uniq if id_or_all.is_a?(Array)
23
+ return id_or_all.filter_map { |id| database_servers[id || ::Rails.env] }.uniq if id_or_all.is_a?(Array)
23
24
 
24
25
  database_servers[id_or_all || ::Rails.env]
25
26
  end
26
27
 
27
28
  def create(settings = {})
28
- raise 'database servers should be set up in database.yml' unless ::Rails.env.test?
29
+ raise "database servers should be set up in database.yml" unless ::Rails.env.test?
29
30
 
30
31
  id = settings[:id]
31
32
  unless id
@@ -38,7 +39,7 @@ module Switchman
38
39
  database_servers[server.id] = server
39
40
  ::ActiveRecord::Base.configurations.configurations <<
40
41
  ::ActiveRecord::DatabaseConfigurations::HashConfig.new(::Rails.env, "#{server.id}/primary", settings)
41
- Shard.send(:initialize_sharding)
42
+ Shard.send(:configure_connects_to)
42
43
  server
43
44
  end
44
45
 
@@ -49,31 +50,48 @@ module Switchman
49
50
  servers[rand(servers.length)]
50
51
  end
51
52
 
53
+ def guard_servers
54
+ all.each { |db| db.guard! if db.config[:prefer_secondary] }
55
+ end
56
+
57
+ def regions
58
+ @regions ||= all.filter_map(&:region).uniq.sort
59
+ end
60
+
52
61
  private
53
62
 
54
63
  def reference_role(role)
55
64
  return if all_roles.include?(role)
56
65
 
57
66
  @all_roles << role
58
- Shard.send(:initialize_sharding)
67
+ Shard.send(:configure_connects_to)
59
68
  end
60
69
 
61
70
  def database_servers
62
71
  if !@database_servers || @database_servers.empty?
63
72
  @database_servers = {}.with_indifferent_access
73
+ roles = []
64
74
  ::ActiveRecord::Base.configurations.configurations.each do |config|
65
- if config.name.include?('/')
66
- name, role = config.name.split('/')
75
+ if config.name.include?("/")
76
+ name, role = config.name.split("/")
67
77
  else
68
78
  name, role = config.env_name, config.name
69
79
  end
80
+ role = role.to_sym
70
81
 
71
- if role == 'primary'
82
+ roles << role
83
+ if role == :primary
72
84
  @database_servers[name] = DatabaseServer.new(config.env_name, config.configuration_hash)
73
85
  else
74
86
  @database_servers[name].roles << role
75
87
  end
76
88
  end
89
+ # Do this after so that all database servers for all roles are established and we won't prematurely
90
+ # configure a connection for the wrong role
91
+ @all_roles = roles.uniq
92
+ return @database_servers if @database_servers.empty?
93
+
94
+ Shard.send(:configure_connects_to)
77
95
  end
78
96
  @database_servers
79
97
  end
@@ -102,8 +120,9 @@ module Switchman
102
120
  self.class.send(:database_servers).delete(id) if id
103
121
  Shard.sharded_models.each do |klass|
104
122
  self.class.all_roles.each do |role|
105
- klass.connection_handler.remove_connection_pool(klass.connection_specification_name, role: role,
106
- shard: id.to_sym)
123
+ klass.connection_handler.remove_connection_pool(klass.connection_specification_name,
124
+ role: role,
125
+ shard: id.to_sym)
107
126
  end
108
127
  end
109
128
  end
@@ -129,32 +148,57 @@ module Switchman
129
148
  end
130
149
  end
131
150
 
132
- def guard_rail_environment
133
- @guard_rail_environment || ::GuardRail.environment
151
+ def region
152
+ config[:region]
153
+ end
154
+
155
+ # @param region [String, Array<String>] the region(s) to check against
156
+ # @return true if the database server doesn't have a region, or it
157
+ # matches the specified region
158
+ def in_region?(region)
159
+ !self.region || (region.is_a?(Array) ? region.include?(self.region) : self.region == region)
160
+ end
161
+
162
+ # @return true if the database server doesn't have a region, Switchman is
163
+ # not configured with a region, or the database server's region matches
164
+ # Switchman's current region
165
+ def in_current_region?
166
+ unless instance_variable_defined?(:@in_current_region)
167
+ @in_current_region = !region ||
168
+ !Switchman.region ||
169
+ region == Switchman.region
170
+ end
171
+ @in_current_region
134
172
  end
135
173
 
136
174
  # locks this db to a specific environment, except for
137
175
  # when doing writes (then it falls back to the current
138
176
  # value of GuardRail.environment)
139
177
  def guard!(environment = :secondary)
140
- @guard_rail_environment = environment
178
+ DatabaseServer.send(:reference_role, environment)
179
+ ::ActiveRecord::Base.connected_to_stack << { shard_roles: { id.to_sym => environment },
180
+ klasses: [::ActiveRecord::Base] }
141
181
  end
142
182
 
143
183
  def unguard!
144
- @guard_rail_environment = nil
184
+ ::ActiveRecord::Base.connected_to_stack << { shard_roles: { id.to_sym => :_switchman_inherit },
185
+ klasses: [::ActiveRecord::Base] }
145
186
  end
146
187
 
147
188
  def unguard
148
- old_env = @guard_rail_environment
149
- unguard!
150
- yield
151
- ensure
152
- guard!(old_env)
189
+ return yield unless ::ActiveRecord::Base.role_overriden?(id.to_sym)
190
+
191
+ begin
192
+ unguard!
193
+ yield
194
+ ensure
195
+ ::ActiveRecord::Base.connected_to_stack.pop
196
+ end
153
197
  end
154
198
 
155
199
  def shards
156
200
  if id == ::Rails.env
157
- Shard.where('database_server_id IS NULL OR database_server_id=?', id)
201
+ Shard.where("database_server_id IS NULL OR database_server_id=?", id)
158
202
  else
159
203
  Shard.where(database_server_id: id)
160
204
  end
@@ -179,7 +223,7 @@ module Switchman
179
223
  end
180
224
 
181
225
  id ||= begin
182
- id_seq = Shard.connection.quote(Shard.connection.quote_table_name('switchman_shards_id_seq'))
226
+ id_seq = Shard.connection.quote(Shard.connection.quote_table_name("switchman_shards_id_seq"))
183
227
  next_id = Shard.connection.select_value("SELECT nextval(#{id_seq})")
184
228
  next_id.to_i
185
229
  end
@@ -196,29 +240,32 @@ module Switchman
196
240
  name: name,
197
241
  database_server_id: self.id)
198
242
  if create_statement
199
- if ::ActiveRecord::Base.connection.select_value("SELECT 1 FROM pg_namespace WHERE nspname=#{::ActiveRecord::Base.connection.quote(name)}")
243
+ if ::ActiveRecord::Base.connection.select_value(
244
+ "SELECT 1 FROM pg_namespace WHERE nspname=#{::ActiveRecord::Base.connection.quote(name)}"
245
+ )
200
246
  schema_already_existed = true
201
- raise 'This schema already exists; cannot overwrite'
247
+ raise "This schema already exists; cannot overwrite"
202
248
  end
203
249
  Array(create_statement.call).each do |stmt|
204
250
  ::ActiveRecord::Base.connection.execute(stmt)
205
251
  end
206
252
  end
207
- if config[:adapter] == 'postgresql'
208
- old_proc = ::ActiveRecord::Base.connection.raw_connection.set_notice_processor do
209
- end
253
+ if config[:adapter] == "postgresql"
254
+ old_proc = ::ActiveRecord::Base.connection.raw_connection.set_notice_processor {}
210
255
  end
211
256
  old_verbose = ::ActiveRecord::Migration.verbose
212
257
  ::ActiveRecord::Migration.verbose = false
213
258
 
214
259
  unless schema == false
215
260
  shard.activate do
216
- reset_column_information
217
-
218
261
  ::ActiveRecord::Base.connection.transaction(requires_new: true) do
219
- ::ActiveRecord::Base.connection.migration_context.migrate
262
+ if ::Rails.version < "7.1"
263
+ ::ActiveRecord::Base.connection.migration_context.migrate
264
+ else
265
+ ::ActiveRecord::MigrationContext.new(::ActiveRecord::Migrator.migrations_paths).migrate
266
+ end
220
267
  end
221
- reset_column_information
268
+
222
269
  ::ActiveRecord::Base.descendants.reject do |m|
223
270
  m <= UnshardedRecord || !m.table_exists?
224
271
  end.each(&:define_attribute_methods)
@@ -232,7 +279,6 @@ module Switchman
232
279
  rescue
233
280
  shard&.destroy
234
281
  shard&.drop_database rescue nil unless schema_already_existed
235
- reset_column_information unless schema == false rescue nil
236
282
  raise
237
283
  ensure
238
284
  self.class.creating_new_shard = false
@@ -257,18 +303,18 @@ module Switchman
257
303
  end
258
304
 
259
305
  def primary_shard
260
- unless instance_variable_defined?(:@primary_shard)
261
- # if sharding isn't fully set up yet, we may not be able to query the shards table
262
- @primary_shard = Shard.default if Shard.default.database_server == self
263
- @primary_shard ||= shards.where(name: nil).first
264
- end
265
- @primary_shard
266
- end
306
+ return nil unless primary_shard_id
267
307
 
268
- private
308
+ Shard.lookup(primary_shard_id)
309
+ end
269
310
 
270
- def reset_column_information
271
- ::ActiveRecord::Base.descendants.reject { |m| m <= UnshardedRecord }.each(&:reset_column_information)
311
+ def primary_shard_id
312
+ unless instance_variable_defined?(:@primary_shard_id)
313
+ # if sharding isn't fully set up yet, we may not be able to query the shards table
314
+ @primary_shard_id = Shard.default.id if Shard.default.database_server == self
315
+ @primary_shard_id ||= shards.where(name: nil).first&.id
316
+ end
317
+ @primary_shard_id
272
318
  end
273
319
  end
274
320
  end
@@ -1,13 +1,12 @@
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
- 'default'
6
+ "default"
9
7
  end
10
- alias cache_key id
8
+ alias_method :cache_key, :id
9
+
11
10
  def activate(*_classes)
12
11
  yield
13
12
  end
@@ -59,8 +58,18 @@ module Switchman
59
58
  self
60
59
  end
61
60
 
61
+ def region; end
62
+
63
+ def in_region?(_region)
64
+ true
65
+ end
66
+
67
+ def in_current_region?
68
+ true
69
+ end
70
+
62
71
  def _dump(_depth)
63
- ''
72
+ ""
64
73
  end
65
74
 
66
75
  def self._load(_str)