switchman 1.1.0 → 1.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 (46) hide show
  1. checksums.yaml +8 -8
  2. data/app/models/switchman/shard.rb +10 -3
  3. data/lib/switchman/active_record/association.rb +36 -10
  4. data/lib/switchman/active_record/base.rb +9 -5
  5. data/lib/switchman/active_record/calculations.rb +12 -11
  6. data/lib/switchman/active_record/connection_handler.rb +102 -47
  7. data/lib/switchman/active_record/finder_methods.rb +1 -1
  8. data/lib/switchman/active_record/query_methods.rb +50 -25
  9. data/lib/switchman/active_record/relation.rb +4 -4
  10. data/lib/switchman/active_record/spawn_methods.rb +76 -29
  11. data/lib/switchman/connection_pool_proxy.rb +2 -1
  12. data/lib/switchman/database_server.rb +4 -2
  13. data/lib/switchman/engine.rb +1 -1
  14. data/lib/switchman/r_spec_helper.rb +14 -6
  15. data/lib/switchman/rails.rb +7 -9
  16. data/lib/switchman/test_helper.rb +4 -3
  17. data/lib/switchman/version.rb +1 -1
  18. data/spec/dummy/app/models/appendage.rb +2 -4
  19. data/spec/dummy/app/models/digit.rb +2 -4
  20. data/spec/dummy/app/models/feature.rb +0 -2
  21. data/spec/dummy/app/models/user.rb +0 -2
  22. data/spec/dummy/config/application.rb +0 -6
  23. data/spec/dummy/config/environments/development.rb +2 -9
  24. data/spec/dummy/config/environments/production.rb +2 -3
  25. data/spec/dummy/config/environments/test.rb +2 -11
  26. data/spec/dummy/config/routes.rb +2 -2
  27. data/spec/dummy/log/test.log +9947 -0
  28. data/spec/dummy/tmp/cache/375/530/shard%2F3200 +1 -0
  29. data/spec/dummy/tmp/cache/37A/590/shard%2F3214 +1 -0
  30. data/spec/dummy/tmp/cache/37E/620/shard%2F3182 +0 -0
  31. data/spec/dummy/tmp/cache/381/650/shard%2F3185 +1 -0
  32. data/spec/dummy/tmp/cache/382/650/shard%2F3177 +1 -0
  33. data/spec/dummy/tmp/cache/386/6B0/shard%2F3199 +1 -0
  34. data/spec/dummy/tmp/cache/3A6/F80/shard%2F13200 +1 -0
  35. data/spec/dummy/tmp/cache/3B0/080/shard%2F13183 +1 -0
  36. data/spec/dummy/tmp/cache/3B1/070/shard%2F13166 +1 -0
  37. data/spec/lib/active_record/association_spec.rb +21 -11
  38. data/spec/lib/active_record/finder_methods_spec.rb +1 -1
  39. data/spec/lib/active_record/query_cache_spec.rb +17 -17
  40. data/spec/lib/active_record/query_methods_spec.rb +6 -5
  41. data/spec/lib/active_record/relation_spec.rb +1 -1
  42. data/spec/lib/active_record/spawn_methods_spec.rb +5 -4
  43. data/spec/lib/connection_pool_proxy_spec.rb +2 -0
  44. data/spec/lib/database_server_spec.rb +12 -4
  45. data/spec/lib/shackles_spec.rb +8 -6
  46. metadata +61 -11
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- NTc5NGVlNTU2ODI2YTQ3NjY4OTBhOGZiNTZjN2MyMWNjYjY5YmNhZg==
4
+ YzRjZGFmYzVmY2EwYjYyNGQ3Nzc0NzYxNjdhOTQ2M2U5ZDU1YmE0Zg==
5
5
  data.tar.gz: !binary |-
