switchman 3.1.0 → 3.5.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +15 -14
  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/active_record/associations.rb +97 -17
  6. data/lib/switchman/active_record/attribute_methods.rb +72 -43
  7. data/lib/switchman/active_record/base.rb +107 -21
  8. data/lib/switchman/active_record/calculations.rb +37 -33
  9. data/lib/switchman/active_record/connection_pool.rb +21 -2
  10. data/lib/switchman/active_record/database_configurations.rb +12 -7
  11. data/lib/switchman/active_record/finder_methods.rb +1 -1
  12. data/lib/switchman/active_record/log_subscriber.rb +2 -2
  13. data/lib/switchman/active_record/migration.rb +35 -8
  14. data/lib/switchman/active_record/persistence.rb +8 -0
  15. data/lib/switchman/active_record/postgresql_adapter.rb +11 -10
  16. data/lib/switchman/active_record/query_cache.rb +1 -1
  17. data/lib/switchman/active_record/query_methods.rb +172 -132
  18. data/lib/switchman/active_record/relation.rb +21 -11
  19. data/lib/switchman/active_record/spawn_methods.rb +2 -2
  20. data/lib/switchman/active_record/statement_cache.rb +9 -5
  21. data/lib/switchman/active_record/tasks/database_tasks.rb +1 -1
  22. data/lib/switchman/active_record/test_fixtures.rb +19 -16
  23. data/lib/switchman/active_support/cache.rb +4 -1
  24. data/lib/switchman/arel.rb +6 -6
  25. data/lib/switchman/call_super.rb +8 -2
  26. data/lib/switchman/database_server.rb +21 -26
  27. data/lib/switchman/default_shard.rb +3 -3
  28. data/lib/switchman/engine.rb +33 -18
  29. data/lib/switchman/environment.rb +2 -2
  30. data/lib/switchman/errors.rb +13 -0
  31. data/lib/switchman/guard_rail/relation.rb +2 -1
  32. data/lib/switchman/parallel.rb +2 -2
  33. data/lib/switchman/r_spec_helper.rb +10 -10
  34. data/lib/switchman/shard.rb +49 -32
  35. data/lib/switchman/sharded_instrumenter.rb +5 -1
  36. data/lib/switchman/shared_schema_cache.rb +11 -0
  37. data/lib/switchman/test_helper.rb +1 -1
  38. data/lib/switchman/version.rb +1 -1
  39. data/lib/switchman.rb +10 -4
  40. data/lib/tasks/switchman.rake +42 -39
  41. metadata +24 -9
@@ -22,16 +22,20 @@ module Switchman
22
22
  @integral_id
23
23
  end
24
24
 
25
- def transaction(**)
26
- if self != ::ActiveRecord::Base && current_scope
27
- current_scope.activate do
28
- db = Shard.current(connection_class_for_self).database_server
29
- db.unguard { super }
25
+ %w[transaction insert_all upsert_all].each do |method|
26
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
27
+ def #{method}(*, **)
28
+ if self != ::ActiveRecord::Base && current_scope
29
+ current_scope.activate do
30
+ db = Shard.current(connection_class_for_self).database_server
31
+ db.unguard { super }
32
+ end
33
+ else
34
+ db = Shard.current(connection_class_for_self).database_server
35
+ db.unguard { super }
36
+ end
30
37
  end
31
- else
32
- db = Shard.current(connection_class_for_self).database_server
33
- db.unguard { super }
34
- end
38
+ RUBY
35
39
  end
36
40
 
37
41
  def reset_column_information
@@ -63,7 +67,10 @@ module Switchman
63
67
  end
64
68
 
65
69
  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
70
+ if config_or_env.is_a?(Symbol) && config_or_env != ::Rails.env.to_sym
71
+ raise ArgumentError,
72
+ "establish connection cannot be used on the non-current shard/role"
73
+ end
67
74
 
68
75
  # Ensure we don't randomly surprise change the connection parms associated with a shard/role
69
76
  config_or_env = nil if config_or_env == ::Rails.env.to_sym
@@ -78,9 +85,15 @@ module Switchman
78
85
  end
79
86
 
80
87
  def connected_to_stack
81
- return super if ::Rails.version < '7.0' ? Thread.current.thread_variable?(:ar_connected_to_stack) : ::ActiveSupport::IsolatedExecutionState.key?(:active_record_connected_to_stack)
88
+ has_own_stack = if ::Rails.version < "7.0"
89
+ Thread.current.thread_variable?(:ar_connected_to_stack)
90
+ else
91
+ ::ActiveSupport::IsolatedExecutionState.key?(:active_record_connected_to_stack)
92
+ end
82
93
 
