ar-octopus 0.8.1 → 0.10.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (109) hide show
  1. checksums.yaml +6 -14
  2. data/.gitignore +1 -0
  3. data/.rspec +1 -1
  4. data/.rubocop.yml +46 -0
  5. data/.rubocop_todo.yml +56 -0
  6. data/.travis.yml +7 -12
  7. data/Appraisals +11 -4
  8. data/Gemfile +1 -1
  9. data/README.mkdn +138 -63
  10. data/Rakefile +23 -16
  11. data/ar-octopus.gemspec +23 -20
  12. data/gemfiles/rails42.gemfile +7 -0
  13. data/gemfiles/{rails32.gemfile → rails5.gemfile} +2 -2
  14. data/gemfiles/{rails4.gemfile → rails51.gemfile} +2 -2
  15. data/gemfiles/rails52.gemfile +7 -0
  16. data/lib/ar-octopus.rb +1 -1
  17. data/lib/octopus/{rails3/abstract_adapter.rb → abstract_adapter.rb} +4 -15
  18. data/lib/octopus/association.rb +8 -99
  19. data/lib/octopus/association_shard_tracking.rb +74 -0
  20. data/lib/octopus/collection_association.rb +17 -0
  21. data/lib/octopus/collection_proxy.rb +16 -0
  22. data/lib/octopus/exception.rb +4 -0
  23. data/lib/octopus/finder_methods.rb +8 -0
  24. data/lib/octopus/load_balancing/round_robin.rb +20 -0
  25. data/lib/octopus/load_balancing.rb +4 -0
  26. data/lib/octopus/{rails3/log_subscriber.rb → log_subscriber.rb} +6 -2
  27. data/lib/octopus/migration.rb +187 -110
  28. data/lib/octopus/model.rb +151 -131
  29. data/lib/octopus/persistence.rb +45 -0
  30. data/lib/octopus/proxy.rb +297 -232
  31. data/lib/octopus/proxy_config.rb +251 -0
  32. data/lib/octopus/query_cache_for_shards.rb +24 -0
  33. data/lib/octopus/railtie.rb +1 -3
  34. data/lib/octopus/relation_proxy.rb +70 -0
  35. data/lib/octopus/result_patch.rb +19 -0
  36. data/lib/octopus/scope_proxy.rb +54 -36
  37. data/lib/octopus/shard_tracking/attribute.rb +22 -0
  38. data/lib/octopus/shard_tracking/dynamic.rb +11 -0
  39. data/lib/octopus/shard_tracking.rb +46 -0
  40. data/lib/octopus/singular_association.rb +9 -0
  41. data/lib/octopus/slave_group.rb +13 -0
  42. data/lib/octopus/version.rb +1 -1
  43. data/lib/octopus.rb +125 -33
  44. data/lib/tasks/octopus.rake +2 -2
  45. data/sample_app/Gemfile +3 -3
  46. data/sample_app/autotest/discover.rb +2 -2
  47. data/sample_app/config/application.rb +1 -1
  48. data/sample_app/config/boot.rb +1 -1
  49. data/sample_app/config/environments/test.rb +1 -1
  50. data/sample_app/config/initializers/session_store.rb +1 -1
  51. data/sample_app/config/initializers/wrap_parameters.rb +1 -1
  52. data/sample_app/config/routes.rb +1 -1
  53. data/sample_app/db/migrate/20100720210335_create_sample_users.rb +2 -2
  54. data/sample_app/db/schema.rb +10 -10
  55. data/sample_app/db/seeds.rb +3 -3
  56. data/sample_app/features/step_definitions/seeds_steps.rb +4 -4
  57. data/sample_app/features/step_definitions/web_steps.rb +3 -4
  58. data/sample_app/features/support/env.rb +3 -4
  59. data/sample_app/features/support/paths.rb +4 -4
  60. data/sample_app/lib/tasks/cucumber.rake +43 -44
  61. data/sample_app/spec/spec_helper.rb +3 -3
  62. data/spec/config/shards.yml +78 -0
  63. data/spec/migrations/10_create_users_using_replication.rb +4 -4
  64. data/spec/migrations/11_add_field_in_all_slaves.rb +4 -4
  65. data/spec/migrations/12_create_users_using_block.rb +8 -8
  66. data/spec/migrations/13_create_users_using_block_and_using.rb +5 -5
  67. data/spec/migrations/14_create_users_on_shards_of_a_group_with_versions.rb +3 -3
  68. data/spec/migrations/15_create_user_on_shards_of_default_group_with_versions.rb +3 -3
  69. data/spec/migrations/1_create_users_on_master.rb +4 -4
  70. data/spec/migrations/2_create_users_on_canada.rb +4 -4
  71. data/spec/migrations/3_create_users_on_both_shards.rb +4 -4
  72. data/spec/migrations/4_create_users_on_shards_of_a_group.rb +4 -4
  73. data/spec/migrations/5_create_users_on_multiples_groups.rb +3 -3
  74. data/spec/migrations/6_raise_exception_with_invalid_shard_name.rb +4 -4
  75. data/spec/migrations/7_raise_exception_with_invalid_multiple_shard_names.rb +4 -4
  76. data/spec/migrations/8_raise_exception_with_invalid_group_name.rb +4 -4
  77. data/spec/migrations/9_raise_exception_with_multiple_invalid_group_names.rb +5 -5
  78. data/spec/octopus/association_shard_tracking_spec.rb +1036 -0
  79. data/spec/octopus/collection_proxy_spec.rb +16 -0
  80. data/spec/octopus/load_balancing/round_robin_spec.rb +15 -0
  81. data/spec/octopus/log_subscriber_spec.rb +5 -5
  82. data/spec/octopus/migration_spec.rb +83 -49
  83. data/spec/octopus/model_spec.rb +544 -292
  84. data/spec/octopus/octopus_spec.rb +64 -31
  85. data/spec/octopus/proxy_spec.rb +145 -141
  86. data/spec/octopus/query_cache_for_shards_spec.rb +40 -0
  87. data/spec/octopus/relation_proxy_spec.rb +132 -0
  88. data/spec/octopus/replicated_slave_grouped_spec.rb +91 -0
  89. data/spec/octopus/replication_spec.rb +140 -65
  90. data/spec/octopus/scope_proxy_spec.rb +90 -10
  91. data/spec/octopus/sharded_replicated_slave_grouped_spec.rb +55 -0
  92. data/spec/octopus/sharded_spec.rb +10 -10
  93. data/spec/spec_helper.rb +8 -6
  94. data/spec/support/active_record/connection_adapters/modify_config_adapter.rb +1 -3
  95. data/spec/support/database_connection.rb +2 -2
  96. data/spec/support/database_models.rb +18 -17
  97. data/spec/support/octopus_helper.rb +32 -25
  98. data/spec/support/query_count.rb +1 -3
  99. data/spec/support/shared_contexts.rb +3 -3
  100. data/spec/tasks/octopus.rake_spec.rb +10 -10
  101. metadata +112 -70
  102. data/.ruby-version +0 -1
  103. data/init.rb +0 -1
  104. data/lib/octopus/association_collection.rb +0 -49
  105. data/lib/octopus/has_and_belongs_to_many_association.rb +0 -17
  106. data/lib/octopus/rails3/persistence.rb +0 -39
  107. data/lib/octopus/rails3/singular_association.rb +0 -34
  108. data/rails/init.rb +0 -1
  109. data/spec/octopus/association_spec.rb +0 -712