6
- ZGNkZjQ3YWQ0Njg5ZTJhMDQ5ZTVmMWIyZGVhZDJhMTQ5MzE4ZDNlZg==
6
+ ZTY2ZDgwNDU2YjFmOWYwMDMyYzgyYzJmOGU5MWIzZTJlNGIzNDVhNg==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- ODY5NDM1MTFiYTFkMWM5MWYzMDJiZjFkNjY1YWNjZDg3MjFmZjM4YTM3ZjEx
10
- MzA4MDJjMjJkMjViOTQ5ZWZlMGYyOWUwMmRmYjI2MzNhYjE3ZmJhODk4ZGJh
11
- ODhlYzllMmQxYzVhYjc2NTE4MjYxNmIzZGM2N2M0YWU2NmZkMzg=
9
+ NTVmOWYyY2Y2YWUzMmQ4MWY3NjUzZTFiYThmMmM0OGM3ZDA1YTczNDdmZjFl
10
+ MjEzYmJhNzBjODc4OGZmYzgwZmEwNDI3NWQ2YWU3Yzg3ODBkNTBmNTYyNThh
11
+ YTJlOGRjMDI5NzhjYmNhZmQyYTMwNWQ4MGIwOWQ2MDM4ODYwZDU=
12
12
  data.tar.gz: !binary |-
13
- YzlkM2FlYmY5MTNlMzUzMDBhYjRkZGU5MGY5NWZjMzRiMGYyYzhlNDAyMmM0
14
- MWE1M2RkZmUyNWRjNDdlN2U5YTY3NTgxNWY4YWI4Y2M3NGZjMmUxMGNjODFh
15
- NTA1MGIwNTdlZTBhY2U1MzM1ZmU5ZDBiYjc1ZmM2MTE2MDdjZmY=
13
+ MzY2ODJiM2E3YWZkNjJhYjllODIyNDBkMzQ5NTkyNzhjNjY4Yzc5MDdiY2Iy
14
+ OGE3ZTA2N2U4YTdlZWFmMWJhZjQzNTU5NzNmYWYyMGIxZTNmOWVhZTc3Mzgw
15
+ MWE3YjYwNTlkODIzZThkNTlmMDI2NTVhNWNlNTA4Nzc5MTdlNjI=
@@ -16,8 +16,6 @@ module Switchman
16
16
  private_constant :CATEGORIES
17
17
  @shard_category = :unsharded
18
18
 
19
- attr_accessible :name, :database_server, :default
20
-
21
19
  # only allow one default
22
20
  validates_uniqueness_of :default, :if => lambda { |s| s.default? }
23
21
 
@@ -87,7 +85,11 @@ module Switchman
87
85
  nil
88
86
  else
89
87
  shard = Shard.new
90
- shard.assign_attributes(attributes, :without_protection => true)
88
+ if ::Rails.version < '4'
89
+ shard.assign_attributes(attributes, :without_protection => true)
90
+ else
91
+ shard.assign_attributes(attributes)
92
+ end
91
93
  shard.instance_variable_set(:@new_record, false)
92
94
  # connection info doesn't exist in database.yml;
93
95
  # pretend the shard doesn't exist either
@@ -520,6 +522,11 @@ module Switchman
520
522
  super
521
523
  end
522
524
 
525
+ # skip global_id.hash
526
+ def hash
527
+ id.hash
528
+ end
529
+
523
530
  private
524
531
 
525
532
  def clear_cache
@@ -2,7 +2,8 @@ module Switchman
2
2
  module ActiveRecord
3
3
  module Association
4
4
  def self.included(klass)
5
- %w{build_record creation_attributes load_target scoped}.each do |method|
5
+ %w{build_record creation_attributes load_target scope}.each do |method|
6
+ method = 'scoped' if method == 'scope' && ::Rails.version < '4'
6
7
  klass.alias_method_chain(method, :sharding)
7
8
  end
8
9
  end
@@ -24,10 +25,14 @@ module Switchman
24
25
  self.shard.activate { load_target_without_sharding }
25
26
  end
26
27
 
