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
data/lib/octopus/proxy.rb CHANGED
@@ -1,300 +1,365 @@
1
- require "set"
1
+ require 'set'
2
+ require 'octopus/slave_group'
3
+ require 'octopus/load_balancing/round_robin'
4
+
5
+ module Octopus
6
+ class Proxy
7
+ attr_accessor :proxy_config
8
+
9
+ delegate :current_model, :current_model=,
10
+ :current_shard, :current_shard=,
11
+ :current_group, :current_group=,
12
+ :current_slave_group, :current_slave_group=,
13
+ :current_load_balance_options, :current_load_balance_options=,
14
+ :block, :block=, :fully_replicated?, :has_group?,
15
+ :shard_names, :shards_for_group, :shards, :sharded, :slaves_list,
16
+ :shards_slave_groups, :slave_groups, :replicated, :slaves_load_balancer,
17
+ :config, :initialize_shards, :shard_name, to: :proxy_config, prefix: false
18
+
19
+ def initialize(config = Octopus.config)
20
+ self.proxy_config = Octopus::ProxyConfig.new(config)
21
+ end
2
22
 
3
- class Octopus::Proxy
4
- attr_accessor :config
23
+ # Rails Connection Methods - Those methods are overriden to add custom behavior that helps
24
+ # Octopus introduce Sharding / Replication.
25
+ delegate :adapter_name, :add_transaction_record, :case_sensitive_modifier,
26
+ :type_cast, :to_sql, :quote, :quote_column_name, :quote_table_name,
27
+ :quote_table_name_for_assignment, :supports_migrations?, :table_alias_for,
28
+ :table_exists?, :in_clause_length, :supports_ddl_transactions?,
29
+ :sanitize_limit, :prefetch_primary_key?, :current_database,
30
+ :combine_bind_parameters, :empty_insert_statement_value, :assume_migrated_upto_version,
31
+ :schema_cache, :substitute_at, :internal_string_options_for_primary_key, :lookup_cast_type_from_column,
32
+ :supports_advisory_locks?, :get_advisory_lock, :initialize_internal_metadata_table,
33
+ :release_advisory_lock, :prepare_binds_for_database, :cacheable_query, :column_name_for_operation,
34
+ :prepared_statements, :transaction_state, :create_table, to: :select_connection
35
+
36
+ def execute(sql, name = nil)
37
+ conn = select_connection
38
+ clean_connection_proxy if should_clean_connection_proxy?('execute')
39
+ conn.execute(sql, name)
40
+ end
5
41
 
6
- def initialize(config = Octopus.config)
7
- initialize_shards(config)
8
- initialize_replication(config) if !config.nil? && config["replicated"]
9
- end
42
+ def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
43
+ conn = select_connection
44
+ clean_connection_proxy if should_clean_connection_proxy?('insert')
45
+ conn.insert(arel, name, pk, id_value, sequence_name, binds)
46
+ end
10
47
 
11
- def initialize_shards(config)
12
- @shards = HashWithIndifferentAccess.new
13
- @groups = {}
14
- @adapters = Set.new
15
- @config = ActiveRecord::Base.connection_pool_without_octopus.connection.instance_variable_get(:@config)
48
+ def update(arel, name = nil, binds = [])
49
+ conn = select_connection
50
+ # Call the legacy should_clean_connection_proxy? method here, emulating an insert.
51
+ clean_connection_proxy if should_clean_connection_proxy?('insert')
52
+ conn.update(arel, name, binds)
53
+ end
16
54
 
17
- if !config.nil?
18
- @entire_sharded = config['entire_sharded']
19
- shards_config = config[Octopus.rails_env()]
55
+ def delete(*args, &block)
56
+ legacy_method_missing_logic('delete', *args, &block)
20
57
  end
21
58
 
22
- shards_config ||= []
59
+ def select_all(*args, &block)
60
+ legacy_method_missing_logic('select_all', *args, &block)
61
+ end
23
62
 
