switchman 1.13.3 → 2.2.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/app/models/switchman/shard.rb +712 -11
  3. data/db/migrate/20130328212039_create_switchman_shards.rb +2 -0
  4. data/db/migrate/20130328224244_create_default_shard.rb +3 -1
  5. data/db/migrate/20161206323434_add_back_default_string_limits_switchman.rb +2 -0
  6. data/db/migrate/20180828183945_add_default_shard_index.rb +3 -1
  7. data/db/migrate/20180828192111_add_timestamps_to_shards.rb +2 -0
  8. data/db/migrate/20190114212900_add_unique_name_indexes.rb +2 -0
  9. data/lib/switchman/action_controller/caching.rb +2 -0
  10. data/lib/switchman/active_record/abstract_adapter.rb +6 -4
  11. data/lib/switchman/active_record/association.rb +45 -16
  12. data/lib/switchman/active_record/attribute_methods.rb +43 -17
  13. data/lib/switchman/active_record/base.rb +60 -20
  14. data/lib/switchman/active_record/batches.rb +2 -0
  15. data/lib/switchman/active_record/calculations.rb +5 -2
  16. data/lib/switchman/active_record/connection_handler.rb +48 -25
  17. data/lib/switchman/active_record/connection_pool.rb +19 -15
  18. data/lib/switchman/active_record/finder_methods.rb +2 -0
  19. data/lib/switchman/active_record/log_subscriber.rb +10 -12
  20. data/lib/switchman/active_record/migration.rb +48 -2
  21. data/lib/switchman/active_record/model_schema.rb +3 -1
  22. data/lib/switchman/active_record/persistence.rb +13 -2
  23. data/lib/switchman/active_record/postgresql_adapter.rb +149 -139
  24. data/lib/switchman/active_record/predicate_builder.rb +3 -1
  25. data/lib/switchman/active_record/query_cache.rb +19 -107
  26. data/lib/switchman/active_record/query_methods.rb +25 -3
  27. data/lib/switchman/active_record/reflection.rb +21 -8
  28. data/lib/switchman/active_record/relation.rb +66 -10
  29. data/lib/switchman/active_record/spawn_methods.rb +2 -0
  30. data/lib/switchman/active_record/statement_cache.rb +6 -25
  31. data/lib/switchman/active_record/table_definition.rb +4 -2
  32. data/lib/switchman/active_record/type_caster.rb +2 -0
  33. data/lib/switchman/active_record/where_clause_factory.rb +2 -0
  34. data/lib/switchman/active_support/cache.rb +18 -0
  35. data/lib/switchman/arel.rb +2 -0
  36. data/lib/switchman/call_super.rb +2 -0
  37. data/lib/switchman/connection_pool_proxy.rb +44 -22
  38. data/lib/switchman/database_server.rb +34 -19
  39. data/lib/switchman/default_shard.rb +3 -0
  40. data/lib/switchman/engine.rb +20 -15
  41. data/lib/switchman/environment.rb +2 -0
  42. data/lib/switchman/errors.rb +2 -0
  43. data/lib/switchman/{shackles → guard_rail}/relation.rb +7 -5
  44. data/lib/switchman/{shackles.rb → guard_rail.rb} +6 -4
  45. data/lib/switchman/open4.rb +2 -0
  46. data/lib/switchman/r_spec_helper.rb +9 -7
  47. data/lib/switchman/rails.rb +2 -0
  48. data/lib/switchman/schema_cache.rb +11 -1
  49. data/lib/switchman/sharded_instrumenter.rb +3 -1
  50. data/lib/switchman/standard_error.rb +3 -1
  51. data/lib/switchman/test_helper.rb +7 -11
  52. data/lib/switchman/version.rb +3 -1
  53. data/lib/switchman.rb +5 -1
  54. data/lib/tasks/switchman.rake +24 -17
  55. metadata +53 -26
  56. data/app/models/switchman/shard_internal.rb +0 -714
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class CreateSwitchmanShards < ActiveRecord::Migration[4.2]
2
4
  def change
3
5
  create_table :switchman_shards do |t|
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class CreateDefaultShard < ActiveRecord::Migration[4.2]
2
4
  def up
3
5
  unless Switchman::Shard.default.is_a?(Switchman::Shard)
4
6
  Switchman::Shard.reset_column_information
5
7
  Switchman::Shard.create!(:default => true)