27
- def scoped_with_sharding
28
- shard_value = @reflection.options[:multishard] ? @owner : self.shard
29
- @owner.shard.activate { scoped_without_sharding.shard(shard_value, :association) }
30
- end
28
+ # scoped is renamed to scope in Rails 4
29
+ method = ::Rails.version < '4' ? 'scoped' : 'scope'
30
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
31
+ def #{method}_with_sharding
32
+ shard_value = @reflection.options[:multishard] ? @owner : self.shard
33
+ @owner.shard.activate { #{method}_without_sharding.shard(shard_value, :association) }
34
+ end
35
+ RUBY
31
36
 
32
37
  def creation_attributes_with_sharding
33
38
  attributes = creation_attributes_without_sharding
@@ -65,9 +70,17 @@ module Switchman
65
70
  end
66
71
 
67
72
  module Builder
68
- module Association
73
+ module CollectionAssociation
69
74
  def self.included(klass)
70
- klass.descendants.each{|d| d.valid_options += [:multishard]}
75
+ if ::Rails.version < '4'
76
+ [klass] + klass.descendants.each do |k|
77
+ k.valid_options << :multishard
78
+ end
79
+ end
80
+ end
81
+
82
+ def valid_options
83
+ super + [:multishard]
71
84
  end
72
85
  end
73
86
  end
@@ -77,7 +90,11 @@ module Switchman
77
90
  def self.included(klass)
78
91
  klass.send(:remove_method, :associated_records_by_owner)
79
92
  klass.send(:remove_method, :owners_by_key)
80
- klass.send(:remove_method, :scoped)
93
+ if ::Rails.version < '4'
94
+ klass.send(:remove_method, :scoped)
95
+ else
96
+ klass.send(:remove_method, :scope)
97
+ end
81
98
  end
82
99
 
83
100
  def associated_records_by_owner
@@ -126,15 +143,24 @@ module Switchman
126
143
  end
127
144
  end
128
145
 
129
- def scoped
146
+ def scope
130
147
  build_scope
131
148
  end
149
+ # renamed to just scope in Rails 4
150
+ if ::Rails.version < '4'
151
+ alias_method :scoped, :scope
152
+ remove_method(:scope)
153
+ end
132
154
  end
133
155
  end
134
156
 
135
157
  module CollectionProxy
136
158
  def shard(*args)
137
- scoped.shard(*args)
159
+ if ::Rails.version < '4'
160
+ scoped.shard(*args)
161
+ else
162
+ scope.shard(*args)
163
+ end
138
164
  end
139
165
  end
140
166
  end
@@ -2,7 +2,11 @@ module Switchman
2
2
  module ActiveRecord
3
3
  module Base
4
4
  module ClassMethods
5
- delegate :shard, :to => :scoped
5
+ if ::Rails.version < '4'
6
+ delegate :shard, to: :scoped
7
+ else
8
+ delegate :shard, to: :all
9
+ end
6
10
 
7
11
  def shard_category
8
12
  @shard_category || :default
@@ -29,7 +33,7 @@ module Switchman
29
33
  end
30
34
 
31
35
  def transaction(*args)
32
- if current_scope
36
+ if self != ::ActiveRecord::Base && current_scope
33
37
  current_scope.activate do
34
38
  db = Shard.current(shard_category).database_server
35
39
  if ::Shackles.environment != db.shackles_environment
@@ -78,16 +82,16 @@ module Switchman
78
82
 
79
83
  def save(*args)
80
84
  @shard_set_in_stone = true
81
- self.class.with_scope(self.class.shard(shard), :overwrite) { super }
85
+ self.class.shard(shard).scoping { super }
82
86
  end
83
87
 
84
88
  def save!(*args)
85
89
  @shard_set_in_stone = true
86
- self.class.with_scope(self.class.shard(shard), :overwrite) { super }
90
+ self.class.shard(shard).scoping { super }
87
91
  end
88
92
 
89
93
  def destroy
90
- self.class.with_scope(self.class.shard(shard), :overwrite) { super }
94
+ self.class.shard(shard).scoping { super }
91
95
  end
92
96
 
93
97
  def clone
@@ -45,15 +45,15 @@ module Switchman
45
45
  relation.select_values = [operation_over_aggregate_column(column, "average", distinct).as("average"),
46
46
  operation_over_aggregate_column(column, "count", distinct).as("count")]
47
47
 
48
- initial_results = relation.activate{ |rel| @klass.connection.select_all(rel) }
48
+ initial_results = relation.activate{ |rel| klass.connection.select_all(rel) }
49
49
  if initial_results.is_a?(Array)
50
50
  initial_results.each do |r|
51
- r["average"] = type_cast_calculated_value(r["average"], nil, "average")
52
- r["count"] = type_cast_calculated_value(r["count"], nil, "count")
51
+ r["average"] = type_cast_calculated_value(r["average"], column_for(column_name), "average")
52
+ r["count"] = type_cast_calculated_value(r["count"], column_for(column_name), "count")
53
53
  end
54
54
  result = initial_results.map{|r| r["average"] * r["count"]}.sum / initial_results.map{|r| r["count"]}.sum
55
55
  else
56
- result = type_cast_calculated_value(initial_results["average"], nil, "average")
56
+ result = type_cast_calculated_value(initial_results.first["average"], column_for(column_name), "average")
57
57
  end
58
58
  result
59
59
  end
@@ -66,7 +66,7 @@ module Switchman
66
66
  target_shard = Shard.current(:default)
67
67
 
68
68
  rows = relation.activate do |rel, shard|
69
- calculated_data = @klass.connection.select_all(rel)
69
+ calculated_data = klass.connection.select_all(rel)
70
70
 
71
71
  if opts[:association]
72
72
  key_ids = calculated_data.collect { |row| row[opts[:group_aliases].first] }
@@ -98,9 +98,9 @@ module Switchman
98
98
  opts = {:operation => operation, :column_name => column_name, :distinct => distinct}
99
99
 
100
100
  opts[:aggregate_alias] = aggregate_alias_for(operation, column_name)
101
- group_attrs = @group_values
101
+ group_attrs = group_values
102
102
  if group_attrs.first.respond_to?(:to_sym)
103
- association = @klass.reflect_on_association(group_attrs.first.to_sym)
103
+ association = klass.reflect_on_association(group_attrs.first.to_sym)
104
104
  associated = group_attrs.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations
105
105
  group_fields = Array(associated ? association.foreign_key : group_attrs)
106
106
  else
@@ -125,12 +125,12 @@ module Switchman
125
125
  elsif operation == 'average'
126
126
  'average'
127
127
  else
128
- column_alias_for(operation, column_name)
128
+ column_alias_for("#{operation} #{column_name}")
129
129
  end
130
130
  end
131
131
 
132
132
  def build_grouped_calculation_relation(opts)
133
- group = @klass.connection.adapter_name == 'FrontBase' ? opts[:group_aliases] : opts[:group_fields]
133
+ group = opts[:group_fields]
134
134
 
135
135
  select_values = [
136
136
  operation_over_aggregate_column(
@@ -146,7 +146,7 @@ module Switchman
146
146
  'count', opts[:distinct]).as('count')
147
147
  end
148
148
 
149
- select_values += @select_values unless @having_values.empty?
149
+ select_values += select_values unless having_values.empty?
150
150
  select_values.concat opts[:group_fields].zip(opts[:group_aliases]).map { |field,aliaz|
151
151
  if field.respond_to?(:as)
152
152
  field.as(aliaz)
@@ -155,7 +155,8 @@ module Switchman
155
155
  end
156
156
  }
157
157
 
158
- relation = except(:group).group(group)
158
+ relation = except(:group)
159
+ relation.group_values = group
159
160
  relation.select_values = select_values
160
161
  relation
161
162
  end
@@ -22,10 +22,11 @@ module Switchman
22
22
  def self.included(klass)
23
23
  klass.alias_method_chain(:establish_connection, :sharding)
24
24
  klass.alias_method_chain(:remove_connection, :sharding)
25
+ klass.send(:remove_method, :retrieve_connection_pool) if ::Rails.version >= '4'
25
26
  end
26
27
 
27
- def establish_connection_with_sharding(name, spec)
28
- establish_connection_without_sharding(name, spec)
28
+ def establish_connection_with_sharding(owner, spec)
29
+ establish_connection_without_sharding(owner, spec)
29
30
 
30
31
  # this is the first place that the adapter would have been required; but now we
31
32
  # need this addition ASAP since it will be called when loading the default shard below
@@ -34,8 +35,16 @@ module Switchman
34
35
  ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:include, ActiveRecord::PostgreSQLAdapter)
35
36
  end
36
37
 
37
- model = name.constantize
38
- pool = connection_pools[spec]
38
+ # AR3 uses the name, AR4 uses the model
39
+ model = case owner
40
+ when String
41
+ owner.constantize
42
+ when Class
43
+ owner
44
+ else
45
+ raise "unknown owner #{owner}"
46
+ end
47
+ pool = ::Rails.version < '4' ? connection_pools[spec] : owner_to_pool[owner.name]
39
48
 
40
49
  first_time = !Shard.instance_variable_get(:@default)
41
50
  if first_time
@@ -52,7 +61,12 @@ module Switchman
52
61
  proxy = ConnectionPoolProxy.new(model.shard_category,
53
62
  pool,
54
63
  @shard_connection_pools)
55
- connection_pools[spec] = proxy
64
+ if ::Rails.version < '4'
65
+ connection_pools[spec] = proxy
66
+ else
67
+ owner_to_pool[owner.name] = proxy
68
+ class_to_pool.clear
69
+ end
56
70
 
57
71
  if first_time
58
72
  if Shard.default.database_server.config[:prefer_slave]
@@ -65,8 +79,10 @@ module Switchman
65
79
  end
66
80
  end
67
81
 
68
- initialize_categories(model)
69
- @class_to_pool[name] = proxy
82
+ if ::Rails.version < '4'
83
+ initialize_categories(model)
84
+ class_to_pool[model.name] = proxy
85
+ end
70
86
 
71
87
  # reload the default shard if we just got a new connection
72
88
  # to where the Shards table is
@@ -96,61 +112,100 @@ module Switchman
96
112
  end
97
113
 
98
114
  def remove_connection_with_sharding(model)
99
- uninitialize_ar(model) if @class_to_pool[model.name].is_a?(ConnectionPoolProxy)
100
- remove_connection_without_sharding(model)
115
+ uninitialize_ar(model) if (::Rails.version < '4' ? class_to_pool : owner_to_pool)[model.name].is_a?(ConnectionPoolProxy)
116
+ result = remove_connection_without_sharding(model)
117
+ initialize_categories if ::Rails.version >= '4'
118
+ result
119
+ end
120
+
121
+ if ::Rails.version >= '4'
122
+ def retrieve_connection_pool(klass)
123
+ class_to_pool[klass.name] ||= begin
124
+ original_klass = klass
125
+ until pool = pool_for(klass)
126
+ klass = klass.superclass
127
+ break unless klass <= Base
128
+ end
129
+
130
+ if pool.is_a?(ConnectionPoolProxy) && pool.category != original_klass.shard_category
131
+ default_pool = pool.default_pool
132
+ pool = nil
133
+ class_to_pool.each_value { |p| pool = p if p.is_a?(ConnectionPoolProxy) &&
134
+ p.category == original_klass.shard_category &&
135
+ p.default_pool == default_pool }
136
+ pool ||= ConnectionPoolProxy.new(original_klass.shard_category, default_pool, @shard_connection_pools)
137
+ end
138
+
139
+ class_to_pool[original_klass.name] = pool
140
+ end
141
+ end
101
142
  end
102
143
 
103
144
  private
104
145
 
146
+ # AR3 only; AR4 defines it, and hides this version,
147
+ # and it's a slightly different data structure
148
+ def class_to_pool
149
+ @class_to_pool
150
+ end
151
+
105
152
  def uninitialize_ar(model = ::ActiveRecord::Base)
106
153
  # take the proxies out
107
- @class_to_pool.each_key do |model_name|
108
- pool_model = model_name.constantize
109
- # only de-proxify models that inherit from what we're uninitializing
110
- next unless pool_model == model || pool_model < model
111
- proxy = @class_to_pool[model_name]
112
- next unless proxy.is_a?(ConnectionPoolProxy)
113
-
114
- # make sure we're switched back to the default shard for the
115
- # connection that will remain
116
- if proxy.connected?
117
- Shard.default.activate { proxy.connection }
154
+ if ::Rails.version >= '4'
155
+ owner_to_pool[model.name] = owner_to_pool[model.name].default_pool
156
+ else
157
+ class_to_pool.each_key do |model_name|
158
+ pool_model = model_name.constantize
159
+ # only de-proxify models that inherit from what we're uninitializing
160
+ next unless pool_model == model || pool_model < model
161
+ proxy = class_to_pool[model_name]
162
+ next unless proxy.is_a?(ConnectionPoolProxy)
163
+
164
+ # make sure we're switched back to the default shard for the
165
+ # connection that will remain
166
+ if proxy.connected?
167
+ Shard.default.activate(proxy.category) { proxy.connection }
168
+ end
169
+ connection_pools[proxy.spec] = proxy.default_pool
170
+ class_to_pool[model_name] = proxy.default_pool
118
171
  end
119
- connection_pools[proxy.spec] = proxy.default_pool
120
- @class_to_pool[model_name] = proxy.default_pool
121
- end
122
172
 
123
- # prune dups that were created for implementing shard categories
124
- @class_to_pool.each_key do |model_name|
125
- next if model_name == ::ActiveRecord::Base.name
126
- pool_model = model_name.constantize
127
- @class_to_pool.delete(model_name) if retrieve_connection_pool(pool_model.superclass) == @class_to_pool[model_name]
173
+ # prune dups that were created for implementing shard categories
174
+ class_to_pool.each_key do |model_name|
175
+ next if model_name == ::ActiveRecord::Base.name
176
+ pool_model = model_name.constantize
177
+ class_to_pool.delete(model_name) if retrieve_connection_pool(pool_model.superclass) == class_to_pool[model_name]
178
+ end
128
179
  end
129
180
  end
130
181
 
131
182
  # semi-private
132
183
  public
133
184
  def initialize_categories(model = ::ActiveRecord::Base)
134
- # now set up pools for models that inherit from this model, but with a different
135
- # sharding category
136
- Shard.const_get(:CATEGORIES).each do |category, models|
137
- next if category == :default
138
- next if category == model.shard_category
139
-
140
- this_proxy = nil
141
- Array(models).each do |category_model|
142
- category_model = category_model.constantize if category_model.is_a? String
143
- next unless category_model < model
144
-
145
- # don't replace existing connections
146
- next if @class_to_pool[category_model.name]
147
-
148
- default_pool = retrieve_connection_pool(model)
149
- default_pool = default_pool.default_pool if default_pool.is_a?(ConnectionPoolProxy)
150
- # look for an existing compatible proxy for this category
151
- this_proxy ||= ConnectionPoolProxy.new(category_model.shard_category, default_pool, @shard_connection_pools)
152
- @class_to_pool[category_model.name] = this_proxy
185
+ if ::Rails.version < '4'
186
+ # now set up pools for models that inherit from this model, but with a different
187
+ # sharding category
188
+ Shard.const_get(:CATEGORIES).each do |category, models|
189
+ next if category == :default
190
+ next if category == model.shard_category
191
+
192
+ this_proxy = nil
193
+ Array(models).each do |category_model|
194
+ category_model = category_model.constantize if category_model.is_a? String
195
+ next unless category_model < model
196
+
197
+ # don't replace existing connections
198
+ next if class_to_pool[category_model.name]
199
+
200
+ default_pool = retrieve_connection_pool(model)
201
+ default_pool = default_pool.default_pool if default_pool.is_a?(ConnectionPoolProxy)
202
+ # look for an existing compatible proxy for this category
203
+ this_proxy ||= ConnectionPoolProxy.new(category_model.shard_category, default_pool, @shard_connection_pools)
204
+ class_to_pool[category_model.name] = this_proxy
205
+ end
153
206
  end
207
+ else
208
+ class_to_pool.clear
154
209
  end
155
210
  end
156
211
  end