switchman 2.1.0 → 3.0.6

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +10 -2
  3. data/app/models/switchman/shard.rb +270 -343
  4. data/app/models/switchman/unsharded_record.rb +7 -0
  5. data/db/migrate/20130328212039_create_switchman_shards.rb +1 -1
  6. data/db/migrate/20130328224244_create_default_shard.rb +5 -5
  7. data/db/migrate/20161206323434_add_back_default_string_limits_switchman.rb +1 -0
  8. data/db/migrate/20180828183945_add_default_shard_index.rb +2 -2
  9. data/db/migrate/20180828192111_add_timestamps_to_shards.rb +8 -6
  10. data/db/migrate/20190114212900_add_unique_name_indexes.rb +5 -3
  11. data/lib/switchman/action_controller/caching.rb +2 -2
  12. data/lib/switchman/active_record/abstract_adapter.rb +0 -8
  13. data/lib/switchman/active_record/association.rb +78 -89
  14. data/lib/switchman/active_record/attribute_methods.rb +127 -52
  15. data/lib/switchman/active_record/base.rb +83 -67
  16. data/lib/switchman/active_record/calculations.rb +73 -66
  17. data/lib/switchman/active_record/connection_pool.rb +12 -59
  18. data/lib/switchman/active_record/database_configurations/database_config.rb +13 -0
  19. data/lib/switchman/active_record/database_configurations.rb +34 -0
  20. data/lib/switchman/active_record/finder_methods.rb +11 -16
  21. data/lib/switchman/active_record/log_subscriber.rb +4 -8
  22. data/lib/switchman/active_record/migration.rb +19 -45
  23. data/lib/switchman/active_record/model_schema.rb +1 -1
  24. data/lib/switchman/active_record/persistence.rb +11 -6
  25. data/lib/switchman/active_record/postgresql_adapter.rb +33 -161
  26. data/lib/switchman/active_record/predicate_builder.rb +1 -1
  27. data/lib/switchman/active_record/query_cache.rb +18 -19
  28. data/lib/switchman/active_record/query_methods.rb +178 -193
  29. data/lib/switchman/active_record/reflection.rb +7 -22
  30. data/lib/switchman/active_record/relation.rb +32 -29
  31. data/lib/switchman/active_record/spawn_methods.rb +27 -29
  32. data/lib/switchman/active_record/statement_cache.rb +18 -35
  33. data/lib/switchman/active_record/tasks/database_tasks.rb +16 -0
  34. data/lib/switchman/active_record/test_fixtures.rb +43 -0
  35. data/lib/switchman/active_support/cache.rb +3 -5
  36. data/lib/switchman/arel.rb +13 -8
  37. data/lib/switchman/database_server.rb +130 -154
  38. data/lib/switchman/default_shard.rb +52 -16
  39. data/lib/switchman/engine.rb +65 -58
  40. data/lib/switchman/environment.rb +4 -8
  41. data/lib/switchman/errors.rb +1 -0
  42. data/lib/switchman/guard_rail/relation.rb +5 -7
  43. data/lib/switchman/guard_rail.rb +6 -19
  44. data/lib/switchman/r_spec_helper.rb +29 -57
  45. data/lib/switchman/rails.rb +14 -12
  46. data/lib/switchman/sharded_instrumenter.rb +1 -1
  47. data/lib/switchman/standard_error.rb +15 -3
  48. data/lib/switchman/test_helper.rb +5 -3
  49. data/lib/switchman/version.rb +1 -1
  50. data/lib/switchman.rb +3 -3
  51. data/lib/tasks/switchman.rake +61 -72
  52. metadata +90 -48
  53. data/lib/switchman/active_record/batches.rb +0 -11
  54. data/lib/switchman/active_record/connection_handler.rb +0 -190
  55. data/lib/switchman/active_record/where_clause_factory.rb +0 -36
  56. data/lib/switchman/connection_pool_proxy.rb +0 -173
  57. data/lib/switchman/schema_cache.rb +0 -28
@@ -6,49 +6,40 @@ module Switchman
6
6
  module ClassMethods
7
7
  delegate :shard, to: :all
8
8
 
