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
@@ -42,7 +42,7 @@ module Switchman
42
42
 
43
43
  activate { return true if connection.select_value(relation, "#{name} Exists") }
44
44
  false
45
- rescue ThrowResult
45
+ rescue ::ActiveRecord::ThrowResult
46
46
  false
47
47
  end
48
48
  end
@@ -12,36 +12,59 @@ module Switchman
12
12
  # for foreign key transposition
13
13
  # :to_a - a special value that Relation#to_a uses when querying multiple shards to
14
14
  # remove primary keys from conditions that aren't applicable to the current shard
15
- attr_accessor :shard_value, :shard_source_value
15
+ if ::Rails.version < '4'
16
+ attr_accessor :shard_value, :shard_source_value
17
+ else
18
+ def shard_value
19
+ @values[:shard]
20
+ end
21
+ def shard_source_value
22
+ @values[:shard_source]
23
+ end
24
+ def shard_value=(value)
25
+ raise ImmutableRelation if @loaded
26
+ @values[:shard] = value
27
+ end
28
+ def shard_source_value=(value)
29
+ raise ImmutableRelation if @loaded
30
+ @values[:shard_source] = value
31
+ end
32
+ end
16
33
 
17
34
  def shard(value, source = :explicit)
18
- relation = clone
19
- relation.shard_value = value
20
- relation.shard_source_value = source
21
- if (primary_shard != relation.primary_shard || source == :to_a)
22
- relation.where_values = relation.transpose_predicates(relation.where_values, primary_shard, relation.primary_shard, source == :to_a) if !relation.where_values.empty?
23
- relation.having_values = relation.transpose_predicates(relation.having_values, primary_shard, relation.primary_shard, source == :to_a) if !relation.having_values.empty?
35
+ (::Rails.version < '4' ? clone : spawn).shard!(value, source)
36
+ end
37
+
38
+ def shard!(value, source = :explicit)
39
+ primary_shard = self.primary_shard
40
+ self.shard_value = value
41
+ self.shard_source_value = source
42
+ if (primary_shard != self.primary_shard || source == :to_a)
43
+ self.where_values = transpose_predicates(where_values, primary_shard, self.primary_shard, source == :to_a) if !where_values.empty?
44
+ self.having_values = transpose_predicates(having_values, primary_shard, self.primary_shard, source == :to_a) if !having_values.empty?
24
45
  end
25
- relation
46
+ self
26
47
  end
27
48
 
28
- # replace these with versions that call build_where on the
29
- # result relation, not the source relation (so build_where
30
- # is able to implicitly change the shard_value)
31
- def where(opts, *rest)
32
- return self if opts.blank?
49
+ if ::Rails.version < '4'
50
+ # replace these with versions that call build_where on the
51
+ # result relation, not the source relation (so build_where
52
+ # is able to implicitly change the shard_value)
53
+ def where(opts, *rest)
54
+ return self if opts.blank?
33
55
 
34
- relation = clone
35
- relation.where_values += relation.build_where(opts, rest)
36
- relation
37
- end
56
+ relation = clone
57
+ relation.where_values += relation.build_where(opts, rest)
58
+ relation
59
+ end
38
60
 
39
- def having(opts, *rest)
40
- return self if opts.blank?
61
+ def having(opts, *rest)
62
+ return self if opts.blank?
41
63
 
42
- relation = clone
43
- relation.having_values += relation.build_where(opts, rest)
44
- relation
64
+ relation = clone
65
+ relation.having_values += relation.build_where(opts, rest)
66
+ relation
67
+ end
45
68
  end
46
69
 
47
70
  def build_where(opts, other = [])
@@ -147,6 +170,7 @@ module Switchman
147
170
  end
148
171
 
149
172
  def sharded_primary_key?(relation, column)
173
+ return column == 'id' if relation.engine == ::ActiveRecord::Base
150
174
  relation.engine.primary_key == column
151
175
  end
152
176
 
@@ -165,6 +189,7 @@ module Switchman
165
189
  attribute = attribute.relation if attribute.relation.is_a?(Arel::Nodes::TableAlias)
166
190
  [attribute.relation, column]
167
191
  end
192
+
168
193
  # semi-private
169
194
  public
170
195
  def transpose_predicates(predicates, source_shard, target_shard, remove_nonlocal_primary_keys = false)
@@ -194,11 +219,11 @@ module Switchman
194
219
  local_ids
195
220
  when Arel::Nodes::BindParam
196
221
  # look for a bind param with a matching column name
197
- if @bind_params && idx = @bind_params.find_index{|b| b.is_a?(Array) && b.first.try(:name) == predicate.left}
198
- column, value = @bind_params[idx]
222
+ if bind_values && idx = bind_values.find_index{|b| b.is_a?(Array) && b.first.try(:name) == predicate.left.name}
223
+ column, value = bind_values[idx]
199
224
  local_id = Shard.relative_id_for(value, current_source_shard, target_shard)