83
94
  ret = super
95
+ return ret if has_own_stack
96
+
84
97
  DatabaseServer.guard_servers
85
98
  ret
86
99
  end
@@ -92,10 +105,13 @@ module Switchman
92
105
  sharded_role = nil
93
106
  connected_to_stack.reverse_each do |hash|
94
107
  shard_role = hash.dig(:shard_roles, target_shard)
95
- if shard_role && (hash[:klasses].include?(::ActiveRecord::Base) || hash[:klasses].include?(connection_class_for_self))
96
- sharded_role = shard_role
97
- break
108
+ unless shard_role &&
109
+ (hash[:klasses].include?(::ActiveRecord::Base) || hash[:klasses].include?(connection_class_for_self))
110
+ next
98
111
  end
112
+
113
+ sharded_role = shard_role
114
+ break
99
115
  end
100
116
  # Allow a shard-specific role to be reverted to regular inheritance
101
117
  return sharded_role if sharded_role && sharded_role != :_switchman_inherit
@@ -115,13 +131,15 @@ module Switchman
115
131
 
116
132
  def current_switchman_shard
117
133
  connected_to_stack.reverse_each do |hash|
118
- return hash[:switchman_shard] if hash[:switchman_shard] && hash[:klasses].include?(connection_class_for_self)
134
+ if hash[:switchman_shard] && hash[:klasses].include?(connection_class_for_self)
135
+ return hash[:switchman_shard]
136
+ end
119
137
  end
120
138
 
121
139
  Shard.default
122
140
  end
123
141
 
124
- if ::Rails.version < '7.0'
142
+ if ::Rails.version < "7.0"
125
143
  def connection_class_for_self
126
144
  connection_classes
127
145
  end
@@ -138,30 +156,86 @@ module Switchman
138
156
  else
139
157
  Shard.current(self.class.connection_class_for_self)
140
158
  end
159
+
160
+ @loaded_from_shard ||= Shard.current(self.class.connection_class_for_self)
161
+ readonly! if shadow_record? && !Switchman.config[:writable_shadow_records]
141
162
  super
142
163
  end
143
164
 
165
+ def shadow_record?
166
+ pkey = self[self.class.primary_key]
167
+ return false unless self.class.sharded_column?(self.class.primary_key) && pkey
168
+
169
+ pkey > Shard::IDS_PER_SHARD
170
+ end
171
+
172
+ def save_shadow_record(new_attrs: attributes, target_shard: Shard.current)
173
+ return if target_shard == shard
174
+
175
+ shadow_attrs = {}
176
+ new_attrs.each do |attr, value|
177
+ shadow_attrs[attr] = if self.class.sharded_column?(attr)
178
+ Shard.relative_id_for(value, shard, target_shard)
179
+ else
180
+ value
181
+ end
182
+ end
183
+ target_shard.activate do
184
+ self.class.upsert(shadow_attrs, unique_by: self.class.primary_key)
185
+ end
186
+ end
187
+
188
+ def destroy_shadow_records(target_shards: [Shard.current])
189
+ raise Errors::ShadowRecordError, "Cannot be called on a shadow record." if shadow_record?
190
+
191
+ unless self.class.sharded_column?(self.class.primary_key)
192
+ raise Errors::MethodUnsupportedForUnshardedTableError,
193
+ "Cannot be called on a record belonging to an unsharded table."
194
+ end
195
+
196
+ Array(target_shards).each do |target_shard|
197
+ next if target_shard == shard
198
+
199
+ target_shard.activate { self.class.where("id = ?", global_id).delete_all }
200
+ end
201
+ end
202
+
203
+ # Returns "the shard that this record was actually loaded from" , as
204
+ # opposed to "the shard this record belongs on", which might be
205
+ # different if this is a shadow record.
206
+ def loaded_from_shard
207
+ @loaded_from_shard || shard
208
+ end
209
+
144
210
  def shard
145
- @shard || Shard.current(self.class.connection_class_for_self) || Shard.default
211
+ @shard || fallback_shard
146
212
  end
147
213
 
148
214
  def shard=(new_shard)
149
215
  raise ::ActiveRecord::ReadOnlyRecord if !new_record? || @shard_set_in_stone