@@ -0,0 +1,251 @@
1
+ module Octopus
2
+ class ProxyConfig
3
+ CURRENT_MODEL_KEY = 'octopus.current_model'.freeze
4
+ CURRENT_SHARD_KEY = 'octopus.current_shard'.freeze
5
+ CURRENT_GROUP_KEY = 'octopus.current_group'.freeze
6
+ CURRENT_SLAVE_GROUP_KEY = 'octopus.current_slave_group'.freeze
7
+ CURRENT_LOAD_BALANCE_OPTIONS_KEY = 'octopus.current_load_balance_options'.freeze
8
+ BLOCK_KEY = 'octopus.block'.freeze
9
+ FULLY_REPLICATED_KEY = 'octopus.fully_replicated'.freeze
10
+
11
+ attr_accessor :config, :sharded, :shards, :shards_slave_groups, :slave_groups,
12
+ :adapters, :replicated, :slaves_load_balancer, :slaves_list, :shards_slave_groups,
13
+ :slave_groups, :groups, :entire_sharded, :shards_config
14
+
15
+ def initialize(config)
16
+ initialize_shards(config)
17
+ initialize_replication(config) if !config.nil? && config['replicated']
18
+ end
19
+
20
+ def current_model
21
+ Thread.current[CURRENT_MODEL_KEY]
22
+ end
23
+
24
+ def current_model=(model)
25
+ Thread.current[CURRENT_MODEL_KEY] = model.is_a?(ActiveRecord::Base) ? model.class : model
26
+ end
27
+
28
+ def current_shard
29
+ Thread.current[CURRENT_SHARD_KEY] ||= Octopus.master_shard
30
+ end
31
+
32
+ def current_shard=(shard_symbol)
33
+ if shard_symbol.is_a?(Array)
34
+ self.current_slave_group = nil
35
+ shard_symbol.each { |symbol| fail "Nonexistent Shard Name: #{symbol}" if shards[symbol].nil? }
36
+ elsif shard_symbol.is_a?(Hash)
37
+ hash = shard_symbol
38
+ shard_symbol = hash[:shard]
39
+ slave_group_symbol = hash[:slave_group]
40
+ load_balance_options = hash[:load_balance_options]
41
+
42
+ if shard_symbol.nil? && slave_group_symbol.nil?
43
+ fail 'Neither shard or slave group must be specified'
44
+ end
45
+
46
+ if shard_symbol.present?
47
+ fail "Nonexistent Shard Name: #{shard_symbol}" if shards[shard_symbol].nil?
48
+ end
49
+
50
+ if slave_group_symbol.present?
51
+ if (shards_slave_groups.try(:[], shard_symbol).present? && shards_slave_groups[shard_symbol][slave_group_symbol].nil?) ||
52
+ (shards_slave_groups.try(:[], shard_symbol).nil? && @slave_groups[slave_group_symbol].nil?)
53
+ fail "Nonexistent Slave Group Name: #{slave_group_symbol} in shards config: #{shards_config.inspect}"
54
+ end
55
+ end
56
+ self.current_slave_group = slave_group_symbol
57
+ self.current_load_balance_options = load_balance_options
58
+ else
59
+ fail "Nonexistent Shard Name: #{shard_symbol}" if shards[shard_symbol].nil?
60
+ end
61
+
62
+ Thread.current[CURRENT_SHARD_KEY] = shard_symbol
63
+ end
64
+
65
+ def current_group
66
+ Thread.current[CURRENT_GROUP_KEY]
67
+ end
68
+
69
+ def current_group=(group_symbol)
70
+ # TODO: Error message should include all groups if given more than one bad name.
71
+ [group_symbol].flatten.compact.each do |group|
72
+ fail "Nonexistent Group Name: #{group}" unless has_group?(group)
73
+ end
74
+
75
+ Thread.current[CURRENT_GROUP_KEY] = group_symbol
76
+ end
77
+
78
+ def current_slave_group
79
+ Thread.current[CURRENT_SLAVE_GROUP_KEY]
80
+ end
81
+
82
+ def current_slave_group=(slave_group_symbol)
83
+ Thread.current[CURRENT_SLAVE_GROUP_KEY] = slave_group_symbol
84
+ Thread.current[CURRENT_LOAD_BALANCE_OPTIONS_KEY] = nil if slave_group_symbol.nil?
85
+ end
86
+
87
+ def current_load_balance_options
88
+ Thread.current[CURRENT_LOAD_BALANCE_OPTIONS_KEY]
89
+ end
90
+
91
+ def current_load_balance_options=(options)
92
+ Thread.current[CURRENT_LOAD_BALANCE_OPTIONS_KEY] = options
93
+ end
94
+
95
+ def block
96
+ Thread.current[BLOCK_KEY]
97
+ end
98
+
99
+ def block=(block)
100
+ Thread.current[BLOCK_KEY] = block
101
+ end
102
+
103
+ def fully_replicated?
104
+ @fully_replicated || Thread.current[FULLY_REPLICATED_KEY]
105
+ end
106
+
107
+ # Public: Whether or not a group exists with the given name converted to a
108
+ # string.
109
+ #
110
+ # Returns a boolean.
111
+ def has_group?(group)
112
+ @groups.key?(group.to_s)
113
+ end
114
+
115
+ # Public: Retrieves names of all loaded shards.
116
+ #
117
+ # Returns an array of shard names as symbols
118
+ def shard_names
119
+ shards.keys
120
+ end
121
+
122
+ def shard_name
123
+ current_shard.is_a?(Array) ? current_shard.first : current_shard
124
+ end
125
+
126
+ # Public: Retrieves the defined shards for a given group.
127
+ #
128
+ # Returns an array of shard names as symbols or nil if the group is not
129
+ # defined.
130
+ def shards_for_group(group)
131
+ @groups.fetch(group.to_s, nil)
132
+ end
133
+
134
+ def initialize_shards(config)
135
+ @original_config = config
136
+
137
+ self.shards = HashWithIndifferentAccess.new
138
+ self.shards_slave_groups = HashWithIndifferentAccess.new
139
+ self.slave_groups = HashWithIndifferentAccess.new
140
+ self.groups = {}
141
+ self.config = ActiveRecord::Base.connection_pool_without_octopus.spec.config
142
+
143
+ unless config.nil?
144
+ self.entire_sharded = config['entire_sharded']
145
+ self.shards_config = config[Octopus.rails_env]
146
+ end
147
+
148
+ self.shards_config ||= []
149
+
150
+ shards_config.each do |key, value|
151
+ if value.is_a?(String)
152
+ value = resolve_string_connection(value).merge(:octopus_shard => key)
153
+ initialize_adapter(value['adapter'])
154
+ shards[key.to_sym] = connection_pool_for(value, "#{value['adapter']}_connection")
155
+ elsif value.is_a?(Hash) && value.key?('adapter')
156
+ value.merge!(:octopus_shard => key)
157
+ initialize_adapter(value['adapter'])
158
+ shards[key.to_sym] = connection_pool_for(value, "#{value['adapter']}_connection")
159
+
160
+ slave_group_configs = value.select do |_k, v|
161
+ structurally_slave_group? v
162
+ end
163
+
164
+ if slave_group_configs.present?
165
+ slave_groups = HashWithIndifferentAccess.new
166
+ slave_group_configs.each do |slave_group_name, slave_configs|
167
+ slaves = HashWithIndifferentAccess.new
168
+ slave_configs.each do |slave_name, slave_config|
169
+ shards[slave_name.to_sym] = connection_pool_for(slave_config, "#{value['adapter']}_connection")
170
+ slaves[slave_name.to_sym] = slave_name.to_sym
171
+ end
172
+ slave_groups[slave_group_name.to_sym] = Octopus::SlaveGroup.new(slaves)
173
+ end
174
+ @shards_slave_groups[key.to_sym] = slave_groups
175
+ @sharded = true
176
+ end
177
+ elsif value.is_a?(Hash)
178
+ @groups[key.to_s] = []
179
+
180
+ value.each do |k, v|
181
+ fail 'You have duplicated shard names!' if shards.key?(k.to_sym)
182
+
183
+ initialize_adapter(v['adapter'])
184
+ config_with_octopus_shard = v.merge(:octopus_shard => k)
185
+
186
+ shards[k.to_sym] = connection_pool_for(config_with_octopus_shard, "#{v['adapter']}_connection")
187
+ @groups[key.to_s] << k.to_sym
188
+ end
189
+
190
+ if structurally_slave_group? value
191
+ slaves = Hash[@groups[key.to_s].map { |v| [v, v] }]
192
+ @slave_groups[key.to_sym] = Octopus::SlaveGroup.new(slaves)
193
+ end
194
+ end
195
+ end
196
+
197
+ shards[:master] ||= ActiveRecord::Base.connection_pool_without_octopus if Octopus.master_shard == :master
198
+ end
199
+
200
+ def initialize_replication(config)
201
+ @replicated = true
202
+ if config.key?('fully_replicated')
203
+ @fully_replicated = config['fully_replicated']
204
+ else
205
+ @fully_replicated = true
206
+ end
207
+
208
+ @slaves_list = shards.keys.map(&:to_s).sort
209
+ @slaves_list.delete('master')
210
+ @slaves_load_balancer = Octopus.load_balancer.new(@slaves_list)
211
+ end
212
+
213
+ def reinitialize_shards
214
+ initialize_shards(@original_config)
215
+ end
216
+
217
+ private
218
+
219
+ def connection_pool_for(config, adapter)
220
+ if Octopus.rails4?
221
+ spec = ActiveRecord::ConnectionAdapters::ConnectionSpecification.new(config.dup, adapter )
222
+ else
223
+ name = adapter["octopus_shard"]
224
+ spec = ActiveRecord::ConnectionAdapters::ConnectionSpecification.new(name, config.dup, adapter)
225
+ end
226
+
227
+ ActiveRecord::ConnectionAdapters::ConnectionPool.new(spec)
228
+ end
229
+
230
+ def resolve_string_connection(spec)
231
+ resolver = ActiveRecord::ConnectionAdapters::ConnectionSpecification::Resolver.new({})
232
+ HashWithIndifferentAccess.new(resolver.spec(spec).config)
233
+ end
234
+
235
+ def structurally_slave?(config)
236
+ config.is_a?(Hash) && config.key?('adapter')
237
+ end
238
+
239
+ def structurally_slave_group?(config)
240
+ config.is_a?(Hash) && config.values.any? { |v| structurally_slave? v }
241
+ end
242
+
243
+ def initialize_adapter(adapter)
244
+ begin
245
+ require "active_record/connection_adapters/#{adapter}_adapter"
246
+ rescue LoadError
247
+ raise "Please install the #{adapter} adapter: `gem install activerecord-#{adapter}-adapter` (#{$ERROR_INFO})"
248
+ end
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,24 @@
1
+ # query cache methods are moved to ConnectionPool for Rails >= 5.0
2
+ module Octopus
3
+ module ConnectionPool
4
+ module QueryCacheForShards
5
+ %i(enable_query_cache! disable_query_cache!).each do |method|
6
+ define_method(method) do
7
+ if(Octopus.enabled? && (shards = ActiveRecord::Base.connection.shards)['master'] == self)
8
+ shards.each do |shard_name, v|
9
+ if shard_name == 'master'
10
+ super()
11
+ else
12
+ v.public_send(method)
13
+ end
14
+ end
15
+ else
16
+ super()
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ ActiveRecord::ConnectionAdapters::ConnectionPool.send(:prepend, Octopus::ConnectionPool::QueryCacheForShards)
@@ -1,10 +1,8 @@
1
1
  begin