200
225
  local_id = [] if remove && local_id > Shard::IDS_PER_SHARD
201
- @bind_params[idx] = [column, local_id]
226
+ bind_values[idx] = [column, local_id]
202
227
  end
203
228
  predicate.right
204
229
  else
@@ -13,10 +13,10 @@ module Switchman
13
13
  end
14
14
  end
15
15
 
16
- def initialize_with_sharding(klass, table)
17
- initialize_without_sharding(klass, table)
18
- self.shard_value = Shard.current(klass.shard_category)
19
- self.shard_source_value = :implicit
16
+ def initialize_with_sharding(*args)
17
+ initialize_without_sharding(*args)
18
+ self.shard_value = Shard.current(klass.try(:shard_category) || :default) unless shard_value
19
+ self.shard_source_value = :implicit unless shard_source_value
20
20
  end
21
21
 
22
22
  def merge(*args)
@@ -1,17 +1,13 @@
1
1
  module Switchman
2
2
  module ActiveRecord
3
3
  module SpawnMethods
4
- def merge(r)
5
- return self unless r
6
- return to_a & r if r.is_a?(Array)
7
-
8
- # have to figure out shard stuff *before* conditions are merged
4
+ def shard_values_for_merge(r)
9
5
  if shard_value != r.shard_value
10
- if (r.shard_source_value == :implicit)
6
+ if r.shard_source_value == :implicit
11
7
  final_shard_value = shard_value
12
8
  final_primary_shard = primary_shard
13
9
  final_shard_source_value = shard_source_value
14
- elsif (shard_source_value == :implicit)
10
+ elsif shard_source_value == :implicit
15
11
  final_shard_value = r.shard_value
16
12
  final_primary_shard = r.primary_shard
17
13
  final_shard_source_value = r.shard_source_value
@@ -30,7 +26,6 @@ module Switchman
30
26
  final_primary_shard = Shard.default
31
27
  else
32
28
  final_shard_value = lhs_shard_value.to_a & rhs_shard_value.to_a
33
- return none if final_shard_value.length == 0
34
29
  final_primary_shard = final_shard_value.first
35
30
  final_shard_value = final_shard_value.first if final_shard_value.length == 1
36
31
  end
@@ -40,34 +35,86 @@ module Switchman
40
35
  shard_source_value == source_value || r.shard_source_value == source_value
41
36
  end
42
37
  raise "unknown shard_source_value" unless final_shard_source_value
43
-
44
- result = super
45
- result.shard_source_value = final_shard_source_value
46
- return result
47
38
  else
48
39
  # nothing fancy
49
- return super
50
40
  end
51
41
 
52
- # change the primary shard if necessary before merging
53
- result = if primary_shard != final_primary_shard && r.primary_shard != final_primary_shard
54
- lhs = shard(final_primary_shard)
55
- r = r.shard(final_primary_shard)
56
- lhs.merge(r)
57
- elsif primary_shard != final_primary_shard
58
- lhs = shard(final_primary_shard)
59
- lhs.merge(r)
60
- elsif r.primary_shard != final_primary_shard
61
- r = r.shard(final_primary_shard)
62
- super(r)
63
- else
64
- super
42
+ [final_shard_value, final_primary_shard, final_shard_source_value]
43
+ end
44
+
45
+ if ::Rails.version < '4'
46
+ def merge(r)
47
+ return self unless r
48
+ return super unless r.is_a?(Relation)
49
+
50
+ # have to figure out shard stuff *before* conditions are merged
51
+ final_shard_value, final_primary_shard, final_shard_source_value = shard_values_for_merge(r)
52
+
53
+ return super unless final_shard_source_value
54
+
55
+ if !final_shard_value
56
+ result = super
57
+ result.shard_source_value = final_shard_source_value
58
+ return result
59
+ end
60
+
61
+ return none if final_shard_value == []
62
+
63
+ # change the primary shard if necessary before merging
64
+ result = if primary_shard != final_primary_shard && r.primary_shard != final_primary_shard
65
+ lhs = shard(final_primary_shard)
66
+ r = r.shard(final_primary_shard)
67
+ lhs.merge(r)
68
+ elsif primary_shard != final_primary_shard
69
+ lhs = shard(final_primary_shard)
70
+ lhs.merge(r)
71
+ elsif r.primary_shard != final_primary_shard
72
+ r = r.shard(final_primary_shard)
73
+ super(r)
74
+ else
75
+ super
76
+ end
77
+
78
+ result.shard_value = final_shard_value
79
+ result.shard_source_value = final_shard_source_value
80
+
81
+ result
65
82
  end