9
- def find_ids_in_ranges(opts={}, &block)
10
- opts.reverse_merge!(:loose => true)
9
+ def find_ids_in_ranges(opts = {}, &block)
10
+ opts.reverse_merge!(loose: true)
11
11
  all.find_ids_in_ranges(opts, &block)
12
12
  end
13
13
 
14
- def shard_category
15
- connection_specification_name.to_sym
16
- end
14
+ def sharded_model
15
+ self.abstract_class = true
17
16
 
18
- def shard_category=(category)
19
- categories = Shard.const_get(:CATEGORIES)
20
- if categories[shard_category]
21
- categories[shard_category].delete(self)
22
- categories.delete(shard_category) if categories[shard_category].empty?
23
- end
24
- categories[category] ||= []
25
- categories[category] << self
26
- self.connection_specification_name = category.to_s
17
+ return if self == UnshardedRecord
18
+
19
+ Shard.send(:add_sharded_model, self)
27
20
  end
28
21
 
29
22
  def integral_id?
30
- if @integral_id == nil
31
- @integral_id = columns_hash[primary_key]&.type == :integer
32
- end
23
+ @integral_id = columns_hash[primary_key]&.type == :integer if @integral_id.nil?
33
24
  @integral_id
34
25
  end
35
26
 
36
27
  def transaction(**)
37
28
  if self != ::ActiveRecord::Base && current_scope
38
29
  current_scope.activate do
39
- db = Shard.current(shard_category).database_server
40
- if ::GuardRail.environment != db.guard_rail_environment
41
- db.unguard { super }
42
- else
30
+ db = Shard.current(connection_classes).database_server
31
+ if ::GuardRail.environment == db.guard_rail_environment
43
32
  super
33
+ else
34
+ db.unguard { super }
44
35
  end
45
36
  end
46
37
  else
47
- db = Shard.current(shard_category).database_server
48
- if ::GuardRail.environment != db.guard_rail_environment
49
- db.unguard { super }
50
- else
38
+ db = Shard.current(connection_classes).database_server
39
+ if ::GuardRail.environment == db.guard_rail_environment
51
40
  super
41
+ else
42
+ db.unguard { super }
52
43
  end
53
44
  end
54
45
  end
@@ -72,53 +63,70 @@ module Switchman
72
63
  end
73
64
 
74
65
  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?
78
- end
66
+ ::ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool|
67
+ pool.connection(switch_shard: false).clear_query_cache if pool.active_connection?
79
68
  end
80
69
  end
81
- end
82
70
 
83
- def self.included(klass)
84
- klass.extend(ClassMethods)
85
- klass.set_callback(:initialize, :before) do
86
- unless @shard
87
- if self.class.sharded_primary_key?
88
- @shard = Shard.shard_for(self[self.class.primary_key], Shard.current(self.class.shard_category))
89
- else
90
- @shard = Shard.current(self.class.shard_category)
91
- end
71
+ # significant change: _don't_ check if klasses.include?(Base)
72
+ # i.e. other sharded models don't inherit the current shard of Base
73
+ def current_shard
74
+ connected_to_stack.reverse_each do |hash|
75
+ return hash[:shard] if hash[:shard] && hash[:klasses].include?(connection_classes)
92
76
  end
77
+
78
+ default_shard
93
79
  end
80
+
81
+ def current_switchman_shard
82
+ connected_to_stack.reverse_each do |hash|
83
+ return hash[:switchman_shard] if hash[:switchman_shard] && hash[:klasses].include?(connection_classes)
84
+ end
85
+
86
+ Shard.default
87
+ end
88
+ end
89
+
90
+ def self.prepended(klass)
91
+ klass.singleton_class.prepend(ClassMethods)
92
+ end
93
+
94
+ def _run_initialize_callbacks
95
+ @shard ||= if self.class.sharded_primary_key?
96
+ Shard.shard_for(self[self.class.primary_key], Shard.current(self.class.connection_classes))
97
+ else
98
+ Shard.current(self.class.connection_classes)
99
+ end
100
+ super
94
101
  end
95
102
 
96
103
  def shard
97
- @shard || Shard.current(self.class.shard_category) || Shard.default
104
+ @shard || Shard.current(self.class.connection_classes) || Shard.default
98
105
  end
99
106
 
100
107
  def shard=(new_shard)
