ar-octopus-ruby-3 0.11.2

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 (160) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +46 -0
  5. data/.rubocop_todo.yml +56 -0
  6. data/.travis.yml +18 -0
  7. data/Appraisals +16 -0
  8. data/Gemfile +4 -0
  9. data/README.mkdn +257 -0
  10. data/Rakefile +175 -0
  11. data/TODO.txt +7 -0
  12. data/ar-octopus.gemspec +44 -0
  13. data/gemfiles/rails42.gemfile +7 -0
  14. data/gemfiles/rails5.gemfile +7 -0
  15. data/gemfiles/rails51.gemfile +7 -0
  16. data/gemfiles/rails52.gemfile +7 -0
  17. data/lib/ar-octopus.rb +1 -0
  18. data/lib/octopus/abstract_adapter.rb +33 -0
  19. data/lib/octopus/association.rb +14 -0
  20. data/lib/octopus/association_shard_tracking.rb +74 -0
  21. data/lib/octopus/collection_association.rb +17 -0
  22. data/lib/octopus/collection_proxy.rb +16 -0
  23. data/lib/octopus/exception.rb +4 -0
  24. data/lib/octopus/finder_methods.rb +8 -0
  25. data/lib/octopus/load_balancing/round_robin.rb +20 -0
  26. data/lib/octopus/load_balancing.rb +4 -0
  27. data/lib/octopus/log_subscriber.rb +26 -0
  28. data/lib/octopus/migration.rb +236 -0
  29. data/lib/octopus/model.rb +216 -0
  30. data/lib/octopus/persistence.rb +45 -0
  31. data/lib/octopus/proxy.rb +399 -0
  32. data/lib/octopus/proxy_config.rb +251 -0
  33. data/lib/octopus/query_cache_for_shards.rb +24 -0
  34. data/lib/octopus/railtie.rb +11 -0
  35. data/lib/octopus/relation_proxy.rb +74 -0
  36. data/lib/octopus/result_patch.rb +19 -0
  37. data/lib/octopus/scope_proxy.rb +68 -0
  38. data/lib/octopus/shard_tracking/attribute.rb +22 -0
  39. data/lib/octopus/shard_tracking/dynamic.rb +11 -0
  40. data/lib/octopus/shard_tracking.rb +46 -0
  41. data/lib/octopus/singular_association.rb +9 -0
  42. data/lib/octopus/slave_group.rb +13 -0
  43. data/lib/octopus/version.rb +3 -0
  44. data/lib/octopus.rb +209 -0
  45. data/lib/tasks/octopus.rake +16 -0
  46. data/sample_app/.gitignore +4 -0
  47. data/sample_app/.rspec +1 -0
  48. data/sample_app/Gemfile +20 -0
  49. data/sample_app/Gemfile.lock +155 -0
  50. data/sample_app/README +3 -0
  51. data/sample_app/README.rdoc +261 -0
  52. data/sample_app/Rakefile +7 -0
  53. data/sample_app/app/assets/images/rails.png +0 -0
  54. data/sample_app/app/assets/javascripts/application.js +15 -0
  55. data/sample_app/app/assets/stylesheets/application.css +13 -0
  56. data/sample_app/app/controllers/application_controller.rb +4 -0
  57. data/sample_app/app/helpers/application_helper.rb +2 -0
  58. data/sample_app/app/mailers/.gitkeep +0 -0
  59. data/sample_app/app/models/.gitkeep +0 -0
  60. data/sample_app/app/models/item.rb +3 -0
  61. data/sample_app/app/models/user.rb +3 -0
  62. data/sample_app/app/views/layouts/application.html.erb +14 -0
  63. data/sample_app/autotest/discover.rb +2 -0
  64. data/sample_app/config/application.rb +62 -0
  65. data/sample_app/config/boot.rb +6 -0
  66. data/sample_app/config/cucumber.yml +8 -0
  67. data/sample_app/config/database.yml +28 -0
  68. data/sample_app/config/environment.rb +5 -0
  69. data/sample_app/config/environments/development.rb +37 -0
  70. data/sample_app/config/environments/production.rb +67 -0
  71. data/sample_app/config/environments/test.rb +37 -0
  72. data/sample_app/config/initializers/backtrace_silencers.rb +7 -0
  73. data/sample_app/config/initializers/inflections.rb +15 -0
  74. data/sample_app/config/initializers/mime_types.rb +5 -0
  75. data/sample_app/config/initializers/secret_token.rb +7 -0
  76. data/sample_app/config/initializers/session_store.rb +8 -0
  77. data/sample_app/config/initializers/wrap_parameters.rb +14 -0
  78. data/sample_app/config/locales/en.yml +5 -0
  79. data/sample_app/config/routes.rb +58 -0
  80. data/sample_app/config/shards.yml +28 -0
  81. data/sample_app/config.ru +4 -0
  82. data/sample_app/db/migrate/20100720172715_create_users.rb +15 -0
  83. data/sample_app/db/migrate/20100720172730_create_items.rb +16 -0
  84. data/sample_app/db/migrate/20100720210335_create_sample_users.rb +11 -0
  85. data/sample_app/db/schema.rb +29 -0
  86. data/sample_app/db/seeds.rb +16 -0
  87. data/sample_app/doc/README_FOR_APP +2 -0
  88. data/sample_app/features/migrate.feature +45 -0
  89. data/sample_app/features/seed.feature +15 -0
  90. data/sample_app/features/step_definitions/seeds_steps.rb +13 -0
  91. data/sample_app/features/step_definitions/web_steps.rb +218 -0
  92. data/sample_app/features/support/database.rb +13 -0
  93. data/sample_app/features/support/env.rb +57 -0
  94. data/sample_app/features/support/paths.rb +33 -0
  95. data/sample_app/lib/assets/.gitkeep +0 -0
  96. data/sample_app/lib/tasks/.gitkeep +0 -0
  97. data/sample_app/lib/tasks/cucumber.rake +64 -0
  98. data/sample_app/log/.gitkeep +0 -0
  99. data/sample_app/public/404.html +26 -0
  100. data/sample_app/public/422.html +26 -0
  101. data/sample_app/public/500.html +26 -0
  102. data/sample_app/public/favicon.ico +0 -0
  103. data/sample_app/public/images/rails.png +0 -0
  104. data/sample_app/public/index.html +279 -0
  105. data/sample_app/public/javascripts/application.js +2 -0
  106. data/sample_app/public/javascripts/controls.js +965 -0
  107. data/sample_app/public/javascripts/dragdrop.js +974 -0
  108. data/sample_app/public/javascripts/effects.js +1123 -0
  109. data/sample_app/public/javascripts/prototype.js +4874 -0
  110. data/sample_app/public/javascripts/rails.js +118 -0
  111. data/sample_app/public/robots.txt +5 -0
  112. data/sample_app/public/stylesheets/.gitkeep +0 -0
  113. data/sample_app/script/cucumber +10 -0
  114. data/sample_app/script/rails +6 -0
  115. data/sample_app/spec/models/item_spec.rb +5 -0
  116. data/sample_app/spec/models/user_spec.rb +5 -0
  117. data/sample_app/spec/spec_helper.rb +27 -0
  118. data/sample_app/vendor/assets/javascripts/.gitkeep +0 -0
  119. data/sample_app/vendor/assets/stylesheets/.gitkeep +0 -0
  120. data/sample_app/vendor/plugins/.gitkeep +0 -0
  121. data/spec/config/shards.yml +231 -0
  122. data/spec/migrations/10_create_users_using_replication.rb +9 -0
  123. data/spec/migrations/11_add_field_in_all_slaves.rb +11 -0
  124. data/spec/migrations/12_create_users_using_block.rb +23 -0
  125. data/spec/migrations/13_create_users_using_block_and_using.rb +15 -0
  126. data/spec/migrations/14_create_users_on_shards_of_a_group_with_versions.rb +11 -0
  127. data/spec/migrations/15_create_user_on_shards_of_default_group_with_versions.rb +9 -0
  128. data/spec/migrations/1_create_users_on_master.rb +9 -0
  129. data/spec/migrations/2_create_users_on_canada.rb +11 -0
  130. data/spec/migrations/3_create_users_on_both_shards.rb +11 -0
  131. data/spec/migrations/4_create_users_on_shards_of_a_group.rb +11 -0
  132. data/spec/migrations/5_create_users_on_multiples_groups.rb +11 -0
  133. data/spec/migrations/6_raise_exception_with_invalid_shard_name.rb +11 -0
  134. data/spec/migrations/7_raise_exception_with_invalid_multiple_shard_names.rb +11 -0
  135. data/spec/migrations/8_raise_exception_with_invalid_group_name.rb +11 -0
  136. data/spec/migrations/9_raise_exception_with_multiple_invalid_group_names.rb +11 -0
  137. data/spec/octopus/association_shard_tracking_spec.rb +1036 -0
  138. data/spec/octopus/collection_proxy_spec.rb +16 -0
  139. data/spec/octopus/load_balancing/round_robin_spec.rb +15 -0
  140. data/spec/octopus/log_subscriber_spec.rb +19 -0
  141. data/spec/octopus/migration_spec.rb +151 -0
  142. data/spec/octopus/model_spec.rb +837 -0
  143. data/spec/octopus/octopus_spec.rb +123 -0
  144. data/spec/octopus/proxy_spec.rb +303 -0
  145. data/spec/octopus/query_cache_for_shards_spec.rb +40 -0
  146. data/spec/octopus/relation_proxy_spec.rb +132 -0
  147. data/spec/octopus/replicated_slave_grouped_spec.rb +91 -0
  148. data/spec/octopus/replication_spec.rb +196 -0
  149. data/spec/octopus/scope_proxy_spec.rb +97 -0
  150. data/spec/octopus/sharded_replicated_slave_grouped_spec.rb +55 -0
  151. data/spec/octopus/sharded_spec.rb +33 -0
  152. data/spec/spec_helper.rb +18 -0
  153. data/spec/support/active_record/connection_adapters/modify_config_adapter.rb +15 -0
  154. data/spec/support/database_connection.rb +4 -0
  155. data/spec/support/database_models.rb +118 -0
  156. data/spec/support/octopus_helper.rb +66 -0
  157. data/spec/support/query_count.rb +17 -0
  158. data/spec/support/shared_contexts.rb +18 -0
  159. data/spec/tasks/octopus.rake_spec.rb +32 -0
  160. metadata +351 -0
