activerecord-turntable 1.0.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 (130) hide show
  1. data/.document +5 -0
  2. data/.gitignore +25 -0
  3. data/.rspec +3 -0
  4. data/Gemfile +25 -0
  5. data/Guardfile +9 -0
  6. data/LICENSE.txt +20 -0
  7. data/README.rdoc +290 -0
  8. data/Rakefile +101 -0
  9. data/activerecord-turntable.gemspec +47 -0
  10. data/lib/active_record/turntable.rb +58 -0
  11. data/lib/active_record/turntable/active_record_ext.rb +26 -0
  12. data/lib/active_record/turntable/active_record_ext/.gitkeep +0 -0
  13. data/lib/active_record/turntable/active_record_ext/abstract_adapter.rb +50 -0
  14. data/lib/active_record/turntable/active_record_ext/clever_load.rb +90 -0
  15. data/lib/active_record/turntable/active_record_ext/fixtures.rb +131 -0
  16. data/lib/active_record/turntable/active_record_ext/log_subscriber.rb +64 -0
  17. data/lib/active_record/turntable/active_record_ext/persistence.rb +95 -0
  18. data/lib/active_record/turntable/active_record_ext/schema_dumper.rb +107 -0
  19. data/lib/active_record/turntable/active_record_ext/sequencer.rb +28 -0
  20. data/lib/active_record/turntable/active_record_ext/transactions.rb +33 -0
  21. data/lib/active_record/turntable/algorithm.rb +7 -0
  22. data/lib/active_record/turntable/algorithm/.gitkeep +0 -0
  23. data/lib/active_record/turntable/algorithm/base.rb +11 -0
  24. data/lib/active_record/turntable/algorithm/range_algorithm.rb +37 -0
  25. data/lib/active_record/turntable/algorithm/range_bsearch_algorithm.rb +41 -0
  26. data/lib/active_record/turntable/base.rb +130 -0
  27. data/lib/active_record/turntable/cluster.rb +70 -0
  28. data/lib/active_record/turntable/compatible.rb +19 -0
  29. data/lib/active_record/turntable/config.rb +24 -0
  30. data/lib/active_record/turntable/connection_proxy.rb +218 -0
  31. data/lib/active_record/turntable/connection_proxy/mixable.rb +39 -0
  32. data/lib/active_record/turntable/error.rb +8 -0
  33. data/lib/active_record/turntable/helpers.rb +5 -0
  34. data/lib/active_record/turntable/helpers/test_helper.rb +25 -0
  35. data/lib/active_record/turntable/master_shard.rb +28 -0
  36. data/lib/active_record/turntable/migration.rb +132 -0
  37. data/lib/active_record/turntable/mixer.rb +203 -0
  38. data/lib/active_record/turntable/mixer/fader.rb +34 -0
  39. data/lib/active_record/turntable/mixer/fader/calculate_shards_sum_result.rb +15 -0
  40. data/lib/active_record/turntable/mixer/fader/insert_shards_merge_result.rb +24 -0
  41. data/lib/active_record/turntable/mixer/fader/select_shards_merge_result.rb +22 -0
  42. data/lib/active_record/turntable/mixer/fader/specified_shard.rb +12 -0
  43. data/lib/active_record/turntable/mixer/fader/update_shards_merge_result.rb +17 -0
  44. data/lib/active_record/turntable/pool_proxy.rb +56 -0
  45. data/lib/active_record/turntable/rack.rb +5 -0
  46. data/lib/active_record/turntable/rack/connection_management.rb +18 -0
  47. data/lib/active_record/turntable/railtie.rb +14 -0
  48. data/lib/active_record/turntable/railties/databases.rake +205 -0
  49. data/lib/active_record/turntable/seq_shard.rb +14 -0
  50. data/lib/active_record/turntable/sequencer.rb +46 -0
  51. data/lib/active_record/turntable/sequencer/api.rb +36 -0
  52. data/lib/active_record/turntable/sequencer/mysql.rb +32 -0
  53. data/lib/active_record/turntable/shard.rb +48 -0
  54. data/lib/active_record/turntable/sql_tree_patch.rb +199 -0
  55. data/lib/active_record/turntable/version.rb +5 -0
  56. data/lib/activerecord-turntable.rb +2 -0
  57. data/lib/generators/active_record/turntable/install_generator.rb +14 -0
  58. data/lib/generators/templates/turntable.yml +40 -0
  59. data/sample_app/.gitignore +16 -0
  60. data/sample_app/Gemfile +41 -0
  61. data/sample_app/README.rdoc +261 -0
  62. data/sample_app/Rakefile +7 -0
  63. data/sample_app/app/assets/images/rails.png +0 -0
  64. data/sample_app/app/assets/javascripts/application.js +15 -0
  65. data/sample_app/app/assets/stylesheets/application.css +13 -0
  66. data/sample_app/app/controllers/application_controller.rb +3 -0
  67. data/sample_app/app/helpers/application_helper.rb +2 -0
  68. data/sample_app/app/mailers/.gitkeep +0 -0
  69. data/sample_app/app/models/.gitkeep +0 -0
  70. data/sample_app/app/models/user.rb +4 -0
  71. data/sample_app/app/views/layouts/application.html.erb +14 -0
  72. data/sample_app/config.ru +4 -0
  73. data/sample_app/config/application.rb +65 -0
  74. data/sample_app/config/boot.rb +6 -0
  75. data/sample_app/config/database.yml +70 -0
  76. data/sample_app/config/environment.rb +5 -0
  77. data/sample_app/config/environments/development.rb +37 -0
  78. data/sample_app/config/environments/production.rb +67 -0
  79. data/sample_app/config/environments/test.rb +37 -0
  80. data/sample_app/config/initializers/backtrace_silencers.rb +7 -0
  81. data/sample_app/config/initializers/inflections.rb +15 -0
  82. data/sample_app/config/initializers/mime_types.rb +5 -0
  83. data/sample_app/config/initializers/secret_token.rb +7 -0
  84. data/sample_app/config/initializers/session_store.rb +8 -0
  85. data/sample_app/config/initializers/wrap_parameters.rb +14 -0
  86. data/sample_app/config/locales/en.yml +5 -0
  87. data/sample_app/config/routes.rb +58 -0
  88. data/sample_app/config/turntable.yml +64 -0
  89. data/sample_app/db/migrate/20120316073058_create_users.rb +11 -0
  90. data/sample_app/db/seeds.rb +7 -0
  91. data/sample_app/lib/assets/.gitkeep +0 -0
  92. data/sample_app/lib/tasks/.gitkeep +0 -0
  93. data/sample_app/log/.gitkeep +0 -0
  94. data/sample_app/public/404.html +26 -0
  95. data/sample_app/public/422.html +26 -0
  96. data/sample_app/public/500.html +25 -0
  97. data/sample_app/public/favicon.ico +0 -0
  98. data/sample_app/public/index.html +241 -0
  99. data/sample_app/public/robots.txt +5 -0
  100. data/sample_app/script/rails +6 -0
  101. data/sample_app/vendor/assets/javascripts/.gitkeep +0 -0
  102. data/sample_app/vendor/assets/stylesheets/.gitkeep +0 -0
  103. data/sample_app/vendor/plugins/.gitkeep +0 -0
  104. data/script/performance/algorithm +32 -0
  105. data/spec/active_record/turntable/active_record_ext/clever_load_spec.rb +81 -0
  106. data/spec/active_record/turntable/active_record_ext/persistence_spec.rb +151 -0
  107. data/spec/active_record/turntable/algorithm/range_algorithm_spec.rb +35 -0
  108. data/spec/active_record/turntable/algorithm_spec.rb +69 -0
  109. data/spec/active_record/turntable/base_spec.rb +13 -0
  110. data/spec/active_record/turntable/cluster_spec.rb +18 -0
  111. data/spec/active_record/turntable/config_spec.rb +17 -0
  112. data/spec/active_record/turntable/connection_proxy_spec.rb +186 -0
  113. data/spec/active_record/turntable/finder_spec.rb +27 -0
  114. data/spec/active_record/turntable/mixer/fader_spec.rb +4 -0
  115. data/spec/active_record/turntable/mixer_spec.rb +114 -0
  116. data/spec/active_record/turntable/shard_spec.rb +21 -0
  117. data/spec/active_record/turntable_spec.rb +30 -0
  118. data/spec/config/database.yml +45 -0
  119. data/spec/config/turntable.yml +17 -0
  120. data/spec/fabricators/.gitkeep +0 -0
  121. data/spec/fabricators/turntable_fabricator.rb +14 -0
  122. data/spec/migrations/.gitkeep +0 -0
  123. data/spec/migrations/001_create_users.rb +16 -0
  124. data/spec/migrations/002_create_user_statuses.rb +16 -0
  125. data/spec/migrations/003_create_cards.rb +14 -0
  126. data/spec/migrations/004_create_cards_users.rb +15 -0
  127. data/spec/spec_helper.rb +23 -0
  128. data/spec/test_models.rb +27 -0
  129. data/spec/turntable_helper.rb +29 -0
  130. metadata +367 -0