24
- shards_config.each do |key, value|
25
- if value.is_a?(String)
26
- value = resolve_string_connection(value).merge(:octopus_shard => key)
27
- initialize_adapter(value['adapter'])
28
- @shards[key.to_sym] = connection_pool_for(value, "#{value['adapter']}_connection")
29
- elsif value.is_a?(Hash) && value.has_key?("adapter")
30
- value.merge!(:octopus_shard => key)
31
- initialize_adapter(value['adapter'])
32
- @shards[key.to_sym] = connection_pool_for(value, "#{value['adapter']}_connection")
33
- elsif value.is_a?(Hash)
34
- @groups[key.to_s] = []
63
+ def select_value(*args, &block)
64
+ legacy_method_missing_logic('select_value', *args, &block)
65
+ end
35
66
 
36
- value.each do |k, v|
37
- raise "You have duplicated shard names!" if @shards.has_key?(k.to_sym)
67
+ # Rails 3.1 sets automatic_reconnect to false when it removes
68
+ # connection pool. Octopus can potentially retain a reference to a closed
69
+ # connection pool. Previously, that would work since the pool would just
70
+ # reconnect, but in Rails 3.1 the flag prevents this.
71
+ def safe_connection(connection_pool)
72
+ connection_pool.automatic_reconnect ||= true
73
+ if !connection_pool.connected? && shards[Octopus.master_shard].connection.query_cache_enabled
74
+ connection_pool.connection.enable_query_cache!
75
+ end
76
+ connection_pool.connection
77
+ end
38
78
 
39
- initialize_adapter(v['adapter'])
40
- config_with_octopus_shard = v.merge(:octopus_shard => k)
79
+ def select_connection
80
+ safe_connection(shards[shard_name])
81
+ end
41
82
 
42
- @shards[k.to_sym] = connection_pool_for(config_with_octopus_shard, "#{v['adapter']}_connection")
43
- @groups[key.to_s] << k.to_sym
83
+ def run_queries_on_shard(shard, &_block)
84
+ keeping_connection_proxy(shard) do
85
+ using_shard(shard) do
86
+ yield
44
87
  end
45
88
  end
46
89
  end
47
90
 
48
- @shards[:master] ||= ActiveRecord::Base.connection_pool_without_octopus()
49
- end
50
-
51
- def initialize_replication(config)
52
- @replicated = true
53
- if config.has_key?("fully_replicated")
54
- @fully_replicated = config["fully_replicated"]
55
- else
56
- @fully_replicated = true
91
+ def send_queries_to_multiple_shards(shards, &block)
92
+ shards.map do |shard|
93
+ run_queries_on_shard(shard, &block)
94
+ end
57
95
  end
58
96
 
59
- @slaves_list = @shards.keys.map {|sym| sym.to_s}.sort
60
- @slaves_list.delete('master')
61
- @slave_index = 0
62
- end
63
-
64
- def current_model
65
- Thread.current["octopus.current_model"]
66
- end
67
-
68
- def current_model=(model)
69
- Thread.current["octopus.current_model"] = model.is_a?(ActiveRecord::Base) ? model.class : model
70
- end
71
-
72
- def current_shard
73
- Thread.current["octopus.current_shard"] ||= :master
74
- end
75
-
76
- def current_shard=(shard_symbol)
77
- if shard_symbol.is_a?(Array)
78
- shard_symbol.each {|symbol| raise "Nonexistent Shard Name: #{symbol}" if @shards[symbol].nil? }
79
- else
80
- raise "Nonexistent Shard Name: #{shard_symbol}" if @shards[shard_symbol].nil?
97
+ def send_queries_to_group(group, &block)
98
+ using_group(group) do
99
+ send_queries_to_multiple_shards(shards_for_group(group), &block)
100
+ end
81
101
  end
82
102
 
83
- Thread.current["octopus.current_shard"] = shard_symbol
84
- end
85
-
86
- def current_group
87
- Thread.current["octopus.current_group"]
88
- end
89
-
90
- def current_group=(group_symbol)
91
- # TODO: Error message should include all groups if given more than one bad name.
92
- [group_symbol].flatten.compact.each do |group|
93
- raise "Nonexistent Group Name: #{group}" unless has_group?(group)
103
+ def send_queries_to_all_shards(&block)
104
+ send_queries_to_multiple_shards(shard_names.uniq { |shard_name| shards[shard_name] }, &block)
94
105
  end