6
- Switchman::Shard.default(true)
8
+ Switchman::Shard.default(reload: true)
7
9
  end
8
10
  end
9
11
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AddBackDefaultStringLimitsSwitchman < ActiveRecord::Migration[4.2]
2
4
  def up
3
5
  add_string_limit_if_missing :switchman_shards, :name
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AddDefaultShardIndex < ActiveRecord::Migration[4.2]
2
4
  def change
3
5
  Switchman::Shard.where(default: nil).update_all(default: false)
@@ -8,6 +10,6 @@ class AddDefaultShardIndex < ActiveRecord::Migration[4.2]
8
10
  else
9
11
  {}
10
12
  end
11
- add_index :switchman_shards, :default, options
13
+ add_index :switchman_shards, :default, **options
12
14
  end
13
15
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AddTimestampsToShards < ActiveRecord::Migration[4.2]
2
4
  def change
3
5
  add_timestamps :switchman_shards, null: true
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AddUniqueNameIndexes < ActiveRecord::Migration[4.2]
2
4
  def change
3
5
  add_index :switchman_shards, [:database_server_id, :name], unique: true
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Switchman
2
4
  module ActionController
3
5
  module Caching
@@ -1,11 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'switchman/sharded_instrumenter'
2
4
 
3
5
  module Switchman
4
6
  module ActiveRecord
5
7
  module AbstractAdapter
6
8
  module ForeignKeyCheck
7
- def add_column(table, name, type, options = {})
8
- Engine.foreign_key_check(name, type, options)
9
+ def add_column(table, name, type, limit: nil, **)
10
+ Engine.foreign_key_check(name, type, limit: limit)
9
11
  super
10
12
  end
11
13
  end
@@ -27,8 +29,8 @@ module Switchman
27
29
  quote_table_name(name)
28
30
  end
29
31
 
30
- def use_qualified_names?
31
- false
32
+ def schema_migration
33
+ ::ActiveRecord::SchemaMigration
32
34
  end
33
35
 
34
36
  protected
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Switchman
2
4
  module ActiveRecord
3
5
  module Association
@@ -30,17 +32,14 @@ module Switchman
30
32
  end
31
33
 
32
34
  module CollectionAssociation
33
- method = ::Rails.version < '5.1' ? :get_records : :find_target
34
- module_eval <<-RUBY, __FILE__, __LINE__ + 1
35
- def #{method}
36
- shards = reflection.options[:multishard] && owner.respond_to?(:associated_shards) ? owner.associated_shards : [shard]
37
- # activate both the owner and the target's shard category, so that Reflection#join_id_for,
38
- # when called for the owner, will be returned relative to shard the query will execute on
39
- Shard.with_each_shard(shards, [klass.shard_category, owner.class.shard_category].uniq) do
40
- super
41
- end
35
+ def find_target
36
+ shards = reflection.options[:multishard] && owner.respond_to?(:associated_shards) ? owner.associated_shards : [shard]
37
+ # activate both the owner and the target's shard category, so that Reflection#join_id_for,
38
+ # when called for the owner, will be returned relative to shard the query will execute on
39
+ Shard.with_each_shard(shards, [klass.shard_category, owner.class.shard_category].uniq) do
40
+ super
42
41
  end
43
- RUBY
42
+ end
44
43
  end
45
44
 
46
45
  module BelongsToAssociation
@@ -76,16 +75,36 @@ module Switchman
76
75
 
77
76
  module Preloader
78
77
  module Association
79
- if ::Rails.version >= "5.2"
78
+ if ::Rails.version >= "5.2" and ::Rails.version < "6.0"
80
79
  def run(preloader)
81
- # TODO - can move associated_records_by_owner into this after 5.1 is gonzo
82
- associated_records_by_owner(preloader).each do |owner, records|
80
+ associated_records_by_owner.each do |owner, records|
83
81
  associate_records_to_owner(owner, records)
84
82
  end
85
83
  end
86
84
  end
87
85
 
86
+ if ::Rails.version >= "6.0"
87
+ # Copypasta from Activerecord but with added global_id_for goodness.
88
+ def records_for(ids)
89
+ scope.where(association_key_name => ids).load do |record|
90
+ global_key = if model.shard_category == :unsharded
91
+ convert_key(record[association_key_name])
92
+ else
93
+ Shard.global_id_for(record[association_key_name], record.shard)
94
+ end
95
+ owner = owners_by_key[global_key.to_s].first
96
+ association = owner.association(reflection.name)
97
+ association.set_inverse_instance(record)
98
+ end
99
+ end
100
+
101
+ def records_by_owner
102
+ associated_records_by_owner
103
+ end
104
+ end
105
+
88
106
  def associated_records_by_owner(preloader = nil)