101
- raise ::ActiveRecord::ReadOnlyRecord if !self.new_record? || @shard_set_in_stone
102
- if shard != new_shard
103
- attributes.each do |attr, value|
104
- self[attr] = Shard.relative_id_for(value, shard, new_shard) if self.class.sharded_column?(attr)
105
- end
106
- @shard = new_shard
108
+ raise ::ActiveRecord::ReadOnlyRecord if !new_record? || @shard_set_in_stone
109
+
110
+ return if shard == new_shard
111
+
112
+ attributes.each do |attr, value|
113
+ self[attr] = Shard.relative_id_for(value, shard, new_shard) if self.class.sharded_column?(attr)
107
114
  end
115
+ @shard = new_shard
108
116
  end
109
117
 
110
118
  def save(*, **)
111
119
  @shard_set_in_stone = true
112
- (self.class.current_scope || self.class.default_scoped).shard(shard, :implicit).scoping { super }
120
+ super
113
121
  end
114
122
 
115
123
  def save!(*, **)
116
124
  @shard_set_in_stone = true
117
- (self.class.current_scope || self.class.default_scoped).shard(shard, :implicit).scoping { super }
125
+ super
118
126
  end
119
127
 
120
128
  def destroy
121
- self.class.shard(shard, :implicit).scoping { super }
129
+ shard.activate(self.class.connection_classes) { super }
122
130
  end
123
131
 
124
132
  def clone
@@ -126,23 +134,29 @@ module Switchman
126
134
  # TODO: adjust foreign keys
127
135
  # don't use the setter, cause the foreign keys are already
128
136
  # relative to this shard
129
- result.instance_variable_set(:@shard, self.shard)
137
+ result.instance_variable_set(:@shard, shard)
130
138
  result
131
139
  end
132
140
 
133
141
  def transaction(**kwargs, &block)
134
- shard.activate(self.class.shard_category) do
142
+ shard.activate(self.class.connection_classes) do
135
143
  self.class.transaction(**kwargs, &block)
136
144
  end
137
145
  end
138
146
 
147
+ def with_transaction_returning_status
148
+ shard.activate(self.class.connection_classes) do
149
+ super
150
+ end
151
+ end
152
+
139
153
  def hash
140
- self.class.sharded_primary_key? ? self.class.hash ^ Shard.global_id_for(id).hash : super
154
+ self.class.sharded_primary_key? ? self.class.hash ^ global_id.hash : super
141
155
  end
142
156
 
143
157
  def to_param
144
158
  short_id = Shard.short_id_for(id)
145
- short_id && short_id.to_s
159
+ short_id&.to_s
146
160
  end
147
161
 
148
162
  def initialize_dup(*args)
@@ -151,16 +165,18 @@ module Switchman
151
165
  copy
152
166
  end
153
167
 
154
- def quoted_id
155
- return super unless self.class.sharded_primary_key?
156
- # do this the Rails 4.2 way, so that if Shard.current != self.shard, the id gets transposed
157
- self.class.connection.quote(id)
168
+ def update_columns(*)
169
+ db = shard.database_server
170
+ return db.unguard { super } if ::GuardRail.environment != db.guard_rail_environment
171
+
172
+ super
158
173
  end
159
174
 
160
- def update_columns(*)
161
- db = Shard.current(self.class.shard_category).database_server
162
- if ::GuardRail.environment != db.guard_rail_environment
163
- return db.unguard { super }
175
+ def id_for_database
176
+ if self.class.sharded_primary_key?
177
+ # It's an int, so so it's safe to just return it without passing it through anything else
178
+ # In theory we should do `@attributes[@primary_key].type.serialize(id)`, but that seems to have surprising side-effects
179
+ id
164
180
  else
165
181
  super
166
182
  end
@@ -168,22 +184,22 @@ module Switchman
168
184
 
169
185
  protected
170
186
 
171
- # see also AttributeMethods#shard_category_code_for_reflection
172
- def shard_category_for_reflection(reflection)
187
+ # see also AttributeMethods#connection_classes_code_for_reflection
188
+ def connection_classes_for_reflection(reflection)
173
189
  if reflection
174
190
  if reflection.options[:polymorphic]
175
191
  begin