150
216
 
151
- return if shard == new_shard
217
+ if shard == new_shard
218
+ @loaded_from_shard = new_shard
219
+ return
220
+ end
152
221
 
153
222
  attributes.each do |attr, value|
154
223
  self[attr] = Shard.relative_id_for(value, shard, new_shard) if self.class.sharded_column?(attr)
155
224
  end
225
+ @loaded_from_shard = new_shard
156
226
  @shard = new_shard
157
227
  end
158
228
 
159
229
  def save(*, **)
230
+ raise Errors::ManuallyCreatedShadowRecordError if creating_shadow_record?
231
+
160
232
  @shard_set_in_stone = true
161
233
  super
162
234
  end
163
235
 
164
236
  def save!(*, **)
237
+ raise Errors::ManuallyCreatedShadowRecordError if creating_shadow_record?
238
+
165
239
  @shard_set_in_stone = true
166
240
  super
167
241
  end
@@ -193,7 +267,7 @@ module Switchman
193
267
  end
194
268
 
195
269
  def hash
196
- self.class.sharded_primary_key? ? self.class.hash ^ global_id.hash : super
270
+ self.class.sharded_primary_key? ? [self.class, global_id].hash : super
197
271
  end
198
272
 
199
273
  def to_param
@@ -214,8 +288,10 @@ module Switchman
214
288
 
215
289
  def id_for_database
216
290
  if self.class.sharded_primary_key?
217
- # It's an int, so so it's safe to just return it without passing it through anything else
218
- # In theory we should do `@attributes[@primary_key].type.serialize(id)`, but that seems to have surprising side-effects
291
+ # It's an int, so it's safe to just return it without passing it
292
+ # through anything else. In theory we should do
293
+ # `@attributes[@primary_key].type.serialize(id)`, but that seems to
294
+ # have surprising side-effects
219
295
  id
220
296
  else
221
297
  super
@@ -242,6 +318,16 @@ module Switchman
242
318
  self.class.connection_class_for_self
243
319
  end
244
320
  end
321
+
322
+ private
323
+
324
+ def fallback_shard
325
+ Shard.current(self.class.connection_class_for_self) || Shard.default
326
+ end
327
+
328
+ def creating_shadow_record?
329
+ new_record? && shadow_record?
330
+ end
245
331
  end
246
332
  end
247
333
  end
@@ -28,7 +28,7 @@ module Switchman
28
28
 
29
29
  def execute_simple_calculation(operation, column_name, distinct)
30
30
  operation = operation.to_s.downcase
31
- if operation == 'average'
31
+ if operation == "average"
32
32
  result = calculate_simple_average(column_name, distinct)
33
33
  else
34
34
  result = activate do |relation|
@@ -36,11 +36,11 @@ module Switchman
36
36
  end
37
37
  if result.is_a?(Array)
38
38
  case operation
39
- when 'count', 'sum'
39
+ when "count", "sum"
40
40
  result = result.sum
41
- when 'minimum'
41
+ when "minimum"
42
42
  result = result.min
43
- when 'maximum'
43
+ when "maximum"
44
44
  result = result.max
45
45
  end
46
46
  end
@@ -52,20 +52,20 @@ module Switchman
52
52
  # See activerecord#execute_simple_calculation
53
53
  relation = except(:order)
54
54
  column = aggregate_column(column_name)
55
- relation.select_values = [operation_over_aggregate_column(column, 'average', distinct).as('average'),
56
- operation_over_aggregate_column(column, 'count', distinct).as('count')]
55
+ relation.select_values = [operation_over_aggregate_column(column, "average", distinct).as("average"),
56
+ operation_over_aggregate_column(column, "count", distinct).as("count")]
57
57
 
58
58
  initial_results = relation.activate { |rel| klass.connection.select_all(rel) }
59
59
  if initial_results.is_a?(Array)
60
60
  initial_results.each do |r|
61
- r['average'] = type_cast_calculated_value_switchman(r['average'], column_name, 'average')
62
- r['count'] = type_cast_calculated_value_switchman(r['count'], column_name, 'count')
61
+ r["average"] = type_cast_calculated_value_switchman(r["average"], column_name, "average")
62
+ r["count"] = type_cast_calculated_value_switchman(r["count"], column_name, "count")
63
63
  end