2
- require "rails/railtie"
3
-
4
2
  module Octopus
5
3
  class Railtie < Rails::Railtie
6
4
  rake_tasks do
7
- Dir[File.join(File.dirname(__FILE__), "../tasks/*.rake")].each { |ext| load ext }
5
+ Dir[File.join(File.dirname(__FILE__), '../tasks/*.rake')].each { |ext| load ext }
8
6
  end
9
7
  end
10
8
  end
@@ -0,0 +1,70 @@
1
+ module Octopus
2
+ class RelationProxy < BasicObject
3
+ include ::Octopus::ShardTracking::Attribute
4
+
5
+ module CaseFixer
6
+ def ===(other)
7
+ other = other.ar_relation while ::Octopus::RelationProxy === other
8
+ super
9
+ end
10
+ end
11
+
12
+ attr_accessor :ar_relation
13
+
14
+ def initialize(shard, ar_relation)
15
+ @current_shard = shard
16
+ @ar_relation = ar_relation
17
+ end
18
+
19
+ def respond_to?(*args)
20
+ method_missing(:respond_to?, *args)
21
+ end
22
+
23
+ # methods redefined in ActiveRecord that should run_on_shard
24
+ ENUM_METHODS = (::Enumerable.instance_methods - ::Object.instance_methods).reject do |m|
25
+ ::ActiveRecord::Relation.instance_method(m).source_location rescue nil
26
+ end + [:each, :map, :index_by]
27
+ # `find { ... }` etc. should run_on_shard, `find(id)` should be sent to relation
28
+ ENUM_WITH_BLOCK_METHODS = [:find, :select, :none?, :any?, :one?, :many?, :sum]
29
+ BATCH_METHODS = [:find_each, :find_in_batches, :in_batches]
30
+ WHERE_CHAIN_METHODS = [:not]
31
+
32
+ def method_missing(method, *args, &block)
33
+ if !block && BATCH_METHODS.include?(method)
34
+ ::Enumerator.new do |yielder|
35
+ run_on_shard do
36
+ @ar_relation.public_send(method, *args) do |batch_item|
37
+ yielder << batch_item
38
+ end
39
+ end
40
+ end
41
+ elsif ENUM_METHODS.include?(method) || block && ENUM_WITH_BLOCK_METHODS.include?(method)
42
+ run_on_shard { @ar_relation.to_a }.public_send(method, *args, &block)
43
+ elsif WHERE_CHAIN_METHODS.include?(method)
44
+ ::Octopus::ScopeProxy.new(@current_shard, run_on_shard { @ar_relation.public_send(method, *args) } )
45
+ elsif block
46
+ @ar_relation.public_send(method, *args, &block)
47
+ else
48
+ run_on_shard do
49
+ if method == :load_records
50
+ @ar_relation.send(method, *args)
51
+ else
52
+ @ar_relation.public_send(method, *args)
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ def ==(other)
59
+ case other
60
+ when ::Octopus::RelationProxy
61
+ method_missing(:==, other.ar_relation)
62
+ else
63
+ method_missing(:==, other)
64
+ end
65
+ end
66
+ alias_method :eql?, :==
67
+ end
68
+ end
69
+
70
+ ActiveRecord::Relation.extend(Octopus::RelationProxy::CaseFixer)
@@ -0,0 +1,19 @@
1
+ module Octopus::ResultPatch
2
+ attr_accessor :current_shard
3
+
4
+ private
5
+
6
+ def hash_rows
7
+ if current_shard.blank?
8
+ super
9
+ else
10
+ foo = super
11
+ foo.each { |f| f.merge!('current_shard' => current_shard) }
12
+ foo
13
+ end
14
+ end
15
+ end
16
+
17
+ class ActiveRecord::Result
18
+ prepend Octopus::ResultPatch
19
+ end
@@ -1,50 +1,68 @@
1
- class Octopus::ScopeProxy
2
- attr_accessor :shard, :klass
1
+ module Octopus
2
+ class ScopeProxy < BasicObject
3
+ include ::Octopus::ShardTracking::Attribute
3
4
 