107
+ return @associated_records_by_owner if defined?(@associated_records_by_owner)
89
108
  owners_map = owners_by_key
90
109
 
91
110
  if klass.nil? || owners_map.empty?
@@ -133,10 +152,13 @@ module Switchman
133
152
  records.flatten!
134
153
  end
135
154
 
155
+ # This ivar may look unused, but remember this is an extension of
156
+ # rails' AR::Associations::Preloader::Association class. It gets used
157
+ # by that class (and its subclasses).
136
158
  @preloaded_records = records
137
159
 
138
160
  # Each record may have multiple owners, and vice-versa
139
- records_by_owner = owners.each_with_object({}) do |owner,h|
161
+ @associated_records_by_owner = owners.each_with_object({}) do |owner,h|
140
162
  h[owner] = []
141
163
  end
142
164
  records.each do |record|
@@ -145,10 +167,10 @@ module Switchman
145
167
 
146
168
  owners_map[owner_key.to_s].each do |owner|
147
169
  owner.association(reflection.name).set_inverse_instance(record)
148
- records_by_owner[owner] << record
170
+ @associated_records_by_owner[owner] << record
149
171
  end
150
172
  end
151
- records_by_owner
173
+ @associated_records_by_owner
152
174
  end
153
175
 
154
176
  def owners_by_key
@@ -183,6 +205,13 @@ module Switchman
183
205
  (record.has_attribute?(reflection.foreign_key) && record.send(reflection.foreign_key) != key) || # have to use send instead of [] because sharding
184
206
  record.attribute_changed?(reflection.foreign_key)
185
207
  end
208
+
209
+ def save_belongs_to_association(reflection)
210
+ # this seems counter-intuitive, but the autosave code will assign to attribute bypassing switchman,
211
+ # after reading the id attribute _without_ bypassing switchman. So we need Shard.current for the
212
+ # category of the associated record to match Shard.current for the category of self
213
+ shard.activate(shard_category_for_reflection(reflection)) { super }
214
+ end
186
215
  end
187
216
  end
188
217
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Switchman
2
4
  module ActiveRecord
3
5
  module AttributeMethods
@@ -36,11 +38,15 @@ module Switchman
36
38
  def define_method_global_attribute(attr_name)
37
39
  if sharded_column?(attr_name)
38
40
  generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