64
- result = initial_results.map { |r| r['average'] * r['count'] }.sum / initial_results.map do |r|
65
- r['count']
66
- end.sum
64
+ result = initial_results.sum { |r| r["average"] * r["count"] } / initial_results.sum do |r|
65
+ r["count"]
66
+ end
67
67
  else
68
- result = type_cast_calculated_value_switchman(initial_results.first['average'], column_name, 'average')
68
+ result = type_cast_calculated_value_switchman(initial_results.first["average"], column_name, "average")
69
69
  end
70
70
  result
71
71
  end
@@ -90,7 +90,7 @@ module Switchman
90
90
  row[opts[:aggregate_alias]] = type_cast_calculated_value_switchman(
91
91
  row[opts[:aggregate_alias]], column_name, opts[:operation]
92
92
  )
93
- row['count'] = row['count'].to_i if opts[:operation] == 'average'
93
+ row["count"] = row["count"].to_i if opts[:operation] == "average"
94
94
 
95
95
  opts[:group_columns].each do |aliaz, _type, group_column_name|
96
96
  if opts[:associated] && (aliaz == opts[:group_aliases].first)
@@ -109,7 +109,7 @@ module Switchman
109
109
  private
110
110
 
111
111
  def type_cast_calculated_value_switchman(value, column_name, operation)
112
- if ::Rails.version < '7.0'
112
+ if ::Rails.version < "7.0"
113
113
  type_cast_calculated_value(value, operation) do |val|
114
114
  column = aggregate_column(column_name)
115
115
  type ||= column.try(:type_caster) ||
@@ -125,7 +125,7 @@ module Switchman
125
125
  end
126
126
 
127
127
  def column_name_for(field)
128
- field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last
128
+ field.respond_to?(:name) ? field.name.to_s : field.to_s.split(".").last
129
129
  end
130
130
 
131
131
  def grouped_calculation_options(operation, column_name, distinct)
@@ -135,7 +135,8 @@ module Switchman
135
135
  group_attrs = group_values
136
136
  if group_attrs.first.respond_to?(:to_sym)
137
137
  association = klass.reflect_on_association(group_attrs.first.to_sym)
138
- associated = group_attrs.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations
138
+ # only count belongs_to associations
139
+ associated = group_attrs.size == 1 && association && association.macro == :belongs_to
139
140
  group_fields = Array(associated ? association.foreign_key : group_attrs)
140
141
  else
141
142
  group_fields = group_attrs
@@ -146,18 +147,20 @@ module Switchman
146
147
  group_columns = group_aliases.zip(group_fields).map do |aliaz, field|
147
148
  [aliaz, type_for(field), column_name_for(field)]
148
149
  end
149
- opts.merge!(association: association, associated: associated,
150
- group_aliases: group_aliases, group_columns: group_columns,
150
+ opts.merge!(association: association,
151
+ associated: associated,
152
+ group_aliases: group_aliases,
153
+ group_columns: group_columns,
151
154
  group_fields: group_fields)
152
155
 
153
156
  opts
154
157
  end
155
158
 
156
159
  def aggregate_alias_for(operation, column_name)
157
- if operation == 'count' && column_name == :all
158
- 'count_all'
159
- elsif operation == 'average'
160
- 'average'
160
+ if operation == "count" && column_name == :all
161
+ "count_all"
162
+ elsif operation == "average"
163
+ "average"
161
164
  else
162
165
  column_alias_for("#{operation} #{column_name}")
163
166
  end
@@ -173,13 +176,14 @@ module Switchman
173
176
  opts[:distinct]
174
177
  ).as(opts[:aggregate_alias])
175
178
  ]
176
- if opts[:operation] == 'average'
179
+ if opts[:operation] == "average"
177
180
  # include count in average so we can recalculate the average
178
181
  # across all shards if needed
179
182
  select_values << operation_over_aggregate_column(
180
183
  aggregate_column(opts[:column_name]),
181
- 'count', opts[:distinct]
182
- ).as('count')
184
+ "count",
185
+ opts[:distinct]
186
+ ).as("count")
183
187
  end
184
188
 
185
189
  haves = having_clause.send(:predicates)
@@ -205,22 +209,22 @@ module Switchman
205
209
  key = key.first if key.size == 1
206
210
  value = row[opts[:aggregate_alias]]
207
211
 
208
- if opts[:operation] == 'average'
212
+ if opts[:operation] == "average"
209
213
  if result.key?(key)
210
214
  old_value, old_count = result[key]