@@ -0,0 +1,45 @@
1
+ module Octopus
2
+ module Persistence
3
+ def update_attribute(*args)
4
+ run_on_shard { super }
5
+ end
6
+
7
+ def update_attributes(*args)
8
+ run_on_shard { super }
9
+ end
10
+
11
+ def update_attributes!(*args)
12
+ run_on_shard { super }
13
+ end
14
+
15
+ def reload(*args)
16
+ run_on_shard { super }
17
+ end
18
+
19
+ def delete
20
+ run_on_shard { super }
21
+ end
22
+
23
+ def destroy
24
+ run_on_shard { super }
25
+ end
26
+
27
+ def touch(*args)
28
+ run_on_shard { super }
29
+ end
30
+
31
+ def update_column(*args)
32
+ run_on_shard { super }
33
+ end
34
+
35
+ def increment!(...)
36
+ run_on_shard { super(...) }
37
+ end
38
+
39
+ def decrement!(*args)
40
+ run_on_shard { super }
41
+ end
42
+ end
43
+ end
44
+
45
+ ActiveRecord::Base.send(:include, Octopus::Persistence)
@@ -0,0 +1,399 @@
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
22
+
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
41
+
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
47
+
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
54
+
55
+ def delete(*args, &block)
56
+ legacy_method_missing_logic('delete', *args, &block)
57
+ end
58
+
59
+ def select_all(*args, &block)
60
+ legacy_method_missing_logic('select_all', *args, &block)
61
+ end
62
+
63
+ def select_value(*args, &block)
64
+ legacy_method_missing_logic('select_value', *args, &block)
65
+ end
66
+
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
78
+
79
+ def select_connection
80
+ safe_connection(shards[shard_name])
81
+ end
82
+
83
+ def run_queries_on_shard(shard, &_block)
84
+ keeping_connection_proxy(shard) do
85
+ using_shard(shard) do
86
+ yield
87
+ end
88
+ end
89
+ end
90
+
91
+ def send_queries_to_multiple_shards(shards, &block)
92
+ shards.map do |shard|
93
+ run_queries_on_shard(shard, &block)
94
+ end
95
+ end
96
+
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
101
+ end
102
+
103
+ def send_queries_to_all_shards(&block)
104
+ send_queries_to_multiple_shards(shard_names.uniq { |shard_name| shards[shard_name] }, &block)
105
+ end
106
+
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
113
+
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
119
+
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
129
+
130
+ def method_missing(method, *args, &block)
131
+ legacy_method_missing_logic(method, *args, &block)
132
+ end
133
+
134
+ def respond_to?(method, include_private = false)
135
+ super || select_connection.respond_to?(method, include_private)
136
+ end
137
+
138
+ def connection_pool
139
+ shards[current_shard]
140
+ end
141
+
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
147
+
148
+ def disable_query_cache!
149
+ with_each_healthy_shard { |v| v.connected? && safe_connection(v).disable_query_cache! }
150
+ end
151
+ end
152
+
153
+ def clear_query_cache
154
+ with_each_healthy_shard { |v| v.connected? && safe_connection(v).clear_query_cache }
155
+ end
156
+
157
+ def clear_active_connections!
158
+ with_each_healthy_shard(&:release_connection)
159
+ end
160
+
161
+ def clear_all_connections!
162
+ with_each_healthy_shard(&:disconnect!)
163
+
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
171
+
172
+ def connected?
173
+ shards.any? { |_k, v| v.connected? }
174
+ end
175
+
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?
178
+ end
179
+
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)
182
+ end
183
+
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
187
+
188
+ def send_queries_to_slave_group(method, *args, &block)
189
+ send_queries_to_balancer(slave_groups[current_slave_group], method, *args, &block)
190
+ end
191
+
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
201
+ end
202
+ end
203
+
204
+ def initialize_metadata_table
205
+ select_connection.transaction { ActiveRecord::InternalMetadata.create_table }
206
+ end
207
+
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
+ clean_connection_proxy
216
+ preparable = check_preparable_arg(args)
217
+
218
+ args.pop unless preparable.nil?
219
+
220
+ if args.present?
221
+ val = select_connection.send(method, *args, **preparable, &block)
222
+ else
223
+ val = select_connection.send(method, *args, &block)
224
+ end
225
+
226
+ # conn.send(method, *args, &block)
227
+ elsif should_send_queries_to_shard_slave_group?(method)
228
+ send_queries_to_shard_slave_group(method, *args, &block)
229
+ elsif should_send_queries_to_slave_group?(method)
230
+ send_queries_to_slave_group(method, *args, &block)
231
+ elsif should_send_queries_to_replicated_databases?(method)
232
+ send_queries_to_selected_slave(method, *args, &block)
233
+ else
234
+ preparable = check_preparable_arg(args)
235
+
236
+ args.pop unless preparable.nil?
237
+
238
+ if args.present?
239
+ val = select_connection.send(method, *args, **preparable, &block)
240
+ else
241
+ val = select_connection.send(method, *args, &block)
242
+ end
243
+
244
+ if val.instance_of? ActiveRecord::Result
245
+ val.current_shard = shard_name
246
+ end
247
+
248
+ val
249
+ end
250
+ end
251
+
252
+ def check_preparable_arg(args)
253
+ selected_hashs = args.select{ |arg| arg.is_a? Hash }.first
254
+
255
+ return selected_hashs if selected_hashs.present?
256
+
257
+ {preparable: nil}
258
+ end
259
+
260
+ # Ensure that a single failing slave doesn't take down the entire application
261
+ def with_each_healthy_shard
262
+ shards.each do |shard_name, v|
263
+ begin
264
+ yield(v)
265
+ rescue => e
266
+ if Octopus.robust_environment?
267
+ Octopus.logger.error "Error on shard #{shard_name}: #{e.message}"
268
+ else
269
+ raise
270
+ end
271
+ end
272
+ end
273
+
274
+ ar_pools = ActiveRecord::Base.connection_handler.connection_pool_list
275
+
276
+ ar_pools.each do |pool|
277
+ next if pool == shards[:master] # Already handled this
278
+
279
+ begin
280
+ yield(pool)
281
+ rescue => e
282
+ if Octopus.robust_environment?
283
+ Octopus.logger.error "Error on pool (spec: #{pool.spec}): #{e.message}"
284
+ else
285
+ raise
286
+ end
287
+ end
288
+ end
289
+ end
290
+
291
+ def should_clean_connection_proxy?(method)
292
+ method.to_s =~ /insert|select|execute/ && !current_model_replicated? && (!block || block != current_shard)
293
+ end
294
+
295
+ # Try to use slaves if and only if `replicated: true` is specified in `shards.yml` and no slaves groups are defined
296
+ def should_send_queries_to_replicated_databases?(method)
297
+ replicated && method.to_s =~ /select/ && !block && !slaves_grouped?
298
+ end
299
+
300
+ def send_queries_to_selected_slave(method, *args, &block)
301
+ if current_model.replicated || fully_replicated?
302
+ selected_slave = slaves_load_balancer.next current_load_balance_options
303
+ else
304
+ selected_slave = Octopus.master_shard
305
+ end
306
+
307
+ send_queries_to_slave(selected_slave, method, *args, &block)
308
+ end
309
+
310
+ # We should use slaves if and only if its safe to do so.
311
+ #
312
+ # We can safely use slaves when:
313
+ # (1) `replicated: true` is specified in `shards.yml`
314
+ # (2) The current model is `replicated()`, or `fully_replicated: true` is specified in `shards.yml` which means that
315
+ # all the model is `replicated()`
316
+ # (3) It's a SELECT query
317
+ # while ensuring that we revert `current_shard` from the selected slave to the (shard's) master
318
+ # not to make queries other than SELECT leak to the slave.
319
+ def should_use_slaves_for_method?(method)
320
+ current_model_replicated? && method.to_s =~ /select/
321
+ end
322
+
323
+ def slaves_grouped?
324
+ slave_groups.present?
325
+ end
326
+
327
+ # Temporarily switch `current_shard` to the next slave in a slave group and send queries to it
328
+ # while preserving `current_shard`
329
+ def send_queries_to_balancer(balancer, method, *args, &block)
330
+ send_queries_to_slave(balancer.next(current_load_balance_options), method, *args, &block)
331
+ end
332
+
333
+ # Temporarily switch `current_shard` to the specified slave and send queries to it
334
+ # while preserving `current_shard`
335
+ def send_queries_to_slave(slave, method, *args, &block)
336
+ using_shard(slave) do
337
+ preparable = check_preparable_arg(args)
338
+
339
+ args.pop unless preparable.nil?
340
+
341
+ if args.present?
342
+ val = select_connection.send(method, *args, **preparable, &block)
343
+ else
344
+ val = select_connection.send(method, *args, &block)
345
+ end
346
+
347
+ if val.instance_of? ActiveRecord::Result
348
+ val.current_shard = slave
349
+ end
350
+ val
351
+ end
352
+ end
353
+
354
+ # Temporarily block cleaning connection proxy and run the block
355
+ #
356
+ # @see Octopus::Proxy#should_clean_connection?
357
+ # @see Octopus::Proxy#clean_connection_proxy
358
+ def keeping_connection_proxy(shard, &_block)
359
+ last_block = block
360
+
361
+ begin
362
+ self.block = shard
363
+ yield
364
+ ensure
365
+ self.block = last_block || nil
366
+ end
367
+ end
368
+
369
+ # Temporarily switch `current_shard` and run the block
370
+ def using_shard(shard, &_block)
371
+ older_shard = current_shard
372
+ older_slave_group = current_slave_group
373
+ older_load_balance_options = current_load_balance_options
374
+
375
+ begin
376
+ unless current_model && !current_model.allowed_shard?(shard)
377
+ self.current_shard = shard
378
+ end
379
+ yield
380
+ ensure
381
+ self.current_shard = older_shard
382
+ self.current_slave_group = older_slave_group
383
+ self.current_load_balance_options = older_load_balance_options
384
+ end
385
+ end
386
+
387
+ # Temporarily switch `current_group` and run the block
388
+ def using_group(group, &_block)
389
+ older_group = current_group
390
+
391
+ begin
392
+ self.current_group = group
393
+ yield
394
+ ensure
395
+ self.current_group = older_group
396
+ end
397
+ end
398
+ end
399
+ end
@@ -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