176
- read_attribute(reflection.foreign_type)&.constantize&.shard_category || :primary
192
+ read_attribute(reflection.foreign_type)&.constantize&.connection_classes
177
193
  rescue NameError
178
194
  # in case someone is abusing foreign_type to not point to an actual class
179
- :primary
195
+ ::ActiveRecord::Base
180
196
  end
181
197
  else
182
198
  # otherwise we can just return a symbol for the statically known type of the association
183
- reflection.klass.shard_category
199
+ reflection.klass.connection_classes
184
200
  end
185
201
  else
186
- shard_category
202
+ self.class.connection_classes
187
203
  end
188
204
  end
189
205
  end
@@ -3,45 +3,44 @@
3
3
  module Switchman
4
4
  module ActiveRecord
5
5
  module Calculations
6
-
7
6
  def pluck(*column_names)
8
- target_shard = Shard.current(klass.shard_category)
7
+ target_shard = Shard.current(klass.connection_classes)
9
8
  shard_count = 0
10
- result = self.activate do |relation, shard|
9
+ result = activate do |relation, shard|
11
10
  shard_count += 1
12
11
  results = relation.call_super(:pluck, Calculations, *column_names)
13
12
  if column_names.length > 1
14
13
  column_names.each_with_index do |column_name, idx|
15
- if klass.sharded_column?(column_name)
16
- results.each{|result| result[idx] = Shard.relative_id_for(result[idx], shard, target_shard)}
14
+ next unless klass.sharded_column?(column_name)
15
+
16
+ results.each do |r|
17
+ r[idx] = Shard.relative_id_for(r[idx], shard, target_shard)
17
18
  end
18
19
  end
19
- else
20
- if klass.sharded_column?(column_names.first.to_s)
21
- results = results.map{|result| Shard.relative_id_for(result, shard, target_shard)}
22
- end
20
+ elsif klass.sharded_column?(column_names.first.to_s)
21
+ results = results.map { |r| Shard.relative_id_for(r, shard, target_shard) }
23
22
  end
24
23
  results
25
24
  end
26
- if distinct_value && shard_count > 1
27
- result.uniq!
28
- end
25
+ result.uniq! if distinct_value && shard_count > 1
29
26
  result
30
27
  end
31
28
 
32
29
  def execute_simple_calculation(operation, column_name, distinct)
33
30
  operation = operation.to_s.downcase
34
- if operation == "average"
31
+ if operation == 'average'
35
32
  result = calculate_simple_average(column_name, distinct)
36
33
  else
37
- result = self.activate{ |relation| relation.call_super(:execute_simple_calculation, Calculations, operation, column_name, distinct) }
34
+ result = activate do |relation|
35
+ relation.call_super(:execute_simple_calculation, Calculations, operation, column_name, distinct)
36
+ end
38
37
  if result.is_a?(Array)
39
38
  case operation
40
- when "count", "sum"
39
+ when 'count', 'sum'
41
40
  result = result.sum
42
- when "minimum"
41
+ when 'minimum'
43
42
  result = result.min
44
- when "maximum"
43
+ when 'maximum'
45
44
  result = result.max
46
45
  end
47
46
  end
@@ -53,18 +52,20 @@ module Switchman
53
52
  # See activerecord#execute_simple_calculation
54
53
  relation = except(:order)
55
54
  column = aggregate_column(column_name)