211
- new_count = old_count + row['count']
212
- new_value = ((old_value * old_count) + (value * row['count'])) / new_count
215
+ new_count = old_count + row["count"]
216
+ new_value = ((old_value * old_count) + (value * row["count"])) / new_count
213
217
  result[key] = [new_value, new_count]
214
218
  else
215
- result[key] = [value, row['count']]
219
+ result[key] = [value, row["count"]]
216
220
  end
217
221
  elsif result.key?(key)
218
222
  case opts[:operation]
219
- when 'count', 'sum'
223
+ when "count", "sum"
220
224
  result[key] += value
221
- when 'minimum'
225
+ when "minimum"
222
226
  result[key] = value if value < result[key]
223
- when 'maximum'
227
+ when "maximum"
224
228
  result[key] = value if value > result[key]
225
229
  end
226
230
  else
@@ -228,7 +232,7 @@ module Switchman
228
232
  end
229
233
  end
230
234
 
231
- result.transform_values!(&:first) if opts[:operation] == 'average'
235
+ result.transform_values!(&:first) if opts[:operation] == "average"
232
236
 
233
237
  result
234
238
  end
@@ -3,6 +3,23 @@
3
3
  module Switchman
4
4
  module ActiveRecord
5
5
  module ConnectionPool
6
+ def get_schema_cache(connection)
7
+ self.schema_cache ||= SharedSchemaCache.get_schema_cache(connection)
8
+ self.schema_cache.connection = connection
9
+
10
+ self.schema_cache
11
+ end
12
+
13
+ # rubocop:disable Naming/AccessorMethodName override method
14
+ def set_schema_cache(cache)
15
+ schema_cache = get_schema_cache(cache.connection)
16
+
17
+ cache.instance_variables.each do |x|
18
+ schema_cache.instance_variable_set(x, cache.instance_variable_get(x))
19
+ end
20
+ end
21
+ # rubocop:enable Naming/AccessorMethodName override method
22
+
6
23
  def default_schema
7
24
  connection unless @schemas
8
25
  # default shard will not switch databases immediately, so it won't be set yet
@@ -31,7 +48,9 @@ module Switchman
31
48
  end
32
49
 
33
50
  def switch_database(conn)
34
- @schemas = conn.current_schemas if !@schemas && conn.adapter_name == 'PostgreSQL' && !current_shard.database_server.config[:shard_name]
51
+ if !@schemas && conn.adapter_name == "PostgreSQL" && !current_shard.database_server.config[:shard_name]
52
+ @schemas = conn.current_schemas
53
+ end
35
54
 
36
55
  conn.shard = current_shard
37
56
  end
@@ -39,7 +58,7 @@ module Switchman
39
58
  private
40
59
 
41
60
  def current_shard
42
- ::Rails.version < '7.0' ? connection_klass.current_switchman_shard : connection_class.current_switchman_shard
61
+ (::Rails.version < "7.0") ? connection_klass.current_switchman_shard : connection_class.current_switchman_shard
43
62
  end
44
63
 
45
64
  def tls_key
@@ -10,9 +10,9 @@ module Switchman
10
10
  def configs_for(include_replicas: false, name: nil, **)
11
11
  res = super
12
12
  if name && !include_replicas
13
- return nil unless name.end_with?('primary')
13
+ return nil unless name.end_with?("primary")
14
14
  elsif !include_replicas
15
- return res.select { |config| config.name.end_with?('primary') }
15
+ return res.select { |config| config.name.end_with?("primary") }
16
16
  end
17
17
  res
18
18
  end
@@ -29,19 +29,24 @@ module Switchman
29
29
  db_configs = configs.flat_map do |env_name, config|
30
30
  # It would be nice to do the auto-fallback that we want here, but we haven't
31
31
  # actually done that for years (or maybe ever) and it will be a big lift to get working
32
- roles = config.keys.select { |k| config[k].is_a?(Hash) || (config[k].is_a?(Array) && config[k].all? { |ck| ck.is_a?(Hash) }) }
32
+ roles = config.keys.select do |k|
33
+ config[k].is_a?(Hash) || (config[k].is_a?(Array) && config[k].all?(Hash))
34
+ end
33
35
  base_config = config.except(*roles)
34
36
 
35
37
  name = "#{env_name}/primary"
36
- name = 'primary' if env_name == default_env
38
+ name = "primary" if env_name == default_env
37
39
  base_db = build_db_config_from_raw_config(env_name, name, base_config)