95
106
 
96
- Thread.current["octopus.current_group"] = group_symbol
97
- end
107
+ def clean_connection_proxy
108
+ self.current_shard = Octopus.master_shard
109
+ self.current_model = nil
110
+ self.current_group = nil
111
+ self.block = nil
112
+ end
98
113
 
99
- def block
100
- Thread.current["octopus.block"]
101
- end
114
+ def check_schema_migrations(shard)
115
+ OctopusModel.using(shard).connection.table_exists?(
116
+ ActiveRecord::Migrator.schema_migrations_table_name,
117
+ ) || OctopusModel.using(shard).connection.initialize_schema_migrations_table
118
+ end
102
119
 
103
- def block=(block)
104
- Thread.current["octopus.block"] = block
105
- end
120
+ def transaction(options = {}, &block)
121
+ if !sharded && current_model_replicated?
122
+ run_queries_on_shard(Octopus.master_shard) do
123
+ select_connection.transaction(options, &block)
124
+ end
125
+ else
126
+ select_connection.transaction(options, &block)
127
+ end
128
+ end
106
129
 
107
- def last_current_shard
108
- Thread.current["octopus.last_current_shard"]
109
- end
130
+ def method_missing(method, *args, &block)
131
+ legacy_method_missing_logic(method, *args, &block)
132
+ end
110
133
 
111
- def last_current_shard=(last_current_shard)
112
- Thread.current["octopus.last_current_shard"] = last_current_shard
113
- end
134
+ def respond_to?(method, include_private = false)
135
+ super || select_connection.respond_to?(method, include_private)
136
+ end
114
137
 
115
- # Public: Whether or not a group exists with the given name converted to a
116
- # string.
117
- #
118
- # Returns a boolean.
119
- def has_group?(group)
120
- @groups.has_key?(group.to_s)
121
- end
138
+ def connection_pool
139
+ shards[current_shard]
140
+ end
122
141
 
123
- # Public: Retrieves names of all loaded shards.
124
- #
125
- # Returns an array of shard names as symbols
126
- def shard_names
127
- @shards.keys
128
- end
142
+ if Octopus.rails4?
143
+ def enable_query_cache!
144
+ clear_query_cache
145
+ with_each_healthy_shard { |v| v.connected? && safe_connection(v).enable_query_cache! }
146
+ end
129
147
 
130
- # Public: Retrieves the defined shards for a given group.
131
- #
132
- # Returns an array of shard names as symbols or nil if the group is not
133
- # defined.
134
- def shards_for_group(group)
135
- @groups.fetch(group.to_s, nil)
136
- end
148
+ def disable_query_cache!
149
+ with_each_healthy_shard { |v| v.connected? && safe_connection(v).disable_query_cache! }
150
+ end
151
+ end
137
152
 
138
- # Rails 3.1 sets automatic_reconnect to false when it removes
139
- # connection pool. Octopus can potentially retain a reference to a closed
140
- # connection pool. Previously, that would work since the pool would just
141
- # reconnect, but in Rails 3.1 the flag prevents this.
142
- def safe_connection(connection_pool)
143
- connection_pool.automatic_reconnect ||= true
144
- connection_pool.connection()
145
- end
153
+ def clear_query_cache
154
+ with_each_healthy_shard { |v| v.connected? && safe_connection(v).clear_query_cache }
155
+ end
146
156
 
147
- def select_connection
148
- safe_connection(@shards[shard_name])
149
- end
157
+ def clear_active_connections!
158
+ with_each_healthy_shard(&:release_connection)
159
+ end
150
160
 
151
- def shard_name
152
- current_shard.is_a?(Array) ? current_shard.first : current_shard
153
- end
161
+ def clear_all_connections!
162
+ with_each_healthy_shard(&:disconnect!)
154
163
 