4
- def initialize(shard, klass)
5
- @shard = shard
6
- @klass = klass
7
- end
5
+ module CaseFixer
6
+ def ===(other)
7
+ other = other.klass while ::Octopus::ScopeProxy === other
8
+ super
9
+ end
10
+ end
8
11
 
9
- def using(shard)
10
- raise "Nonexistent Shard Name: #{shard}" if @klass.connection.instance_variable_get(:@shards)[shard].nil?
11
- @shard = shard
12
- return self
13
- end
12
+ attr_accessor :klass
13
+
14
+ # Dup and clone should be delegated to the class.
15
+ # We want to dup the query, not the scope proxy.
16
+ delegate :dup, :clone, to: :klass
14
17
 
15
- # Transaction Method send all queries to a specified shard.
16
- def transaction(options = {}, &block)
17
- @klass.connection.run_queries_on_shard(@shard) do
18
- @klass = @klass.connection().transaction(options, &block)
18
+ def initialize(shard, klass)
19
+ @current_shard = shard
20
+ @klass = klass
19
21
  end
20
- end
21
22
 
22
- def connection
23
- @klass.connection().current_shard = @shard
24
- @klass.connection()
25
- end
23
+ def using(shard)
24
+ fail "Nonexistent Shard Name: #{shard}" if @klass.connection.shards[shard].nil?
25
+ @current_shard = shard
26
+ self
27
+ end
26
28
 