38
40
  [base_db] + roles.map do |role|
39
- build_db_config_from_raw_config(env_name, "#{env_name}/#{role}",
40
- base_config.merge(config[role].is_a?(Array) ? config[role].first : config[role]))
41
+ build_db_config_from_raw_config(
42
+ env_name,
43
+ "#{env_name}/#{role}",
44
+ base_config.merge(config[role].is_a?(Array) ? config[role].first : config[role])
45
+ )
41
46
  end
42
47
  end
43
48
 
44
- db_configs << environment_url_config(default_env, 'primary', {}) unless db_configs.find(&:for_current_env?)
49
+ db_configs << environment_url_config(default_env, "primary", {}) unless db_configs.find(&:for_current_env?)
45
50
 
46
51
  merge_db_environment_variables(default_env, db_configs.compact)
47
52
  end
@@ -49,7 +49,7 @@ module Switchman
49
49
  relation = apply_join_dependency(eager_loading: false)
50
50
  return false if ::ActiveRecord::NullRelation === relation
51
51
 
52
- relation = relation.except(:select, :order).select('1 AS one').limit(1)
52
+ relation = relation.except(:select, :order).select("1 AS one").limit(1)
53
53
 
54
54
  case conditions
55
55
  when Array, Hash
@@ -14,14 +14,14 @@ module Switchman
14
14
 
15
15
  name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
16
16
  name = "CACHE #{name}" if payload[:cached]
17
- sql = payload[:sql].squeeze(' ')
17
+ sql = payload[:sql].squeeze(" ")
18
18
  binds = nil
19
19
  shard = payload[:shard]
20
20
  shard = " [#{shard[:database_server_id]}:#{shard[:id]} #{shard[:env]}]" if shard
21
21
 
22
22
  unless (payload[:binds] || []).empty?
23
23
  casted_params = type_casted_binds(payload[:type_casted_binds])
24
- binds = ' ' + payload[:binds].zip(casted_params).map do |attr, value|
24
+ binds = " " + payload[:binds].zip(casted_params).map do |attr, value|
25
25
  render_bind(attr, value)
26
26
  end.inspect
27
27
  end
@@ -14,19 +14,23 @@ module Switchman
14
14
 
15
15
  def connection
16
16
  conn = super
17
- ::ActiveRecord::Base.connection_pool.switch_database(conn) if conn.shard != ::ActiveRecord::Base.current_switchman_shard
17
+ if conn.shard != ::ActiveRecord::Base.current_switchman_shard
18
+ ::ActiveRecord::Base.connection_pool.switch_database(conn)
19
+ end
18
20
  conn
19
21
  end
20
22
  end
21
23
 
22
24
  module Migrator
23
- # significant change: just return MIGRATOR_SALT directly
24
- # especially if you're going through pgbouncer, the database
25
- # name you're accessing may not be consistent. it is NOT allowed
26
- # to run migrations against multiple shards in the same database
27
- # concurrently
25
+ # significant change: use the shard name instead of the database name
26
+ # in the lock id. Especially if you're going through pgbouncer, the
27
+ # database name you're accessing may not be consistent
28
28
  def generate_migrator_advisory_lock_id
29
- ::ActiveRecord::Migrator::MIGRATOR_SALT
29
+ db_name_hash = Zlib.crc32(Shard.current.name)
30
+ shard_name_hash = ::ActiveRecord::Migrator::MIGRATOR_SALT * db_name_hash
31
+ # Store in internalmetadata to allow other tools to be able to lock out migrations
32
+ ::ActiveRecord::InternalMetadata[:migrator_advisory_lock_id] = shard_name_hash
33
+ shard_name_hash
30
34
  end
31
35
 
32
36
  # significant change: strip out prefer_secondary from config
@@ -42,13 +46,36 @@ module Switchman
42
46
  end
43
47
 
44
48
  module MigrationContext
49
+ def migrate(...)
50
+ connection = ::ActiveRecord::Base.connection
51
+ connection_pool = ::ActiveRecord::Base.connection_pool
52
+ previous_schema_cache = connection_pool.get_schema_cache(connection)
53
+ temporary_schema_cache = ::ActiveRecord::ConnectionAdapters::SchemaCache.new(connection)
54
+
55
+ reset_column_information
56
+ connection_pool.set_schema_cache(temporary_schema_cache)
57
+
58
+ begin
59
+ super(...)
60
+ ensure
61
+ connection_pool.set_schema_cache(previous_schema_cache)
62
+ reset_column_information
63
+ end
64
+ end
65
+
45
66
  def migrations