83
+ else
84
+ def merge!(r)
85
+ # have to figure out shard stuff *before* conditions are merged
86
+ final_shard_value, final_primary_shard, final_shard_source_value = shard_values_for_merge(r)
87
+
88
+ return super unless final_shard_source_value
89
+
90
+ if !final_shard_value
91
+ super
92
+ self.shard_source_value = final_shard_source_value
93
+ return self
94
+ end
66
95
 
67
- result.shard_value = final_shard_value
68
- result.shard_source_value = final_shard_source_value
96
+ return none! if final_shard_value == []
69
97
 
70
- result
98
+ # change the primary shard if necessary before merging
99
+ if primary_shard != final_primary_shard && r.primary_shard != final_primary_shard
100
+ shard!(final_primary_shard)
101
+ r = r.shard(final_primary_shard)
102
+ super(r)
103
+ elsif primary_shard != final_primary_shard
104
+ shard!(final_primary_shard)
105
+ super(r)
106
+ elsif r.primary_shard != final_primary_shard
107
+ r = r.shard(final_primary_shard)
108
+ super(r)
109
+ else
110
+ super
111
+ end
112
+
113
+ self.shard_value = final_shard_value
114
+ self.shard_source_value = final_shard_source_value
115
+
116
+ self
117
+ end
71
118
  end
72
119
  end
73
120
  end
@@ -91,7 +91,8 @@ module Switchman
91
91
  end
92
92
  end
93
93
  end
94
- spec = ::ActiveRecord::Base::ConnectionSpecification.new(config, "#{config[:adapter]}_connection")
94
+ klass = ::Rails.version < '4' ? ::ActiveRecord::Base : ::ActiveRecord::ConnectionAdapters
95
+ spec = klass::ConnectionSpecification.new(config, "#{config[:adapter]}_connection")
95
96
  # unfortunately the AR code that does this require logic can't really be
96
97
  # called in isolation
97
98
  require "active_record/connection_adapters/#{config[:adapter]}_adapter"
@@ -125,7 +125,7 @@ module Switchman
125
125
  end
126
126
 
127
127
  def create_new_shard(options = {})
128
- raise MethodNotImplemented unless Shard.default.is_a?(Shard)
128
+ raise NotImplementedError.new("Cannot create new shards when sharding isn't initialized") unless Shard.default.is_a?(Shard)
129
129
 
130
130
  db_name = options[:db_name]
131
131
  create_schema = options[:schema]
@@ -193,13 +193,15 @@ module Switchman
193
193
  ::ActiveRecord::Base.connection.execute(stmt)
194
194
  end
195
195
  # have to disconnect and reconnect to the correct db
196
+ shard.name = db_name
196
197
  if self.shareable? && other_shard
197
198
  other_shard.activate { ::ActiveRecord::Base.connection }
198
199
  else
199
200
  ::ActiveRecord::Base.connection_pool.current_pool.disconnect!
200
201
  end
202
+ else
203
+ shard.name = db_name
201
204
  end
202
- shard.name = db_name
203
205
  old_proc = ::ActiveRecord::Base.connection.raw_connection.set_notice_processor {} if config[:adapter] == 'postgresql'
204
206
  old_verbose = ::ActiveRecord::Migration.verbose
205
207
  ::ActiveRecord::Migration.verbose = false
@@ -75,7 +75,7 @@ module Switchman
75
75
  ::ActiveRecord::Associations::Association.send(:include, ActiveRecord::Association)
76
76
  ::ActiveRecord::Associations::BelongsToAssociation.send(:include, ActiveRecord::BelongsToAssociation)
77
77
  ::ActiveRecord::Associations::CollectionProxy.send(:include, ActiveRecord::CollectionProxy)
78
- ::ActiveRecord::Associations::Builder::Association.send(:include, ActiveRecord::Builder::Association)
78
+ ::ActiveRecord::Associations::Builder::CollectionAssociation.send(:include, ActiveRecord::Builder::CollectionAssociation)
79
79
 
80
80
  ::ActiveRecord::Associations::Preloader::Association.send(:include, ActiveRecord::Preloader::Association)
81
81
  ::ActiveRecord::ConnectionAdapters::AbstractAdapter.send(:include, ActiveRecord::AbstractAdapter)
@@ -96,9 +96,13 @@ module Switchman
96
96
  shards.each do |shard|
97
97
  shard.activate do
98
98
  # this is how AR does it in fixtures.rb
99
- ::ActiveRecord::Base.connection.increment_open_transactions
100
- ::ActiveRecord::Base.connection.transaction_joinable = false
101
- ::ActiveRecord::Base.connection.begin_db_transaction
99
+ if ::Rails.version < '4'
100
+ ::ActiveRecord::Base.connection.increment_open_transactions
101
+ ::ActiveRecord::Base.connection.transaction_joinable = false
102
+ ::ActiveRecord::Base.connection.begin_db_transaction
103
+ else
104
+ ::ActiveRecord::Base.connection.begin_transaction joinable: false
105
+ end
102
106
  end