155
- def should_clean_table_name?
156
- @adapters.size > 1
157
- end
164
+ if Octopus.atleast_rails52?
165
+ # On Rails 5.2 it is no longer safe to re-use connection pools after they have been discarded
166
+ # This happens on webservers with forking, for example Phusion Passenger.
167
+ # Therefor after we clear all connections we reinitialize the shards to get fresh and not discarded ConnectionPool objects
168
+ proxy_config.reinitialize_shards
169
+ end
170
+ end
158
171
 
159
- def run_queries_on_shard(shard, &block)
160
- older_shard = self.current_shard
161
- last_block = self.block
172
+ def connected?
173
+ shards.any? { |_k, v| v.connected? }
174
+ end
162
175
 
163
- begin
164
- self.block = true
165
- self.current_shard = shard
166
- yield
167
- ensure
168
- self.block = last_block || false
169
- self.current_shard = older_shard
176
+ def should_send_queries_to_shard_slave_group?(method)
177
+ should_use_slaves_for_method?(method) && shards_slave_groups.try(:[], current_shard).try(:[], current_slave_group).present?
170
178
  end
171
- end
172
179
 
173
- def send_queries_to_multiple_shards(shards, &block)
174
- shards.each do |shard|
175
- self.run_queries_on_shard(shard, &block)
180
+ def send_queries_to_shard_slave_group(method, *args, &block)
181
+ send_queries_to_balancer(shards_slave_groups[current_shard][current_slave_group], method, *args, &block)
176
182
  end
177
- end
178
183
 
179
- def clean_proxy()
180
- self.current_shard = :master
181
- self.current_group = nil
182
- self.block = false
183
- end
184
+ def should_send_queries_to_slave_group?(method)
185
+ should_use_slaves_for_method?(method) && slave_groups.try(:[], current_slave_group).present?
186
+ end
184
187
 
185
- def check_schema_migrations(shard)
186
- if !OctopusModel.using(shard).connection.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name())
187
- OctopusModel.using(shard).connection.initialize_schema_migrations_table
188
+ def send_queries_to_slave_group(method, *args, &block)
189
+ send_queries_to_balancer(slave_groups[current_slave_group], method, *args, &block)
188
190
  end
189
- end
190
191
 
191
- def transaction(options = {}, &block)
192
- if @replicated && (current_model.replicated || @fully_replicated)
193
- self.run_queries_on_shard(:master) do
194
- select_connection.transaction(options, &block)
192
+ def current_model_replicated?
193
+ replicated && (current_model.try(:replicated) || fully_replicated?)
194
+ end
195
+
196
+ def initialize_schema_migrations_table
197
+ if Octopus.atleast_rails52?
198
+ select_connection.transaction { ActiveRecord::SchemaMigration.create_table }
199
+ else
200
+ select_connection.initialize_schema_migrations_table
195
201
  end
196
- else
197
- select_connection.transaction(options, &block)
198
202
  end
199
- end
200
-
201
- def method_missing(method, *args, &block)
202
- if should_clean_connection?(method)
203
- conn = select_connection()
204
- self.last_current_shard = self.current_shard
205
- clean_proxy()
206
- conn.send(method, *args, &block)
207
- elsif should_send_queries_to_replicated_databases?(method)
208
- send_queries_to_selected_slave(method, *args, &block)
209
- else
210
- select_connection().send(method, *args, &block)
203
+
204
+ def initialize_metadata_table
205
+ select_connection.transaction { ActiveRecord::InternalMetadata.create_table }
211
206
  end
212
- end
213
207
 
214
- def respond_to?(method, include_private = false)
215
- super || select_connection.respond_to?(method, include_private)
216
- end
208
+ protected
209
+
210
+ # @thiagopradi - This legacy method missing logic will be keep for a while for compatibility
211
+ # and will be removed when Octopus 1.0 will be released.
212
+ # We are planning to migrate to a much stable logic for the Proxy that doesn't require method missing.
213
+ def legacy_method_missing_logic(method, *args, &block)
214
+ if should_clean_connection_proxy?(method)
215
+ conn = select_connection
216
+ clean_connection_proxy
217
+ conn.send(method, *args, &block)
218
+ elsif should_send_queries_to_shard_slave_group?(method)
219
+ send_queries_to_shard_slave_group(method, *args, &block)
220
+ elsif should_send_queries_to_slave_group?(method)
221
+ send_queries_to_slave_group(method, *args, &block)
222
+ elsif should_send_queries_to_replicated_databases?(method)
223
+ send_queries_to_selected_slave(method, *args, &block)
224
+ else
225
+ val = select_connection.send(method, *args, &block)
217
226
 