27
- def method_missing(method, *args, &block)
28
- result = @klass.connection.run_queries_on_shard(@shard) do
29
- @klass.send(method, *args, &block)
29
+ # Transaction Method send all queries to a specified shard.
30
+ def transaction(options = {}, &block)
31
+ run_on_shard { klass.transaction(options, &block) }
30
32
  end
31
33
 
32
- if result.respond_to?(:scoped)
33
- @klass = result
34
- return self
34
+ def connection
35
+ @klass.connection_proxy.current_shard = @current_shard
36
+
37
+ if @klass.custom_octopus_connection && @klass.allowed_shard?(@current_shard)
38
+ # Force use of proxy, given we called 'using' explicitly to get here
39
+ @klass.connection_proxy.current_model = @klass
40
+ @klass.connection_proxy
41
+ else
42
+ @klass.connection
43
+ end
35
44
  end
36
45
 
37
- result
38
- end
46
+ def method_missing(method, *args, &block)
47
+ result = run_on_shard { @klass.__send__(method, *args, &block) }
48
+ if result.respond_to?(:all)
49
+ return ::Octopus::ScopeProxy.new(current_shard, result)
50
+ end
39
51
 
40
- def as_json(options = nil)
41
- method_missing(:as_json, options)
42
- end
52
+ if result.respond_to?(:current_shard)
53
+ result.current_shard = current_shard
54
+ end
43
55
 