46
67
  return @migrations if instance_variable_defined?(:@migrations)
47
68
 
48
69
  migrations_cache = Thread.current[:migrations_cache] ||= {}
49
- key = Digest::MD5.hexdigest(migration_files.sort.join(','))
70
+ key = Digest::MD5.hexdigest(migration_files.sort.join(","))
50
71
  @migrations = migrations_cache[key] ||= super
51
72
  end
73
+
74
+ private
75
+
76
+ def reset_column_information
77
+ ::ActiveRecord::Base.descendants.reject { |m| m <= UnshardedRecord }.each(&:reset_column_information)
78
+ end
52
79
  end
53
80
  end
54
81
  end
@@ -16,6 +16,14 @@ module Switchman
16
16
  db = shard.database_server
17
17
  db.unguard { super }
18
18
  end
19
+
20
+ def reload(*)
21
+ res = super
22
+ # When a shadow record is reloaded the real record is returned. So
23
+ # we need to ensure the loaded_from_shard is set correctly after a reload.
24
+ @loaded_from_shard = @shard
25
+ res
26
+ end
19
27
  end
20
28
  end
21
29
  end
@@ -5,9 +5,9 @@ module Switchman
5
5
  module PostgreSQLAdapter
6
6
  # copy/paste; use quote_local_table_name
7
7
  def create_database(name, options = {})
8
- options = { encoding: 'utf8' }.merge!(options.symbolize_keys)
8
+ options = { encoding: "utf8" }.merge!(options.symbolize_keys)
9
9
 
10
- option_string = options.sum('') do |key, value|
10
+ option_string = options.sum("") do |key, value|
11
11
  case key
12
12
  when :owner
13
13
  " OWNER = \"#{value}\""
@@ -24,7 +24,7 @@ module Switchman
24
24
  when :connection_limit
25
25
  " CONNECTION LIMIT = #{value}"
26
26
  else
27
- ''
27
+ ""
28
28
  end
29
29
  end
30
30
 
@@ -37,7 +37,7 @@ module Switchman
37
37
  end
38
38
 
39
39
  def current_schemas
40
- select_values('SELECT * FROM unnest(current_schemas(false))')
40
+ select_values("SELECT * FROM unnest(current_schemas(false))")
41
41
  end
42
42
 
43
43
  def extract_schema_qualified_name(string)
@@ -49,13 +49,13 @@ module Switchman
49
49
  # significant change: use the shard name if no explicit schema
50
50
  def quoted_scope(name = nil, type: nil)
51
51
  schema, name = extract_schema_qualified_name(name)
52
- type = \
52
+ type =
53
53
  case type # rubocop:disable Style/HashLikeCase
54
- when 'BASE TABLE'
54
+ when "BASE TABLE"
55
55
  "'r','p'"
56
- when 'VIEW'
56
+ when "VIEW"
57
57
  "'v','m'"
58
- when 'FOREIGN TABLE'
58
+ when "FOREIGN TABLE"
59
59
  "'f'"
60
60
  end
61
61
  scope = {}
@@ -67,7 +67,8 @@ module Switchman
67
67
 
68
68
  def foreign_keys(table_name)
69
69
  super.each do |fk|
70
- to_table_qualified_name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(fk.to_table)
70
+ to_table_qualified_name =
71
+ ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(fk.to_table)
71
72
  fk.to_table = to_table_qualified_name.identifier if to_table_qualified_name.schema == shard.name
72
73
  end
73
74
  end
@@ -101,7 +102,7 @@ module Switchman
101
102
 
102
103
  def add_index_options(_table_name, _column_name, **)
103
104
  index, algorithm, if_not_exists = super
104
- algorithm = nil if DatabaseServer.creating_new_shard && algorithm == 'CONCURRENTLY'
105
+ algorithm = nil if DatabaseServer.creating_new_shard && algorithm == "CONCURRENTLY"
105
106
  [index, algorithm, if_not_exists]
106
107
  end
107
108
 
@@ -20,7 +20,7 @@ module Switchman
20
20
  type_casted_binds: -> { type_casted_binds(binds) }
21
21
  }
22
22
  ::ActiveSupport::Notifications.instrument(
23
- 'sql.active_record',
23
+ "sql.active_record",
24
24
  args
25
25
  )
26
26
  query_cache[sql][binds]