218
- def connection_pool
219
- return @shards[current_shard]
220
- end
227
+ if val.instance_of? ActiveRecord::Result
228
+ val.current_shard = shard_name
229
+ end
221
230
 
222
- def enable_query_cache!
223
- clear_query_cache
224
- @shards.each { |k, v| safe_connection(v).enable_query_cache! }
225
- end
231
+ val
232
+ end
233
+ end
226
234
 
227
- def disable_query_cache!
228
- @shards.each { |k, v| safe_connection(v).disable_query_cache! }
229
- end
235
+ # Ensure that a single failing slave doesn't take down the entire application
236
+ def with_each_healthy_shard
237
+ shards.each do |shard_name, v|
238
+ begin
239
+ yield(v)
240
+ rescue => e
241
+ if Octopus.robust_environment?
242
+ Octopus.logger.error "Error on shard #{shard_name}: #{e.message}"
243
+ else
244
+ raise
245
+ end
246
+ end
247
+ end
230
248
 
231
- def clear_query_cache
232
- @shards.each { |k, v| safe_connection(v).clear_query_cache }
233
- end
249
+ ar_pools = ActiveRecord::Base.connection_handler.connection_pool_list
234
250
 
235
- def clear_active_connections!
236
- @shards.each { |k, v| v.release_connection }
237
- end
251
+ ar_pools.each do |pool|
252
+ next if pool == shards[:master] # Already handled this
238
253
 
239
- def clear_all_connections!
240
- @shards.each { |k, v| v.disconnect! }
241
- end
254
+ begin
255
+ yield(pool)
256
+ rescue => e
257
+ if Octopus.robust_environment?
258
+ Octopus.logger.error "Error on pool (spec: #{pool.spec}): #{e.message}"
259
+ else
260
+ raise
261
+ end
262
+ end
263
+ end
264
+ end
242
265
 
243
- def connected?
244
- @shards.any? { |k, v| v.connected? }
245
- end
266
+ def should_clean_connection_proxy?(method)
267
+ method.to_s =~ /insert|select|execute/ && !current_model_replicated? && (!block || block != current_shard)
268
+ end
246
269
 
247
- protected
270
+ # Try to use slaves if and only if `replicated: true` is specified in `shards.yml` and no slaves groups are defined
271
+ def should_send_queries_to_replicated_databases?(method)
272
+ replicated && method.to_s =~ /select/ && !block && !slaves_grouped?
273
+ end
248
274
 
249
- def connection_pool_for(adapter, config)
250
- if Octopus.rails4?
251
- arg = ActiveRecord::ConnectionAdapters::ConnectionSpecification.new(adapter.dup, config)
252
- else
253
- arg = ActiveRecord::Base::ConnectionSpecification.new(adapter.dup, config)
275
+ def send_queries_to_selected_slave(method, *args, &block)
276
+ if current_model.replicated || fully_replicated?
277
+ selected_slave = slaves_load_balancer.next current_load_balance_options
278
+ else
279
+ selected_slave = Octopus.master_shard
280
+ end
281
+
282
+ send_queries_to_slave(selected_slave, method, *args, &block)
254
283
  end
255
284
 
256
- ActiveRecord::ConnectionAdapters::ConnectionPool.new(arg)
257
- end
285
+ # We should use slaves if and only if its safe to do so.
286
+ #
287
+ # We can safely use slaves when:
288
+ # (1) `replicated: true` is specified in `shards.yml`
289
+ # (2) The current model is `replicated()`, or `fully_replicated: true` is specified in `shards.yml` which means that
290
+ # all the model is `replicated()`
291
+ # (3) It's a SELECT query
292
+ # while ensuring that we revert `current_shard` from the selected slave to the (shard's) master
293
+ # not to make queries other than SELECT leak to the slave.
294
+ def should_use_slaves_for_method?(method)
295
+ current_model_replicated? && method.to_s =~ /select/
296
+ end
258
297
 