44
- # Delegates to method_missing (instead of @klass) so that User.using(:blah).where(:name => "Mike")
45
- # gets run in the correct shard context when #== is evaluated.
46
- def ==(*args)
47
- method_missing(:==, *args)
56
+ result
57
+ end
58
+
59
+ # Delegates to method_missing (instead of @klass) so that User.using(:blah).where(:name => "Mike")
60
+ # gets run in the correct shard context when #== is evaluated.
61
+ def ==(other)
62
+ method_missing(:==, other)
63
+ end
64
+ alias_method :eql?, :==
48
65
  end
49
- alias :eql? :==
50
66
  end
67
+
68
+ ActiveRecord::Relation.extend(Octopus::ScopeProxy::CaseFixer)
@@ -0,0 +1,22 @@
1
+ # Adds current_shard as an attribute; provide a default
2
+ # implementation of set_current_shard which considers
3
+ # only the current ActiveRecord::Base.connection_proxy
4
+ module Octopus
5
+ module ShardTracking
6
+ module Attribute
7
+ def self.included(base)
8
+ base.send(:include, Octopus::ShardTracking)
9
+ end
10
+
11
+ attr_accessor :current_shard
12
+
13
+ def set_current_shard
14
+ return unless Octopus.enabled?
15
+
16
+ if ActiveRecord::Base.connection_proxy.block
17
+ self.current_shard = ActiveRecord::Base.connection_proxy.current_shard
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ require 'octopus/shard_tracking'
2
+
3
+ module Octopus
4
+ module ShardTracking
5
+ module Dynamic
6
+ def self.included(base)
7
+ base.send(:include, Octopus::ShardTracking)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,46 @@
1
+ module Octopus
2
+ module ShardTracking
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+ # If the class which includes this module responds to the class
9
+ # method sharded_methods, then automagically alias_method_chain
10
+ # a sharding-friendly version of each of those methods into existence
11
+ def sharded_methods(*methods)
12
+ methods.each { |m| create_sharded_method(m) }
13
+ end
14
+
15
+ def create_sharded_method(name)
16
+ name.to_s =~ /([^!?]+)([!?])?/
17
+ method, punctuation = [Regexp.last_match[1], Regexp.last_match[2]]
18
+ with = :"#{method}_with_octopus#{punctuation}"
19
+ without = :"#{method}_without_octopus#{punctuation}"
20
+ define_method with do |*args, &block|
21
+ run_on_shard { send(without, *args, &block) }
22
+ end
23
+ alias_method without.to_sym, name.to_sym
24
+ alias_method name.to_sym, with.to_sym
25
+ end
26
+ end
27
+
28
+ # Adds run_on_shard method, but does not implement current_shard method
29
+ def run_on_shard(&block)
30
+ if (cs = current_shard)
31
+ r = ActiveRecord::Base.connection_proxy.run_queries_on_shard(cs, &block)
32
+ # Use a case statement to avoid any path through ActiveRecord::Delegation's
33
+ # respond_to? code. We want to avoid the respond_to? code because it can have
34
+ # the side effect of causing a call to load_target
35
+
36
+ if (ActiveRecord::Relation === r || ActiveRecord::QueryMethods::WhereChain === r) && !(Octopus::RelationProxy === r)
37
+ Octopus::RelationProxy.new(cs, r)
38
+ else
39
+ r
40
+ end
41
+ else
42
+ yield
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,9 @@
1
+ module Octopus
2
+ module SingularAssociation
3
+ def self.included(base)
4
+ base.sharded_methods :reader, :writer, :create, :create!, :build
5
+ end
6
+ end
7
+ end
8
+
9
+ ActiveRecord::Associations::SingularAssociation.send(:include, Octopus::SingularAssociation)
@@ -0,0 +1,13 @@
1
+ module Octopus
2
+ class SlaveGroup
3
+ def initialize(slaves)
4
+ slaves = HashWithIndifferentAccess.new(slaves)
5
+ slaves_list = slaves.values
6
+ @load_balancer = Octopus.load_balancer.new(slaves_list)
7
+ end
8
+
9
+ def next(options)
10
+ @load_balancer.next options
11
+ end
12
+ end
13
+ end
@@ -1,3 +1,3 @@
1
1
  module Octopus
2
- VERSION = '0.8.1'
2
+ VERSION = '0.10.2'
3
3
  end