@@ -0,0 +1,70 @@
1
+ module ActiveRecord::Turntable
2
+ class Cluster
3
+
4
+ DEFAULT_CONFIG = {
5
+ "shards" => [],
6
+ "algorithm" => "range",
7
+ }.with_indifferent_access
8
+
9
+ def initialize(klass, cluster_spec, options = {})
10
+ @klass = klass
11
+ @config = DEFAULT_CONFIG.merge(cluster_spec)
12
+ @options = options.with_indifferent_access
13
+ @shards = {}.with_indifferent_access
14
+
15
+ # setup master shard
16
+ @master_shard = MasterShard.new(klass)
17
+
18
+ # setup sequencer
19
+ if (seq = (@options[:seq] || @config[:seq]))
20
+ @seq_shard = SeqShard.new(seq)
21
+ end
22
+
23
+ # setup shards
24
+ @config[:shards].each do |spec|
25
+ @shards[spec["connection"]] ||= Shard.new(spec)
26
+ end
27
+
28
+ # setup algorithm
29
+ alg_name = "ActiveRecord::Turntable::Algorithm::#{@config["algorithm"].camelize}Algorithm"
30
+ @algorithm = alg_name.constantize.new(@config)
31
+
32
+ # setup proxy
33
+ @connection_proxy = ConnectionProxy.new(self, cluster_spec)
34
+ end
35
+
36
+ def klass
37
+ @klass
38
+ end
39
+
40
+ def master
41
+ @master_shard
42
+ end
43
+
44
+ def seq
45
+ @seq_shard || @master_shard
46
+ end
47
+
48
+ def shards
49
+ @shards
50
+ end
51
+
52
+ def connection_proxy
53
+ @connection_proxy
54
+ end
55
+
56
+ def select_shard(key)
57
+ @shards[@algorithm.calculate(key)]
58
+ rescue
59
+ raise ActiveRecord::Turntable::CannotSpecifyShardError,
60
+ "[#{klass}] cannot select_shard for key:#{key}"
61
+ end
62
+
63
+ def weighted_shards(key = nil)
64
+ key ||= @klass.current_sequence
65
+ Hash[@algorithm.calculate_used_shards_with_weight(key).map do |k,v|
66
+ [@shards[k], v]
67
+ end]
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,19 @@
1
+ #
2
+ #=ActiveRecord::Turntable::Compatible
3
+ #
4
+ # for ActiveRecord versions compatibility
5
+ #
6
+ module ActiveRecord::Turntable
7
+ module Compatible
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ # class_attributes
12
+ unless respond_to?(:class_attribute)
13
+ class << self
14
+ alias_method :class_attribute, :class_inheritable_accessor
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,24 @@
1
+ require 'active_support/core_ext/hash/indifferent_access'
2
+
3
+ module ActiveRecord::Turntable
4
+ class Config
5
+ include Singleton
6
+
7
+ def self.[](key)
8
+ instance[key]
9
+ end
10
+
11
+ def [](key)
12
+ self.class.load!(ActiveRecord::Base.turntable_config_file) unless @config
13
+ @config[key]
14
+ end
15
+
16
+ def self.load!(config_file, env = (defined?(Rails) ? Rails.env : 'development'))
17
+ instance.load!(config_file, env)
18
+ end
19
+
20
+ def load!(config_file, env)
21
+ @config = YAML.load_file(config_file).with_indifferent_access[env]
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,218 @@
1
+ require 'active_record/turntable/connection_proxy/mixable'
2
+ module ActiveRecord::Turntable
3
+ class ConnectionProxy
4
+ include Mixable
5
+
6
+ attr_writer :spec
7
+
8
+ def initialize(cluster, options = {})
9
+ @cluster = cluster
10
+ @model_class = cluster.klass
11
+ @current_shard = (cluster.master || cluster.shards.first[1])
12
+ @fixed_shard = false
13
+ @mixer = ActiveRecord::Turntable::Mixer.new(self)
14
+ end
15
+
16
+ delegate :logger, :to => ActiveRecord::Base
17
+
18
+ delegate :create_table, :rename_table, :drop_table, :add_column, :remove_colomn,
19
+ :change_column, :change_column_default, :rename_column, :add_index,
20
+ :remove_index, :initialize_schema_information,
21
+ :dump_schema_information, :execute_ignore_duplicate, :to => :master_connection
22
+
23
+ # delegate :insert_many, :to => :master # ar-extensions bulk insert support
24
+
25
+ def transaction(options = {}, &block)
26
+ connection.transaction(options, &block)
27
+ end
28
+
29
+ def shards_transaction(shards, options = {}, in_recursion = false, &block)
30
+ shards = in_recursion ? shards : Array.wrap(shards).dup
31
+ shard_or_object = shards.shift
32
+ shard = to_shard(shard_or_object)
33
+ if shards.present?
34
+ shard.connection.transaction(options) do
35
+ shards_transaction(shards, options, true, &block)
36
+ end
37
+ else
38
+ shard.connection.transaction(options) do
39
+ block.call
40
+ end
41
+ end
42
+ end
43
+
44
+ def to_shard(shard_or_object)
45
+ case shard_or_object
46
+ when ActiveRecord::Turntable::Shard
47
+ shard_or_object
48
+ when ActiveRecord::Base
49
+ shard_or_object.turntable_shard
50
+ else
51
+ raise ActiveRecord::Turntable::Error,
52
+ "transaction cannot call to object: #{shard_or_object}"
53
+ end
54
+ end
55
+
56
+ def method_missing(method, *args, &block)
57
+ if shard_fixed?
58
+ connection.send(method, *args, &block)
59
+ elsif mixable?(method, *args)
60
+ fader = @mixer.build_fader(method, *args, &block)
61
+ logger.debug { "[ActiveRecord::Turntable] Sending method: #{method}, " +
62
+ "sql: #{args.first}, " +
63
+ "shards: #{fader.shards_query_hash.keys.map(&:name)}" }
64
+ fader.execute
65
+ else
66
+ connection.send(method, *args, &block)
67
+ end
68
+ end
69
+
70
+ # for 3.2.2
71
+ def to_sql(arel, binds = [])
72
+ if master.connection.method(:to_sql).arity < 0
73
+ master.connection.to_sql(arel, binds)
74
+ else
75
+ master.connection.to_sql(arel)
76
+ end
77
+ end
78
+
79
+ def cluster
80
+ @cluster
81
+ end
82
+
83
+ def shards
84
+ @cluster.shards
85
+ end
86
+
87
+ def shard_fixed?
88
+ !!fixed_shard
89
+ end
90
+
91
+ def fixed_shard
92
+ @fixed_shard
93
+ end
94
+
95
+ def master
96
+ @cluster.master
97
+ end
98
+
99
+ def master_connection
100
+ master.connection
101
+ end
102
+
103
+ def seq
104
+ @cluster.seq
105
+ end
106
+
107
+ def current_shard
108
+ @current_shard
109
+ end
110
+
111
+ def current_shard=(shard)
112
+ logger.debug { "Chainging #{@model_class}'s shard to #{shard.name}"}
113
+ @current_shard = shard
114
+ end
115
+
116
+ def connection
117
+ @current_shard.connection
118
+ end
119
+
120
+ def connection_pool
121
+ @current_shard.connection_pool
122
+ end
123
+
124
+ def with_shard(shard)
125
+ old_shard, old_fixed = current_shard, fixed_shard
126
+ self.current_shard = shard
127
+ @fixed_shard = shard
128
+ yield
129
+ ensure
130
+ @fixed_shard = old_fixed
131
+ self.current_shard = old_shard
132
+ end
133
+
134
+ def with_recursive_shards(connection_name, *klasses, &block)
135
+ with_shard(shards[connection_name]) do
136
+ if klasses.blank?
137
+ yield
138
+ else
139
+ current_klass = klasses.shift
140
+ current_klass.connection.with_recursive_shards(connection_name, *klasses, &block)
141
+ end
142
+ end
143
+ end
144
+
145
+ def with_all(continue_on_error = false)
146
+ @cluster.shards.values.map do |shard|
147
+ begin
148
+ with_shard(shard) {
149
+ yield
150
+ }
151
+ rescue Exception => err
152
+ unless continue_on_error
153
+ raise err
154
+ end
155
+ err
156
+ end
157
+ end
158
+ end
159
+
160
+ def with_master(&block)
161
+ with_shard(@cluster.master) do
162
+ yield
163
+ end
164
+ end
165
+
166
+ def connected?
167
+ connection_pool.connected?
168
+ end
169
+
170
+ if ActiveRecord::VERSION::STRING > '3.1'
171
+ %w(columns columns_hash column_defaults primary_keys).each do |name|
172
+ define_method(name.to_sym) do
173
+ master.connection_pool.send(name.to_sym)
174
+ end
175
+ end
176
+
177
+ if ActiveRecord::VERSION::STRING < '3.2'
178
+ %w(table_exists?).each do |name|
179
+ define_method(name.to_sym) do |*args|
180
+ master.connection_pool.send(name.to_sym, *args)
181
+ end
182
+ end
183
+ else
184
+ %w(table_exists?).each do |name|
185
+ define_method(name.to_sym) do |*args|
186
+ master.connection_pool.with_connection do |c|
187
+ c.schema_cache.send(name.to_sym, *args)
188
+ end
189
+ end
190
+ end
191
+ end
192
+
193
+ def columns(*args)
194
+ if args.size > 0
195
+ master.connection_pool.columns[*args]
196
+ else
197
+ master.connection_pool.columns
198
+ end
199
+ end
200
+ end
201
+
202
+ def pk_and_sequence_for(*args)
203
+ master.connection.send(:pk_and_sequence_for, *args)
204
+ end
205
+
206
+ def primary_key(*args)
207
+ master.connection.send(:primary_key, *args)
208
+ end
209
+
210
+ def supports_views?(*args)
211
+ master.connection.send(:supports_views?, *args)
212
+ end
213
+
214
+ def spec
215
+ @spec ||= master.connection_pool.spec
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,39 @@
1
+ module ActiveRecord::Turntable
2
+ class ConnectionProxy
3
+ module Mixable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ if ActiveRecord::VERSION::STRING < '3.1'
8
+ include Rails30
9
+ else
10
+ include Rails3x
11
+ end
12
+ end
13
+
14
+ module Rails3x
15
+ METHODS_REGEXP = /\A(insert|select|update|delete|exec_)/
16
+ EXCLUDE_QUERY_REGEXP = /\A\s*SHOW/i
17
+ QUERY_REGEXP = /\A\s*(INSERT|DELETE|UPDATE|SELECT)/i
18
+
19
+ def mixable?(method, *args)
20
+ (method.to_s =~ METHODS_REGEXP &&
21
+ args.first !~ EXCLUDE_QUERY_REGEXP) ||
22
+ (method.to_s == 'execute' && args.first =~ QUERY_REGEXP)
23
+ end
24
+ end
25
+
26
+ module Rails30
27
+ METHODS_REGEXP = /\A(insert|select|update|delete)/
28
+ EXCLUDE_QUERY_REGEXP = /\A\s*SHOW/i
29
+ QUERY_REGEXP = /\A\s*(INSERT|DELETE|UPDATE|SELECT)/i
30
+
31
+ def mixable?(method, *args)
32
+ (method.to_s =~ METHODS_REGEXP &&
33
+ args.first !~ EXCLUDE_QUERY_REGEXP) ||
34
+ (method.to_s == 'execute' && args.first =~ QUERY_REGEXP)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,8 @@
1
+ module ActiveRecord::Turntable
2
+ class Error < StandardError; end
3
+ class NotImplementedError < Error; end
4
+ class SequenceNotFoundError < Error; end
5
+ class CannotSpecifyShardError < Error; end
6
+ class MasterShardNotConnected < Error; end
7
+ class UnknownOperatorError < Error; end
8
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveRecord::Turntable
2
+ module Helpers
3
+ autoload :TestHelper, 'active_record/turntable/helpers/test_helper'
4
+ end
5
+ end
@@ -0,0 +1,25 @@
1
+ module ActiveRecord::Turntable
2
+ module Helpers
3
+ module TestHelper
4
+ # all shards
5
+ def FabricateAll(name, overrides={}, &block)
6
+ obj = Fabrication::Fabricator.generate(name, {
7
+ :save => true
8
+ }, overrides, &block)
9
+
10
+ default_pool = obj.class.connection_pool
11
+ connection_pools = obj.class.connection_handler.instance_variable_get(:@connection_pools)
12
+
13
+ ActiveRecord::Base.turntable_connections.each do |conn_name, conn|
14
+ new_obj = obj.dup
15
+ connection_pools[new_obj.class.name] = conn
16
+ new_obj.id = obj.id
17
+ new_obj.send(:create)
18
+ end
19
+ obj
20
+ ensure
21
+ connection_pools[obj.class.name] = default_pool
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ module ActiveRecord::Turntable
2
+ class MasterShard < Shard
3
+ def initialize(klass)
4
+ (klass and klass.connection_pool) or
5
+ raise MasterShardNotConnected, "connection_pool is nil"
6
+ @klass = klass
7
+ @name = 'master'
8
+ end
9
+
10
+ def connection_pool
11
+ if ActiveRecord::Base == @klass
12
+ ActiveRecord::Base.connection_pool
13
+ else
14
+ # use parentclass connection which is turntable disabled
15
+ klass = @klass.superclass
16
+ candidate_connection_pool = nil
17
+ while !candidate_connection_pool
18
+ if klass == ActiveRecord::Base or !klass.turntable_enabled?
19
+ candidate_connection_pool = klass.connection_pool
20
+ else
21
+ klass = klass.superclass
22
+ end
23
+ end
24
+ candidate_connection_pool
25
+ end
26
+ end
27
+ end
28
+ end