259
- def initialize_adapter(adapter)
260
- @adapters << adapter
261
- begin
262
- require "active_record/connection_adapters/#{adapter}_adapter"
263
- rescue LoadError
264
- raise "Please install the #{adapter} adapter: `gem install activerecord-#{adapter}-adapter` (#{$!})"
298
+ def slaves_grouped?
299
+ slave_groups.present?
265
300
  end
266
- end
267
301
 
268
- def resolve_string_connection(spec)
269
- if Octopus.rails4?
270
- resolver = ActiveRecord::ConnectionAdapters::ConnectionSpecification::Resolver.new(spec, {})
271
- else
272
- resolver = ActiveRecord::Base::ConnectionSpecification::Resolver.new(spec, {})
302
+ # Temporarily switch `current_shard` to the next slave in a slave group and send queries to it
303
+ # while preserving `current_shard`
304
+ def send_queries_to_balancer(balancer, method, *args, &block)
305
+ send_queries_to_slave(balancer.next(current_load_balance_options), method, *args, &block)
273
306
  end
274
- resolver.spec.config.stringify_keys
275
- end
276
307
 
277
- def should_clean_connection?(method)
278
- method.to_s =~ /insert|select|execute/ && !@replicated && !self.block
279
- end
308
+ # Temporarily switch `current_shard` to the specified slave and send queries to it
309
+ # while preserving `current_shard`
310
+ def send_queries_to_slave(slave, method, *args, &block)
311
+ using_shard(slave) do
312
+ val = select_connection.send(method, *args, &block)
313
+ if val.instance_of? ActiveRecord::Result
314
+ val.current_shard = slave
315
+ end
316
+ val
317
+ end
318
+ end
280
319
 
281
- def should_send_queries_to_replicated_databases?(method)
282
- @replicated && method.to_s =~ /select/ && !self.block
283
- end
320
+ # Temporarily block cleaning connection proxy and run the block
321
+ #
322
+ # @see Octopus::Proxy#should_clean_connection?
323
+ # @see Octopus::Proxy#clean_connection_proxy
324
+ def keeping_connection_proxy(shard, &_block)
325
+ last_block = block
326
+
327
+ begin
328
+ self.block = shard
329
+ yield
330
+ ensure
331
+ self.block = last_block || nil
332
+ end
333
+ end
284
334
 
285
- def send_queries_to_selected_slave(method, *args, &block)
286
- old_shard = self.current_shard
335
+ # Temporarily switch `current_shard` and run the block
336
+ def using_shard(shard, &_block)
337
+ older_shard = current_shard
338
+ older_slave_group = current_slave_group
339
+ older_load_balance_options = current_load_balance_options
287
340
 
288
- begin
289
- if current_model.replicated || @fully_replicated
290
- self.current_shard = @slaves_list[@slave_index = (@slave_index + 1) % @slaves_list.length]
291
- else
292
- self.current_shard = :master
341
+ begin
342
+ unless current_model && !current_model.allowed_shard?(shard)
343
+ self.current_shard = shard
344
+ end
345
+ yield
346
+ ensure
347
+ self.current_shard = older_shard
348
+ self.current_slave_group = older_slave_group
349
+ self.current_load_balance_options = older_load_balance_options
293
350
  end
351
+ end
352
+
353
+ # Temporarily switch `current_group` and run the block
354
+ def using_group(group, &_block)
355
+ older_group = current_group
294
356
 
295
- select_connection.send(method, *args, &block)
296
- ensure
297
- self.current_shard = old_shard
357
+ begin
358
+ self.current_group = group
359
+ yield
360
+ ensure
361
+ self.current_group = older_group
362
+ end
298
363
  end
299
364
  end
300
365
  end