56
- relation.select_values = [operation_over_aggregate_column(column, "average", distinct).as("average"),
57
- 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')]
58
57
 
59
- initial_results = relation.activate{ |rel| klass.connection.select_all(rel) }
58
+ initial_results = relation.activate { |rel| klass.connection.select_all(rel) }
60
59
  if initial_results.is_a?(Array)
61
60
  initial_results.each do |r|
62
- r["average"] = type_cast_calculated_value(r["average"], type_for(column_name), "average")
63
- r["count"] = type_cast_calculated_value(r["count"], type_for(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')
64
63
  end
65
- result = initial_results.map{|r| r["average"] * r["count"]}.sum / initial_results.map{|r| r["count"]}.sum
64
+ result = initial_results.map { |r| r['average'] * r['count'] }.sum / initial_results.map do |r|
65
+ r['count']
66
+ end.sum
66
67
  else
67
- result = type_cast_calculated_value(initial_results.first["average"], type_for(column_name), "average")
68
+ result = type_cast_calculated_value_switchman(initial_results.first['average'], column_name, 'average')
68
69
  end
69
70
  result
70
71
  end
@@ -74,27 +75,28 @@ module Switchman
74
75
  opts = grouped_calculation_options(operation.to_s.downcase, column_name, distinct)
75
76
 
76
77
  relation = build_grouped_calculation_relation(opts)
77
- target_shard = Shard.current(:primary)
78
+ target_shard = Shard.current
78
79
 
79
80
  rows = relation.activate do |rel, shard|
80
81
  calculated_data = klass.connection.select_all(rel)
81
82
 
82
83
  if opts[:association]
83
84
  key_ids = calculated_data.collect { |row| row[opts[:group_aliases].first] }
84
- key_records = opts[:association].klass.base_class.where(:id => key_ids)
85
- key_records = Hash[key_records.map { |r| [Shard.relative_id_for(r, shard, target_shard), r] }]
85
+ key_records = opts[:association].klass.base_class.where(id: key_ids)
86
+ key_records = key_records.map { |r| [Shard.relative_id_for(r, shard, target_shard), r] }.to_h
86
87
  end
87
88
 
88
89
  calculated_data.map do |row|
89
- row[opts[:aggregate_alias]] = type_cast_calculated_value(
90
- row[opts[:aggregate_alias]], type_for(opts[:column_name]), opts[:operation])
90
+ row[opts[:aggregate_alias]] = type_cast_calculated_value_switchman(
91
+ row[opts[:aggregate_alias]], column_name, opts[:operation]
92
+ )
91
93
  row['count'] = row['count'].to_i if opts[:operation] == 'average'
92
94
 
93
- opts[:group_columns].each do |aliaz, type, column_name|
95
+ opts[:group_columns].each do |aliaz, _type, group_column_name|
94
96
  if opts[:associated] && (aliaz == opts[:group_aliases].first)
95
97
  row[aliaz] = key_records[Shard.relative_id_for(row[aliaz], shard, target_shard)]
96
- elsif column_name && @klass.sharded_column?(column_name)
97
- row[aliaz] = Shard.relative_id_for(type_cast_calculated_value(row[aliaz], type), shard, target_shard)
98
+ elsif group_column_name && @klass.sharded_column?(group_column_name)
99
+ row[aliaz] = Shard.relative_id_for(row[aliaz], shard, target_shard)
98
100
  end
99
101
  end
100
102
  row
@@ -106,12 +108,21 @@ module Switchman
106
108
 
107
109
  private
108
110
 
111
+ def type_cast_calculated_value_switchman(value, column_name, operation)
112
+ type_cast_calculated_value(value, operation) do |val|
113
+ column = aggregate_column(column_name)
114
+ type ||= column.try(:type_caster) ||
115
+ lookup_cast_type_from_join_dependencies(column_name.to_s) || Type.default_value
116
+ type.deserialize(val)
117
+ end
118
+ end
119
+
109
120
  def column_name_for(field)
110
121
  field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last
111
122
  end
112
123
 
113
124
  def grouped_calculation_options(operation, column_name, distinct)
114
- opts = {:operation => operation, :column_name => column_name, :distinct => distinct}
125
+ opts = { operation: operation, column_name: column_name, distinct: distinct }
115
126
 
116
127
  opts[:aggregate_alias] = aggregate_alias_for(operation, column_name)
117
128
  group_attrs = group_values
@@ -125,12 +136,12 @@ module Switchman
125
136
 
126
137
  # to_s is because Rails 5 returns a string but Rails 6 returns a symbol.
127
138
  group_aliases = group_fields.map { |field| column_alias_for(field.downcase.to_s).to_s }
128
- group_columns = group_aliases.zip(group_fields).map { |aliaz, field|
139
+ group_columns = group_aliases.zip(group_fields).map do |aliaz, field|
129
140
  [aliaz, type_for(field), column_name_for(field)]
130
- }
131
- opts.merge!(:association => association, :associated => associated,
132
- :group_aliases => group_aliases, :group_columns => group_columns,
133
- :group_fields => group_fields)
141
+ end
142
+ opts.merge!(association: association, associated: associated,
143
+ group_aliases: group_aliases, group_columns: group_columns,
144
+ group_fields: group_fields)
134
145
 
135
146
  opts
136
147
  end
@@ -149,28 +160,30 @@ module Switchman
149
160
  group = opts[:group_fields]
150
161
 
151
162
  select_values = [
152
- operation_over_aggregate_column(
153
- aggregate_column(opts[:column_name]),
154
- opts[:operation],
155
- opts[:distinct]).as(opts[:aggregate_alias])
163
+ operation_over_aggregate_column(
164
+ aggregate_column(opts[:column_name]),
165
+ opts[:operation],
166
+ opts[:distinct]
167
+ ).as(opts[:aggregate_alias])
156
168
  ]
157
- if opts[:operation ]== 'average'
169
+ if opts[:operation] == 'average'
158
170
  # include count in average so we can recalculate the average
159
171
  # across all shards if needed
160
172
  select_values << operation_over_aggregate_column(
161
- aggregate_column(opts[:column_name]),
162
- 'count', opts[:distinct]).as('count')
173
+ aggregate_column(opts[:column_name]),
174
+ 'count', opts[:distinct]
175
+ ).as('count')
163
176
  end
164
177
 
165
178
  haves = having_clause.send(:predicates)
166
179
  select_values += select_values unless haves.empty?
167
- select_values.concat opts[:group_fields].zip(opts[:group_aliases]).map { |field,aliaz|
180
+ select_values.concat(opts[:group_fields].zip(opts[:group_aliases]).map do |field, aliaz|
168
181
  if field.respond_to?(:as)
169
182
  field.as(aliaz)
170
183
  else
171
184
  "#{field} AS #{aliaz}"
172
185
  end
173
- }
186
+ end)
174
187
 
175
188
  relation = except(:group)
176
189
  relation.group_values = group
@@ -181,12 +194,12 @@ module Switchman
181
194
  def compact_grouped_calculation_rows(rows, opts)
182
195
  result = ::ActiveSupport::OrderedHash.new
183
196
  rows.each do |row|
184
- key = opts[:group_columns].map { |aliaz, column| row[aliaz] }
197
+ key = opts[:group_columns].map { |aliaz, _column| row[aliaz] }
185
198
  key = key.first if key.size == 1
186
199
  value = row[opts[:aggregate_alias]]
187
200
 
188
201
  if opts[:operation] == 'average'
189
- if result.has_key?(key)
202
+ if result.key?(key)
190
203
  old_value, old_count = result[key]
191
204
  new_count = old_count + row['count']
192
205
  new_value = ((old_value * old_count) + (value * row['count'])) / new_count
@@ -194,30 +207,24 @@ module Switchman
194
207
  else
195
208
  result[key] = [value, row['count']]
196
209
  end
197
- else
198
- if result.has_key?(key)
199
- case opts[:operation]
200
- when "count", "sum"
201
- result[key] += value
202
- when "minimum"
203
- result[key] = value if value < result[key]
204
- when "maximum"
205
- result[key] = value if value > result[key]
206
- end
207
- else
208
- result[key] = value
210
+ elsif result.key?(key)
211
+ case opts[:operation]
212
+ when 'count', 'sum'
213
+ result[key] += value
214
+ when 'minimum'
215
+ result[key] = value if value < result[key]
216
+ when 'maximum'
217
+ result[key] = value if value > result[key]
209
218
  end
219
+ else
220
+ result[key] = value
210
221
  end
211
222
  end
212
223
 
213
- if opts[:operation] == 'average'
214
- result = Hash[result.map{|k, v| [k, v.first]}]
215
- end
224
+ result.transform_values!(&:first) if opts[:operation] == 'average'
216
225
 
217
226
  result
218
227
  end
219
-
220
-
221
228
  end
222
229
  end
223
230
  end
@@ -5,16 +5,7 @@ require 'switchman/errors'
5
5
  module Switchman
6
6
  module ActiveRecord
7
7
  module ConnectionPool
8
- def shard
9
- Thread.current[tls_key] || Shard.default
10
- end
11
-
12
- def shard=(value)
13
- Thread.current[tls_key] = value
14
- end
15
-
16
8
  def default_schema
17
- raise "Not postgres!" unless self.spec.config[:adapter] == 'postgresql'
18
9
  connection unless @schemas
19
10
  # default shard will not switch databases immediately, so it won't be set yet
20
11
  @schemas ||= connection.current_schemas
@@ -22,75 +13,37 @@ module Switchman
22
13
  end
23
14
 
24
15
  def checkout_new_connection
25
- conn = synchronize do
26
- # ideally I would just keep a thread-local spec that I could modify
27
- # without locking anything, but if spec returns not-the-object passed
28
- # to initialize this pool, things break
29
- spec.config[:shard_name] = self.shard.name
30
-
31
- super
32
- end
33
- conn.shard = self.shard
16
+ conn = super
17
+ conn.shard = current_shard
34
18
  conn
35
19
  end
36
20
 
37
21
  def connection(switch_shard: true)
38
22
  conn = super()
39
- raise NonExistentShardError if shard.new_record?
40
- switch_database(conn) if conn.shard != self.shard && switch_shard
23
+ raise NonExistentShardError if current_shard.new_record?
24
+
25
+ switch_database(conn) if conn.shard != current_shard && switch_shard
41
26
  conn
42
27
  end
43
28
 
44
29
  def release_connection(with_id = Thread.current)
45
30
  super(with_id)
46
31
 
47
- if spec.config[:idle_timeout]
48
- clear_idle_connections!(Time.now - spec.config[:idle_timeout].to_i)
49
- end
50
- end
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
-
66
- def clear_idle_connections!(since_when)
67
- synchronize do
68
- @connections.reject! do |conn|
69
- if conn.last_query_at < since_when && !conn.in_use?
70
- conn.disconnect!
71
- true
72
- else
73
- false
74
- end
75
- end
76
- @available.clear
77
- @connections.each do |conn|
78
- @available.add conn
79
- end
80
- end
32
+ flush
81
33
  end
82
34
 
83
35
  def switch_database(conn)
84
- if !@schemas && conn.adapter_name == 'PostgreSQL' && !self.shard.database_server.config[:shard_name]
85
- @schemas = conn.current_schemas
86
- end
36
+ @schemas = conn.current_schemas if !@schemas && conn.adapter_name == 'PostgreSQL' && !current_shard.database_server.config[:shard_name]
87
37
 
88
- spec.config[:shard_name] = self.shard.name
89
- conn.shard = shard
38
+ conn.shard = current_shard
90
39
  end
91
40
 
92
41
  private
93
42
 
43
+ def current_shard
44
+ connection_klass.current_switchman_shard
45
+ end
46
+
94
47
  def tls_key
95
48
  "#{object_id}_shard".to_sym
96
49
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Switchman
4
+ module ActiveRecord
5
+ module DatabaseConfigurations
6
+ module DatabaseConfig
7
+ def for_current_env?
8
+ true
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Switchman
4
+ module ActiveRecord
5
+ module DatabaseConfigurations
6
+ private
7
+
8
+ # key difference: assumes a hybrid two-tier structure; each third tier
9
+ # is implicitly named, and their config is constructing by merging into
10
+ # its parent
11
+ def build_configs(configs)
12
+ return configs.configurations if configs.is_a?(DatabaseConfigurations)
13
+ return configs if configs.is_a?(Array)
14
+
15
+ db_configs = configs.flat_map do |env_name, config|
16
+ roles = config.keys.select { |k| config[k].is_a?(Hash) }
17
+ base_config = config.except(*roles)
18
+
19
+ name = "#{env_name}/primary"
20
+ name = 'primary' if env_name == default_env
21
+ base_db = build_db_config_from_raw_config(env_name, name, base_config)
22
+ [base_db] + roles.map do |role|
23
+ build_db_config_from_raw_config(env_name, "#{env_name}/#{role}",
24
+ base_config.merge(config[role]).merge(replica: true))
25
+ end
26
+ end
27
+
28
+ db_configs << environment_url_config(default_env, 'primary', {}) unless db_configs.find(&:for_current_env?)
29
+
30
+ merge_db_environment_variables(default_env, db_configs.compact)
31
+ end
32
+ end
33
+ end
34
+ end