39
- def __temp__
41
+ def __temp_global_attribute__
42
+ raw_value = original_#{attr_name}
43
+ return nil if raw_value.nil?
44
+ return raw_value if raw_value > Shard::IDS_PER_SHARD
45
+
40
46
  Shard.global_id_for(original_#{attr_name}, shard)
41
47
  end
42
- alias_method 'global_#{attr_name}', :__temp__
43
- undef_method :__temp__
48
+ alias_method 'global_#{attr_name}', :__temp_global_attribute__
49
+ undef_method :__temp_global_attribute__
44
50
  RUBY
45
51
  else
46
52
  define_method_unsharded_column(attr_name, 'global')
@@ -50,17 +56,22 @@ module Switchman
50
56
  def define_method_local_attribute(attr_name)
51
57
  if sharded_column?(attr_name)
52
58
  generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
53
- def __temp__
54
- Shard.local_id_for(original_#{attr_name}).first
59
+ def __temp_local_attribute__
60
+ raw_value = original_#{attr_name}
61
+ return nil if raw_value.nil?
62
+ return raw_value % Shard::IDS_PER_SHARD
55
63
  end
56
- alias_method 'local_#{attr_name}', :__temp__
57
- undef_method :__temp__
64
+ alias_method 'local_#{attr_name}', :__temp_local_attribute__
65
+ undef_method :__temp_local_attribute__
58
66
  RUBY
59
67
  else
60
68
  define_method_unsharded_column(attr_name, 'local')
61
69
  end
62
70
  end
63
71
 
72
+ # see also Base#shard_category_for_reflection
73
+ # the difference being this will output static strings for the common cases, making them
74
+ # more performant
64
75
  def shard_category_code_for_reflection(reflection)
65
76
  if reflection
66
77
  if reflection.options[:polymorphic]
@@ -89,18 +100,33 @@ module Switchman
89
100
  # rename the original method to original_
90
101
  alias_method 'original_#{attr_name}', '#{attr_name}'
91
102
  # and replace with one that transposes the id
92
- def __temp__
93
- Shard.relative_id_for(original_#{attr_name}, shard, Shard.current(#{shard_category_code_for_reflection(reflection)}))
103
+ def __temp_relative_attribute__
104
+ raw_value = original_#{attr_name}
105
+ return nil if raw_value.nil?
106
+
107
+ abs_raw_value = raw_value.abs
108
+ current_shard = Shard.current(#{shard_category_code_for_reflection(reflection)})
109
+ same_shard = shard == current_shard
110
+ return raw_value if same_shard && abs_raw_value < Shard::IDS_PER_SHARD
111
+
112
+ value_shard_id = abs_raw_value / Shard::IDS_PER_SHARD
113
+ # this is a stupid case when someone stuffed a global id for the current shard in instead
114
+ # of a local id
115
+ return raw_value % Shard::IDS_PER_SHARD if value_shard_id == current_shard.id
116
+ return raw_value if !same_shard && abs_raw_value > Shard::IDS_PER_SHARD
117
+ return shard.global_id_for(raw_value) if !same_shard && abs_raw_value < Shard::IDS_PER_SHARD
118
+
119
+ Shard.relative_id_for(raw_value, shard, current_shard)
94
120
  end
95
- alias_method '#{attr_name}', :__temp__
96
- undef_method :__temp__
121
+ alias_method '#{attr_name}', :__temp_relative_attribute__
122
+ undef_method :__temp_relative_attribute__
97
123
 
98
124
  alias_method 'original_#{attr_name}=', '#{attr_name}='
99
- def __temp__(new_value)
125
+ def __temp_relative_attribute_assignment__(new_value)
100
126
  self.original_#{attr_name} = Shard.relative_id_for(new_value, Shard.current(#{shard_category_code_for_reflection(reflection)}), shard)
101
127
  end
102
- alias_method '#{attr_name}=', :__temp__
103
- undef_method :__temp__
128
+ alias_method '#{attr_name}=', :__temp_relative_attribute_assignment__
129
+ undef_method :__temp_relative_attribute_assignment__
104
130
  RUBY
105
131
  else
106
132
  define_method_unsharded_column(attr_name, 'global')
@@ -110,11 +136,11 @@ module Switchman
110
136
  def define_method_unsharded_column(attr_name, prefix)
111
137
  return if columns_hash["#{prefix}_#{attr_name}"]
112
138
  generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
113
- def __temp__
139
+ def __temp_unsharded_attribute__
114
140
  raise NoMethodError, "undefined method `#{prefix}_#{attr_name}'; are you missing an association?"
115
141
  end
116
- alias_method '#{prefix}_#{attr_name}', :__temp__
117
- undef_method :__temp__
142
+ alias_method '#{prefix}_#{attr_name}', :__temp_unsharded_attribute__
143
+ undef_method :__temp_unsharded_attribute__
118
144
  RUBY
119
145
  end
120
146
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Switchman
2
4
  module ActiveRecord
3
5
  module Base
@@ -31,20 +33,20 @@ module Switchman
31
33
  @integral_id
32
34
  end
33
35
 
34
- def transaction(*args)
36
+ def transaction(**)
35
37
  if self != ::ActiveRecord::Base && current_scope
36
38
  current_scope.activate do
37
39
  db = Shard.current(shard_category).database_server
38
- if ::Shackles.environment != db.shackles_environment
39
- db.unshackle { super }
40
+ if ::GuardRail.environment != db.guard_rail_environment
41
+ db.unguard { super }
40
42
  else
41
43
  super
42
44
  end
43
45
  end
44
46
  else
45
47
  db = Shard.current(shard_category).database_server
46
- if ::Shackles.environment != db.shackles_environment
47
- db.unshackle { super }
48
+ if ::GuardRail.environment != db.guard_rail_environment
49
+ db.unguard { super }
48
50
  else
49
51
  super
50
52
  end
@@ -68,21 +70,29 @@ module Switchman
68
70
  result
69
71
  end
70
72
  end
71
- end
72
73
 
73
- def self.included(klass)
74
- klass.extend(ClassMethods)
75
- klass.set_callback(:initialize, :before) do
76
- unless @shard
77
- if self.class.sharded_primary_key?
78
- @shard = Shard.shard_for(self[self.class.primary_key], Shard.current(self.class.shard_category))
79
- else
80
- @shard = Shard.current(self.class.shard_category)
74
+ def clear_query_caches_for_current_thread
75
+ ::ActiveRecord::Base.connection_handlers.each_value do |handler|
76
+ handler.connection_pool_list.each do |pool|
77
+ pool.connection(switch_shard: false).clear_query_cache if pool.active_connection?
81
78
  end
82
79
  end
83
80
  end
84
81
  end
85
82
 
83
+ def self.prepended(klass)
84
+ klass.singleton_class.prepend(ClassMethods)
85
+ end
86
+
87
+ def _run_initialize_callbacks
88
+ @shard ||= if self.class.sharded_primary_key?
89
+ Shard.shard_for(self[self.class.primary_key], Shard.current(self.class.shard_category))
90
+ else
91
+ Shard.current(self.class.shard_category)
92
+ end
93
+ super
94
+ end
95
+
86
96
  def shard
87
97
  @shard || Shard.current(self.class.shard_category) || Shard.default
88
98
  end
@@ -97,14 +107,14 @@ module Switchman
97
107
  end
98
108
  end
99
109
 
100
- def save(*args)
110
+ def save(*, **)
101
111
  @shard_set_in_stone = true
102
- self.class.shard(shard, :implicit).scoping { super }
112
+ (self.class.current_scope || self.class.default_scoped).shard(shard, :implicit).scoping { super }
103
113
  end
104
114
 
105
- def save!(*args)
115
+ def save!(*, **)
106
116
  @shard_set_in_stone = true
107
- self.class.shard(shard, :implicit).scoping { super }
117
+ (self.class.current_scope || self.class.default_scoped).shard(shard, :implicit).scoping { super }
108
118
  end
109
119
 
110
120
  def destroy
@@ -120,9 +130,9 @@ module Switchman
120
130
  result
121
131
  end
122
132
 
123
- def transaction(options={}, &block)
133
+ def transaction(**kwargs, &block)
124
134
  shard.activate(self.class.shard_category) do
125
- self.class.transaction(options, &block)
135
+ self.class.transaction(**kwargs, &block)
126
136
  end
127
137
  end
128
138
 
@@ -146,6 +156,36 @@ module Switchman
146
156
  # do this the Rails 4.2 way, so that if Shard.current != self.shard, the id gets transposed
147
157
  self.class.connection.quote(id)
148
158
  end
159
+
160
+ def update_columns(*)
161
+ db = shard.database_server
162
+ if ::GuardRail.environment != db.guard_rail_environment
163
+ return db.unguard { super }
164
+ else
165
+ super
166
+ end
167
+ end
168
+
169
+ protected
170
+
171
+ # see also AttributeMethods#shard_category_code_for_reflection
172
+ def shard_category_for_reflection(reflection)
173
+ if reflection
174
+ if reflection.options[:polymorphic]
175
+ begin
176
+ read_attribute(reflection.foreign_type)&.constantize&.shard_category || :primary
177
+ rescue NameError
178
+ # in case someone is abusing foreign_type to not point to an actual class
179
+ :primary
180
+ end
181
+ else
182
+ # otherwise we can just return a symbol for the statically known type of the association
183
+ reflection.klass.shard_category
184
+ end
185
+ else
186
+ shard_category
187
+ end
188
+ end
149
189
  end
150
190
  end
151
191
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Switchman
2
4
  module ActiveRecord
3
5
  module Batches
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Switchman
2
4
  module ActiveRecord
3
5
  module Calculations
@@ -49,7 +51,7 @@ module Switchman
49
51
 
50
52
  def calculate_simple_average(column_name, distinct)
51
53
  # See activerecord#execute_simple_calculation
52
- relation = reorder(nil)
54
+ relation = except(:order)
53
55
  column = aggregate_column(column_name)
54
56
  relation.select_values = [operation_over_aggregate_column(column, "average", distinct).as("average"),
55
57
  operation_over_aggregate_column(column, "count", distinct).as("count")]
@@ -121,7 +123,8 @@ module Switchman
121
123
  group_fields = group_attrs
122
124
  end
123
125
 
124
- group_aliases = group_fields.map { |field| column_alias_for(field) }
126
+ # to_s is because Rails 5 returns a string but Rails 6 returns a symbol.
127
+ group_aliases = group_fields.map { |field| column_alias_for(field.downcase.to_s).to_s }
125
128
  group_columns = group_aliases.zip(group_fields).map { |aliaz, field|
126
129
  [aliaz, type_for(field), column_name_for(field)]
127
130
  }
@@ -1,11 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'switchman/connection_pool_proxy'
2
4
 
3
5
  module Switchman
4
6
  module ActiveRecord
5
7
  module ConnectionHandler
6
8
  def self.make_sharing_automagic(config, shard = Shard.current)
7
- key = config[:adapter] == 'postgresql' ? :schema_search_path : :database
8
-
9
9
  # only load the shard name from the db if we have to
10
10
  if !config[:shard_name]
11
11
  # we may not be able to connect to this shard yet, cause it might be an empty database server
@@ -15,15 +15,13 @@ module Switchman
15
15
 
16
16
  config[:shard_name] ||= shard_name
17
17
  end
18
-
19
- if !config[key] || config[key] == shard_name
20
- # this may truncate the schema_search_path if it was not specified in database.yml
21
- # but that's what our old behavior was anyway, so I guess it's okay
22
- config[key] = '%{shard_name}'
23
- end
24
18
  end
25
19
 
26
20
  def establish_connection(spec)
21
+ # Just skip establishing a sharded connection if sharding isn't loaded; we'll do it again later
22
+ # This only can happen when loading ActiveRecord::Base; after everything is loaded Shard will
23
+ # be defined and this will actually establish a connection
24
+ return unless defined?(Shard)
27
25
  pool = super
28
26
 
29
27
  # this is the first place that the adapter would have been required; but now we
@@ -39,19 +37,27 @@ module Switchman
39
37
  # to sharding will recurse onto itself trying to access column information
40
38
  Shard.default
41
39
 
40
+ config = pool.spec.config
42
41
  # automatically change config to allow for sharing connections with simple config
43
- config = ::Rails.version < '5.1' ? spec.config : pool.spec.config
44
42
  ConnectionHandler.make_sharing_automagic(config)
45
43
  ConnectionHandler.make_sharing_automagic(Shard.default.database_server.config)
46
44
 
47
- if ::Rails.version < '5.1'
48
- ::ActiveRecord::Base.configurations[::Rails.env] = spec.instance_variable_get(:@config).stringify_keys
49
- else
45
+ if ::Rails.version < '6.0'
50
46
  ::ActiveRecord::Base.configurations[::Rails.env] = config.stringify_keys
47
+ else
48
+ # Adopted from the deprecated code that currently lives in rails proper
49
+ remaining_configs = ::ActiveRecord::Base.configurations.configurations.reject { |db_config| db_config.env_name == ::Rails.env }
50
+ new_config = ::ActiveRecord::DatabaseConfigurations.new(::Rails.env => config.stringify_keys).configurations
51
+ new_configs = remaining_configs + new_config
52
+
53
+ ::ActiveRecord::Base.configurations = new_configs
51
54
  end
55
+ else
56
+ # this is probably wrong now
57
+ Shard.default.remove_instance_variable(:@name) if Shard.default.instance_variable_defined?(:@name)
52
58
  end
53
59
 
54
- @shard_connection_pools ||= { [:master, Shard.default.database_server.shareable? ? ::Rails.env : Shard.default] => pool}
60
+ @shard_connection_pools ||= { [:primary, Shard.default.database_server.shareable? ? ::Rails.env : Shard.default] => pool}
55
61
 
56
62
  category = pool.spec.name.to_sym
57
63
  proxy = ConnectionPoolProxy.new(category,
@@ -60,13 +66,13 @@ module Switchman
60
66
  owner_to_pool[pool.spec.name] = proxy
61
67
 
62
68
  if first_time
63
- if Shard.default.database_server.config[:prefer_slave]
64
- Shard.default.database_server.shackle!
69
+ if Shard.default.database_server.config[:prefer_secondary]
70
+ Shard.default.database_server.guard!
65
71
  end
66
72
 
67
- if Shard.default.is_a?(DefaultShard) && Shard.default.database_server.config[:slave]
68
- Shard.default.database_server.shackle!
69
- Shard.default(true)
73
+ if Shard.default.is_a?(DefaultShard) && Shard.default.database_server.config[:secondary]
74
+ Shard.default.database_server.guard!
75
+ Shard.default(reload: true)
70
76
  end
71
77
  end
72
78
 
@@ -106,8 +112,20 @@ module Switchman
106
112
  end
107
113
 
108
114
  def remove_connection(spec_name)
115
+ # also remove pools based on the same spec name that are for shard category purposes
116
+ # can't just use delete_if, because it's a Concurrent::Map, not a Hash
117
+ owner_to_pool.keys.each do |k|
118
+ next if k == spec_name
119
+
120
+ v = owner_to_pool[k]
121
+ owner_to_pool.delete(k) if v.is_a?(ConnectionPoolProxy) && v.default_pool.spec.name == spec_name
122
+ end
123
+
124
+ # unwrap the pool from inside a ConnectionPoolProxy
109
125
  pool = owner_to_pool[spec_name]
110
126
  owner_to_pool[spec_name] = pool.default_pool if pool.is_a?(ConnectionPoolProxy)
127
+
128
+ # now let Rails do its thing with the data type it expects
111
129
  super
112
130
  end
113
131
 
@@ -122,16 +140,21 @@ module Switchman
122
140
  else
123
141
  ancestor_pool.spec
124
142
  end
125
- spec = spec.to_hash if ::Rails.version >= '5.1'
126
- pool = establish_connection spec
127
- pool.instance_variable_set(:@schema_cache, ancestor_pool.schema_cache) if ancestor_pool.schema_cache
128
- pool
129
- elsif spec_name != "primary"
143
+ # avoid copying "duplicate" pools that implement shard categories.
144
+ # they'll have a spec.name of primary, but a spec_name of something else, like unsharded
145
+ if spec.name == spec_name
146
+ pool = establish_connection(spec.to_hash)
147
+ pool.instance_variable_set(:@schema_cache, ancestor_pool.schema_cache) if ancestor_pool.schema_cache
148
+ next pool
149
+ end
150
+ end
151
+
152
+ if spec_name != "primary"
130
153
  primary_pool = retrieve_connection_pool("primary")
131
154
  if primary_pool.is_a?(ConnectionPoolProxy)
132
155
  pool = ConnectionPoolProxy.new(spec_name.to_sym, primary_pool.default_pool, @shard_connection_pools)
133
- pool.schema_cache.copy_values(primary_pool.schema_cache)
134
- pool
156
+ pool.schema_cache.copy_references(primary_pool.schema_cache)
157
+ owner_to_pool[spec_name] = pool
135
158
  else
136
159
  primary_pool
137
160
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'switchman/errors'
2
4
 
3
5
  module Switchman
@@ -32,10 +34,10 @@ module Switchman
32
34
  conn
33
35
  end
34
36
 
35
- def connection
36
- conn = super
37
+ def connection(switch_shard: true)
38
+ conn = super()
37
39
  raise NonExistentShardError if shard.new_record?
38
- switch_database(conn) if conn.shard != self.shard
40
+ switch_database(conn) if conn.shard != self.shard && switch_shard
39
41
  conn
40
42
  end
41
43
 
@@ -47,6 +49,20 @@ module Switchman
47
49
  end
48
50
  end
49
51
 
52
+ def remove_shard!(shard)
53
+ synchronize do
54
+ # The shard might be currently active, so we need to update our own shard
55
+ if self.shard == shard
56
+ self.shard = Shard.default
57
+ end
58
+ # Update out any connections that may be using this shard
59
+ @connections.each do |conn|
60
+ # This will also update the connection's shard to the default shard
61
+ switch_database(conn) if conn.shard == shard
62
+ end
63
+ end
64
+ end
65
+
50
66
  def clear_idle_connections!(since_when)
51
67
  synchronize do
52
68
  @connections.reject! do |conn|
@@ -70,18 +86,6 @@ module Switchman
70
86
  end
71
87
 
72
88
  spec.config[:shard_name] = self.shard.name
73
- case conn.adapter_name
74
- when 'MySQL', 'Mysql2'
75
- conn.execute("USE #{spec.config[:database]}")
76
- when 'PostgreSQL'
77
- if conn.schema_search_path != spec.config[:schema_search_path]
78
- conn.schema_search_path = spec.config[:schema_search_path]
79
- end
80
- when 'SQLite'
81
- # This is an artifact of the adapter modifying the path to be an absolute path when it is instantiated; just let it slide
82
- else
83
- raise("Cannot switch databases on same DatabaseServer with adapter type: #{conn.adapter_name}. Limit one Shard per DatabaseServer.")
84
- end
85
89
  conn.shard = shard
86
90
  end
87
91