103
107
  end
104
108
  end
@@ -110,9 +114,13 @@ module Switchman
110
114
  shards << @shard1 unless @shard1.database_server == Shard.default.database_server
111
115
  shards.each do |shard|
112
116
  shard.activate do
113
- if ::ActiveRecord::Base.connection.open_transactions != 0
114
- ::ActiveRecord::Base.connection.rollback_db_transaction
115
- ::ActiveRecord::Base.connection.decrement_open_transactions
117
+ if ::Rails.version < '4'
118
+ if ::ActiveRecord::Base.connection.open_transactions != 0
119
+ ::ActiveRecord::Base.connection.rollback_db_transaction
120
+ ::ActiveRecord::Base.connection.decrement_open_transactions
121
+ end
122
+ else
123
+ ::ActiveRecord::Base.connection.rollback_transaction if ::ActiveRecord::Base.connection.transaction_open?
116
124
  end
117
125
  end
118
126
  end
@@ -1,21 +1,19 @@
1
1
  module Switchman::Rails
2
2
  module ClassMethods
3
- def cache_with_sharding
3
+ def cache
4
4
  Switchman::Shard.current.database_server.cache_store
5
5
  end
6
+ end
7
+
8
+ def self.included(klass)
9
+ klass.extend(ClassMethods)
10
+ klass.singleton_class.send(:remove_method, :cache)
6
11
 
7
12
  # in Rails 4+, the Rails.cache= method was used during bootstrap to set
8
13
  # Rails.cache(_without_sharding) to the value from the config file. but now
9
14
  # that that's done (the bootstrap happened before this module is included
10
15
  # into Rails), we want to make sure no one tries to assign to Rails.cache,
11
16
  # because it would be wrong w.r.t. sharding.
12
- def cache=(new_cache)
13
- raise NoMethodError
14
- end
15
- end
16
-
17
- def self.included(klass)
18
- klass.extend(ClassMethods)
19
- klass.singleton_class.alias_method_chain :cache, :sharding
17
+ klass.singleton_class.send(:remove_method, :cache=) if ::Rails.version >= '4'
20
18
  end
21
19
  end
@@ -5,9 +5,9 @@ module Switchman
5
5
  # recreate the default shard (it got buhleted)
6
6
  if Shard.default(true).is_a?(DefaultShard)
7
7
  begin
8
- Shard.create!(:default => true)
8
+ Shard.create!(default: true)
9
9
  rescue
10
- # database doesn't exist yet, presumably
10
+ # database doesn't exist yet, presumably cause we're creating it right now
11
11
  [nil, nil]
12
12
  end
13
13
  Shard.default(true)
@@ -15,7 +15,8 @@ module Switchman
15
15
 
16
16
  # can't auto-create a new shard on the default shard's db server if the
17
17
  # default shard is split across multiple db servers
18
- if ::ActiveRecord::Base.connection_handler.connection_pools.length > 1
18
+ if (::Rails.version < '4' ? ::ActiveRecord::Base.connection_handler.connection_pools.length :
19
+ ::ActiveRecord::Base.connection_handler.connection_pool_list.length) > 1
19
20
  server1 = DatabaseServer.create(:config => Shard.default.database_server.config)
20
21
  else
21
22
  server1 = Shard.default.database_server
@@ -1,3 +1,3 @@
1
1
  module Switchman
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.0"
3
3
  end
@@ -1,13 +1,11 @@
1
1
  class Appendage < ActiveRecord::Base
2
- attr_accessible :user, :user_id, :value
3
-
4
2
  belongs_to :user
5
3
  has_many :digits
6
4
 
7
5
  has_many :features, :as => :owner
8
6
 
9
- scope :has_no_value, where(:value => nil)
10
- scope :has_value, where("appendages.value IS NOT NULL")
7
+ scope :has_no_value, -> { where(:value => nil) }
8
+ scope :has_value, -> { where("appendages.value IS NOT NULL") }
11
9
 
12
10
  attr_writer :associated_shards
13
11
  class << self
@@ -1,9 +1,7 @@
1
1
  class Digit < ActiveRecord::Base
2
- attr_accessible :appendage, :appendage_id, :value
3
-
4
2
  belongs_to :appendage
5
3
  has_one :user, :through => :appendage
6
4
 
7
- scope :has_no_value, where(:value => nil)
8
- scope :has_value, where("digits.value IS NOT NULL")
5
+ scope :has_no_value, -> { where(:value => nil) }
6
+ scope :has_value, -> { where("digits.value IS NOT NULL") }
9
7
  end
@@ -1,5 +1,3 @@
1
1
  class Feature < ActiveRecord::Base
2
2
  belongs_to :owner, :polymorphic => true
3
-
4
- attr_accessible :owner, :value
5
3
  end