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.
- data/.document +5 -0
- data/.gitignore +25 -0
- data/.rspec +3 -0
- data/Gemfile +25 -0
- data/Guardfile +9 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +290 -0
- data/Rakefile +101 -0
- data/activerecord-turntable.gemspec +47 -0
- data/lib/active_record/turntable.rb +58 -0
- data/lib/active_record/turntable/active_record_ext.rb +26 -0
- data/lib/active_record/turntable/active_record_ext/.gitkeep +0 -0
- data/lib/active_record/turntable/active_record_ext/abstract_adapter.rb +50 -0
- data/lib/active_record/turntable/active_record_ext/clever_load.rb +90 -0
- data/lib/active_record/turntable/active_record_ext/fixtures.rb +131 -0
- data/lib/active_record/turntable/active_record_ext/log_subscriber.rb +64 -0
- data/lib/active_record/turntable/active_record_ext/persistence.rb +95 -0
- data/lib/active_record/turntable/active_record_ext/schema_dumper.rb +107 -0
- data/lib/active_record/turntable/active_record_ext/sequencer.rb +28 -0
- data/lib/active_record/turntable/active_record_ext/transactions.rb +33 -0
- data/lib/active_record/turntable/algorithm.rb +7 -0
- data/lib/active_record/turntable/algorithm/.gitkeep +0 -0
- data/lib/active_record/turntable/algorithm/base.rb +11 -0
- data/lib/active_record/turntable/algorithm/range_algorithm.rb +37 -0
- data/lib/active_record/turntable/algorithm/range_bsearch_algorithm.rb +41 -0
- data/lib/active_record/turntable/base.rb +130 -0
- data/lib/active_record/turntable/cluster.rb +70 -0
- data/lib/active_record/turntable/compatible.rb +19 -0
- data/lib/active_record/turntable/config.rb +24 -0
- data/lib/active_record/turntable/connection_proxy.rb +218 -0
- data/lib/active_record/turntable/connection_proxy/mixable.rb +39 -0
- data/lib/active_record/turntable/error.rb +8 -0
- data/lib/active_record/turntable/helpers.rb +5 -0
- data/lib/active_record/turntable/helpers/test_helper.rb +25 -0
- data/lib/active_record/turntable/master_shard.rb +28 -0
- data/lib/active_record/turntable/migration.rb +132 -0
- data/lib/active_record/turntable/mixer.rb +203 -0
- data/lib/active_record/turntable/mixer/fader.rb +34 -0
- data/lib/active_record/turntable/mixer/fader/calculate_shards_sum_result.rb +15 -0
- data/lib/active_record/turntable/mixer/fader/insert_shards_merge_result.rb +24 -0
- data/lib/active_record/turntable/mixer/fader/select_shards_merge_result.rb +22 -0
- data/lib/active_record/turntable/mixer/fader/specified_shard.rb +12 -0
- data/lib/active_record/turntable/mixer/fader/update_shards_merge_result.rb +17 -0
- data/lib/active_record/turntable/pool_proxy.rb +56 -0
- data/lib/active_record/turntable/rack.rb +5 -0
- data/lib/active_record/turntable/rack/connection_management.rb +18 -0
- data/lib/active_record/turntable/railtie.rb +14 -0
- data/lib/active_record/turntable/railties/databases.rake +205 -0
- data/lib/active_record/turntable/seq_shard.rb +14 -0
- data/lib/active_record/turntable/sequencer.rb +46 -0
- data/lib/active_record/turntable/sequencer/api.rb +36 -0
- data/lib/active_record/turntable/sequencer/mysql.rb +32 -0
- data/lib/active_record/turntable/shard.rb +48 -0
- data/lib/active_record/turntable/sql_tree_patch.rb +199 -0
- data/lib/active_record/turntable/version.rb +5 -0
- data/lib/activerecord-turntable.rb +2 -0
- data/lib/generators/active_record/turntable/install_generator.rb +14 -0
- data/lib/generators/templates/turntable.yml +40 -0
- data/sample_app/.gitignore +16 -0
- data/sample_app/Gemfile +41 -0
- data/sample_app/README.rdoc +261 -0
- data/sample_app/Rakefile +7 -0
- data/sample_app/app/assets/images/rails.png +0 -0
- data/sample_app/app/assets/javascripts/application.js +15 -0
- data/sample_app/app/assets/stylesheets/application.css +13 -0
- data/sample_app/app/controllers/application_controller.rb +3 -0
- data/sample_app/app/helpers/application_helper.rb +2 -0
- data/sample_app/app/mailers/.gitkeep +0 -0
- data/sample_app/app/models/.gitkeep +0 -0
- data/sample_app/app/models/user.rb +4 -0
- data/sample_app/app/views/layouts/application.html.erb +14 -0
- data/sample_app/config.ru +4 -0
- data/sample_app/config/application.rb +65 -0
- data/sample_app/config/boot.rb +6 -0
- data/sample_app/config/database.yml +70 -0
- data/sample_app/config/environment.rb +5 -0
- data/sample_app/config/environments/development.rb +37 -0
- data/sample_app/config/environments/production.rb +67 -0
- data/sample_app/config/environments/test.rb +37 -0
- data/sample_app/config/initializers/backtrace_silencers.rb +7 -0
- data/sample_app/config/initializers/inflections.rb +15 -0
- data/sample_app/config/initializers/mime_types.rb +5 -0
- data/sample_app/config/initializers/secret_token.rb +7 -0
- data/sample_app/config/initializers/session_store.rb +8 -0
- data/sample_app/config/initializers/wrap_parameters.rb +14 -0
- data/sample_app/config/locales/en.yml +5 -0
- data/sample_app/config/routes.rb +58 -0
- data/sample_app/config/turntable.yml +64 -0
- data/sample_app/db/migrate/20120316073058_create_users.rb +11 -0
- data/sample_app/db/seeds.rb +7 -0
- data/sample_app/lib/assets/.gitkeep +0 -0
- data/sample_app/lib/tasks/.gitkeep +0 -0
- data/sample_app/log/.gitkeep +0 -0
- data/sample_app/public/404.html +26 -0
- data/sample_app/public/422.html +26 -0
- data/sample_app/public/500.html +25 -0
- data/sample_app/public/favicon.ico +0 -0
- data/sample_app/public/index.html +241 -0
- data/sample_app/public/robots.txt +5 -0
- data/sample_app/script/rails +6 -0
- data/sample_app/vendor/assets/javascripts/.gitkeep +0 -0
- data/sample_app/vendor/assets/stylesheets/.gitkeep +0 -0
- data/sample_app/vendor/plugins/.gitkeep +0 -0
- data/script/performance/algorithm +32 -0
- data/spec/active_record/turntable/active_record_ext/clever_load_spec.rb +81 -0
- data/spec/active_record/turntable/active_record_ext/persistence_spec.rb +151 -0
- data/spec/active_record/turntable/algorithm/range_algorithm_spec.rb +35 -0
- data/spec/active_record/turntable/algorithm_spec.rb +69 -0
- data/spec/active_record/turntable/base_spec.rb +13 -0
- data/spec/active_record/turntable/cluster_spec.rb +18 -0
- data/spec/active_record/turntable/config_spec.rb +17 -0
- data/spec/active_record/turntable/connection_proxy_spec.rb +186 -0
- data/spec/active_record/turntable/finder_spec.rb +27 -0
- data/spec/active_record/turntable/mixer/fader_spec.rb +4 -0
- data/spec/active_record/turntable/mixer_spec.rb +114 -0
- data/spec/active_record/turntable/shard_spec.rb +21 -0
- data/spec/active_record/turntable_spec.rb +30 -0
- data/spec/config/database.yml +45 -0
- data/spec/config/turntable.yml +17 -0
- data/spec/fabricators/.gitkeep +0 -0
- data/spec/fabricators/turntable_fabricator.rb +14 -0
- data/spec/migrations/.gitkeep +0 -0
- data/spec/migrations/001_create_users.rb +16 -0
- data/spec/migrations/002_create_user_statuses.rb +16 -0
- data/spec/migrations/003_create_cards.rb +14 -0
- data/spec/migrations/004_create_cards_users.rb +15 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/test_models.rb +27 -0
- data/spec/turntable_helper.rb +29 -0
- metadata +367 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
module ActiveRecord::Turntable::ActiveRecordExt
|
|
2
|
+
module Persistence
|
|
3
|
+
if ActiveRecord::VERSION::STRING < '3.1'
|
|
4
|
+
::ActiveRecord::Persistence.class_eval do
|
|
5
|
+
def destroy
|
|
6
|
+
klass = self.class
|
|
7
|
+
if persisted?
|
|
8
|
+
condition_scope = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id))
|
|
9
|
+
if klass.turntable_enabled? and klass.primary_key != klass.turntable_shard_key.to_s
|
|
10
|
+
condition_scope = condition_scope.where(klass.turntable_shard_key => self.send(turntable_shard_key))
|
|
11
|
+
end
|
|
12
|
+
condition_scope.delete_all
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
@destroyed = true
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# overrides ActiveRecord::Persistence's original method so that
|
|
22
|
+
def update(attribute_names = @attributes.keys)
|
|
23
|
+
klass = self.class
|
|
24
|
+
attributes_with_values = arel_attributes_values(false, false, attribute_names)
|
|
25
|
+
return 0 if attributes_with_values.empty?
|
|
26
|
+
condition_scope = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id))
|
|
27
|
+
if klass.turntable_enabled? and klass.primary_key != klass.turntable_shard_key.to_s
|
|
28
|
+
condition_scope = condition_scope.where(klass.turntable_shard_key => self.send(turntable_shard_key))
|
|
29
|
+
end
|
|
30
|
+
condition_scope.arel.update(attributes_with_values)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
else
|
|
35
|
+
::ActiveRecord::Persistence.class_eval do
|
|
36
|
+
def destroy
|
|
37
|
+
klass = self.class
|
|
38
|
+
destroy_associations
|
|
39
|
+
|
|
40
|
+
if persisted?
|
|
41
|
+
ActiveRecord::IdentityMap.remove(self) if ActiveRecord::IdentityMap.enabled?
|
|
42
|
+
pk = klass.primary_key
|
|
43
|
+
column = klass.columns_hash[pk]
|
|
44
|
+
substitute = connection.substitute_at(column, 0)
|
|
45
|
+
|
|
46
|
+
relation = klass.unscoped.where(klass.arel_table[pk].eq(substitute))
|
|
47
|
+
if klass.turntable_enabled? and klass.primary_key != klass.turntable_shard_key.to_s
|
|
48
|
+
relation = relation.where(klass.turntable_shard_key => self.send(turntable_shard_key))
|
|
49
|
+
end
|
|
50
|
+
relation.bind_values = [[column, id]]
|
|
51
|
+
relation.delete_all
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
@destroyed = true
|
|
55
|
+
freeze
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def destroy_without_callbacks
|
|
59
|
+
klass = self.class
|
|
60
|
+
destroy_associations
|
|
61
|
+
|
|
62
|
+
if persisted?
|
|
63
|
+
ActiveRecord::IdentityMap.remove(self) if ActiveRecord::IdentityMap.enabled?
|
|
64
|
+
pk = klass.primary_key
|
|
65
|
+
column = klass.columns_hash[pk]
|
|
66
|
+
substitute = connection.substitute_at(column, 0)
|
|
67
|
+
|
|
68
|
+
relation = klass.unscoped.where(klass.arel_table[pk].eq(substitute))
|
|
69
|
+
if klass.turntable_enabled? and klass.primary_key != klass.turntable_shard_key.to_s
|
|
70
|
+
relation = relation.where(klass.turntable_shard_key => self.send(turntable_shard_key))
|
|
71
|
+
end
|
|
72
|
+
relation.bind_values = [[column, id]]
|
|
73
|
+
relation.delete_all
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
@destroyed = true
|
|
77
|
+
freeze
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
def update(attribute_names = @attributes.keys)
|
|
82
|
+
attributes_with_values = arel_attributes_values(false, false, attribute_names)
|
|
83
|
+
return 0 if attributes_with_values.empty?
|
|
84
|
+
klass = self.class
|
|
85
|
+
condition_scope = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id))
|
|
86
|
+
if klass.turntable_enabled? and klass.primary_key != klass.turntable_shard_key.to_s
|
|
87
|
+
condition_scope = condition_scope.where(klass.turntable_shard_key => self.send(turntable_shard_key))
|
|
88
|
+
end
|
|
89
|
+
stmt = condition_scope.arel.compile_update(attributes_with_values)
|
|
90
|
+
klass.connection.update stmt.to_sql
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
module ActiveRecord::Turntable
|
|
3
|
+
module ActiveRecordExt
|
|
4
|
+
module SchemaDumper
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
alias_method_chain :table, :turntable
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def table_with_turntable(table, stream)
|
|
14
|
+
columns = @connection.columns(table)
|
|
15
|
+
begin
|
|
16
|
+
tbl = StringIO.new
|
|
17
|
+
|
|
18
|
+
# first dump primary key column
|
|
19
|
+
if @connection.respond_to?(:pk_and_sequence_for)
|
|
20
|
+
pk, _ = @connection.pk_and_sequence_for(table)
|
|
21
|
+
elsif @connection.respond_to?(:primary_key)
|
|
22
|
+
pk = @connection.primary_key(table)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# turntable sequencer dump
|
|
26
|
+
if table =~ /\A(.*)_id_seq\z/
|
|
27
|
+
tbl.print " create_sequence_for #{$1.inspect}"
|
|
28
|
+
else
|
|
29
|
+
tbl.print " create_table #{table.inspect}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if columns.detect { |c| c.name == pk }
|
|
33
|
+
if pk != 'id'
|
|
34
|
+
tbl.print %Q(, :primary_key => "#{pk}")
|
|
35
|
+
end
|
|
36
|
+
else
|
|
37
|
+
tbl.print ", :id => false"
|
|
38
|
+
end
|
|
39
|
+
tbl.print ", :force => true"
|
|
40
|
+
tbl.puts " do |t|"
|
|
41
|
+
|
|
42
|
+
# then dump all non-primary key columns
|
|
43
|
+
column_specs = columns.map do |column|
|
|
44
|
+
raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil?
|
|
45
|
+
next if column.name == pk
|
|
46
|
+
spec = {}
|
|
47
|
+
spec[:name] = column.name.inspect
|
|
48
|
+
|
|
49
|
+
# AR has an optimisation which handles zero-scale decimals as integers. This
|
|
50
|
+
# code ensures that the dumper still dumps the column as a decimal.
|
|
51
|
+
spec[:type] = if column.type == :integer && [/^numeric/, /^decimal/].any? { |e| e.match(column.sql_type) }
|
|
52
|
+
'decimal'
|
|
53
|
+
else
|
|
54
|
+
column.type.to_s
|
|
55
|
+
end
|
|
56
|
+
spec[:limit] = column.limit.inspect if column.limit != @types[column.type][:limit] && spec[:type] != 'decimal'
|
|
57
|
+
spec[:precision] = column.precision.inspect if column.precision
|
|
58
|
+
spec[:scale] = column.scale.inspect if column.scale
|
|
59
|
+
spec[:null] = 'false' unless column.null
|
|
60
|
+
spec[:default] = default_string(column.default) if column.has_default?
|
|
61
|
+
(spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")}
|
|
62
|
+
spec
|
|
63
|
+
end.compact
|
|
64
|
+
|
|
65
|
+
# find all migration keys used in this table
|
|
66
|
+
keys = [:name, :limit, :precision, :scale, :default, :null] & column_specs.map{ |k| k.keys }.flatten
|
|
67
|
+
|
|
68
|
+
# figure out the lengths for each column based on above keys
|
|
69
|
+
lengths = keys.map{ |key| column_specs.map{ |spec| spec[key] ? spec[key].length + 2 : 0 }.max }
|
|
70
|
+
|
|
71
|
+
# the string we're going to sprintf our values against, with standardized column widths
|
|
72
|
+
format_string = lengths.map{ |len| "%-#{len}s" }
|
|
73
|
+
|
|
74
|
+
# find the max length for the 'type' column, which is special
|
|
75
|
+
type_length = column_specs.map{ |column| column[:type].length }.max
|
|
76
|
+
|
|
77
|
+
# add column type definition to our format string
|
|
78
|
+
format_string.unshift " t.%-#{type_length}s "
|
|
79
|
+
|
|
80
|
+
format_string *= ''
|
|
81
|
+
|
|
82
|
+
column_specs.each do |colspec|
|
|
83
|
+
values = keys.zip(lengths).map{ |key, len| colspec.key?(key) ? colspec[key] + ", " : " " * len }
|
|
84
|
+
values.unshift colspec[:type]
|
|
85
|
+
tbl.print((format_string % values).gsub(/,\s*$/, ''))
|
|
86
|
+
tbl.puts
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
tbl.puts " end"
|
|
90
|
+
tbl.puts
|
|
91
|
+
|
|
92
|
+
indexes(table, tbl)
|
|
93
|
+
|
|
94
|
+
tbl.rewind
|
|
95
|
+
stream.print tbl.read
|
|
96
|
+
rescue => e
|
|
97
|
+
stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}"
|
|
98
|
+
stream.puts "# #{e.message}"
|
|
99
|
+
stream.puts
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
stream
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module ActiveRecord::Turntable::ActiveRecordExt
|
|
2
|
+
module Sequencer
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
include DatabaseStatements
|
|
7
|
+
alias_method_chain :prefetch_primary_key?, :turntable
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module DatabaseStatements
|
|
11
|
+
def default_sequence_name(table_name, pk = nil)
|
|
12
|
+
ActiveRecord::Turntable::Sequencer.sequence_name(table_name, pk)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def prefetch_primary_key_with_turntable?(table_name = nil)
|
|
17
|
+
ActiveRecord::Turntable::Sequencer.has_sequencer?(table_name)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def next_sequence_value(sequence_name)
|
|
21
|
+
ActiveRecord::Turntable::Sequencer.sequences[sequence_name].next_sequence_value(sequence_name)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def current_sequence_value(sequence_name)
|
|
25
|
+
ActiveRecord::Turntable::Sequencer.sequences[sequence_name].current_sequence_value(sequence_name)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module ActiveRecord::Turntable
|
|
2
|
+
module ActiveRecordExt
|
|
3
|
+
module Transactions
|
|
4
|
+
def with_transaction_returning_status
|
|
5
|
+
if self.class.turntable_enabled?
|
|
6
|
+
status = nil
|
|
7
|
+
if self.new_record? and self.turntable_shard_key.to_s == self.class.primary_key and
|
|
8
|
+
self.id.nil? and connection.prefetch_primary_key?(self.class.table_name)
|
|
9
|
+
self.id = connection.next_sequence_value(self.class.sequence_name)
|
|
10
|
+
end
|
|
11
|
+
self.class.connection.shards_transaction([self.turntable_shard]) do
|
|
12
|
+
add_to_transaction
|
|
13
|
+
status = yield
|
|
14
|
+
raise ActiveRecord::Rollback unless status
|
|
15
|
+
end
|
|
16
|
+
status
|
|
17
|
+
else
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def add_to_transaction
|
|
23
|
+
if self.class.turntable_enabled?
|
|
24
|
+
if self.turntable_shard.connection.add_transaction_record(self)
|
|
25
|
+
remember_transaction_record_state
|
|
26
|
+
end
|
|
27
|
+
else
|
|
28
|
+
super
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
module ActiveRecord::Turntable
|
|
2
|
+
module Algorithm
|
|
3
|
+
autoload :Base, "active_record/turntable/algorithm/base"
|
|
4
|
+
autoload :RangeAlgorithm, "active_record/turntable/algorithm/range_algorithm"
|
|
5
|
+
autoload :RangeBsearchAlgorithm, "active_record/turntable/algorithm/range_bsearch_algorithm"
|
|
6
|
+
end
|
|
7
|
+
end
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
module ActiveRecord::Turntable::Algorithm
|
|
3
|
+
class RangeAlgorithm < Base
|
|
4
|
+
def initialize(config)
|
|
5
|
+
@config = config
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def calculate(key)
|
|
9
|
+
idx = calculate_idx(key)
|
|
10
|
+
@config["shards"][idx]["connection"]
|
|
11
|
+
rescue
|
|
12
|
+
raise ActiveRecord::Turntable::CannotSpecifyShardError, "cannot specify shard for key:#{key}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def calculate_idx(key)
|
|
16
|
+
@config["shards"].find_index {|h| h["less_than"] > key }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# { connection_name => weight, ... }
|
|
20
|
+
def calculate_used_shards_with_weight(sequence_value)
|
|
21
|
+
idx = calculate_idx(sequence_value)
|
|
22
|
+
last_connection = calculate(sequence_value)
|
|
23
|
+
shards = @config["shards"][0..idx]
|
|
24
|
+
weighted_hash = Hash.new {|h,k| h[k]=0}
|
|
25
|
+
prev_max = 0
|
|
26
|
+
shards.each_with_index do |h,idx|
|
|
27
|
+
weighted_hash[h["connection"]] += if idx < shards.size - 1
|
|
28
|
+
h["less_than"] - prev_max - 1
|
|
29
|
+
else
|
|
30
|
+
sequence_value - prev_max
|
|
31
|
+
end
|
|
32
|
+
prev_max = h["less_than"] - 1
|
|
33
|
+
end
|
|
34
|
+
return weighted_hash
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
require 'bsearch'
|
|
3
|
+
module ActiveRecord::Turntable::Algorithm
|
|
4
|
+
class RangeBsearchAlgorithm < Base
|
|
5
|
+
def initialize(config)
|
|
6
|
+
@config = config
|
|
7
|
+
@config["shards"].sort_by! {|a| a["less_than"]}
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def calculate(key)
|
|
11
|
+
idx = calculate_idx(key)
|
|
12
|
+
@config["shards"][idx]["connection"]
|
|
13
|
+
rescue
|
|
14
|
+
raise ActiveRecord::Turntable::CannotSpecifyShardError, "cannot specify shard for key:#{key}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def calculate_idx(key)
|
|
18
|
+
@config["shards"].bsearch_lower_boundary { |h|
|
|
19
|
+
h["less_than"] <=> key
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# { connection_name => weight, ... }
|
|
24
|
+
def calculate_used_shards_with_weight(sequence_value)
|
|
25
|
+
idx = calculate_idx(sequence_value)
|
|
26
|
+
last_connection = calculate(sequence_value)
|
|
27
|
+
shards = @config["shards"][0..idx]
|
|
28
|
+
weighted_hash = Hash.new {|h,k| h[k]=0}
|
|
29
|
+
prev_max = 0
|
|
30
|
+
shards.each_with_index do |h,idx|
|
|
31
|
+
weighted_hash[h["connection"]] += if idx < shards.size - 1
|
|
32
|
+
h["less_than"] - prev_max - 1
|
|
33
|
+
else
|
|
34
|
+
sequence_value - prev_max
|
|
35
|
+
end
|
|
36
|
+
prev_max = h["less_than"] - 1
|
|
37
|
+
end
|
|
38
|
+
return weighted_hash
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
module ActiveRecord::Turntable
|
|
2
|
+
module Base
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
include Compatible
|
|
7
|
+
class_attribute :turntable_connections,
|
|
8
|
+
:turntable_enabled, :turntable_sequencer_enabled
|
|
9
|
+
|
|
10
|
+
self.turntable_connections = {}
|
|
11
|
+
self.turntable_enabled = false
|
|
12
|
+
self.turntable_sequencer_enabled = false
|
|
13
|
+
class << self
|
|
14
|
+
delegate :shards_transaction, :to => :connection
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module ClassMethods
|
|
19
|
+
def turntable(cluster_name, shard_key_name, options = {})
|
|
20
|
+
class_attribute :turntable_shard_key,
|
|
21
|
+
:turntable_cluster, :turntable_cluster_name
|
|
22
|
+
|
|
23
|
+
self.turntable_enabled = true
|
|
24
|
+
self.turntable_cluster_name = cluster_name
|
|
25
|
+
self.turntable_shard_key = shard_key_name
|
|
26
|
+
self.turntable_cluster = Cluster.new(
|
|
27
|
+
self,
|
|
28
|
+
turntable_config[:clusters][cluster_name],
|
|
29
|
+
options
|
|
30
|
+
)
|
|
31
|
+
turntable_replace_connection_pool
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def force_transaction_all_shards!(options={}, &block)
|
|
35
|
+
force_connect_all_shards!
|
|
36
|
+
shards = turntable_connections.values
|
|
37
|
+
shards += [ActiveRecord::Base.connection_pool]
|
|
38
|
+
recursive_transaction(shards, options, &block)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def recursive_transaction(pools, options, &block)
|
|
42
|
+
pool = pools.shift
|
|
43
|
+
if pools.present?
|
|
44
|
+
pool.connection.transaction(options) do
|
|
45
|
+
recursive_transaction(pools, options, &block)
|
|
46
|
+
end
|
|
47
|
+
else
|
|
48
|
+
pool.connection.transaction(options, &block)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def force_connect_all_shards!
|
|
53
|
+
conf = configurations[Rails.env]
|
|
54
|
+
shards = conf["shards"]
|
|
55
|
+
shards = shards.merge(conf["seq"]) if conf["seq"]
|
|
56
|
+
shards.each do |name, config|
|
|
57
|
+
turntable_connections[name] ||=
|
|
58
|
+
ActiveRecord::ConnectionAdapters::ConnectionPool.new(spec_for(config))
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def turntable_replace_connection_pool
|
|
63
|
+
ch = connection_handler
|
|
64
|
+
cp = turntable_cluster.connection_proxy
|
|
65
|
+
if ActiveRecord::VERSION::STRING >= '3.2.0'
|
|
66
|
+
ch.connection_pools[cp.spec] = PoolProxy.new(cp)
|
|
67
|
+
ch.instance_variable_get(:@class_to_pool)[name] = ch.connection_pools[cp.spec]
|
|
68
|
+
else
|
|
69
|
+
ch.connection_pools[name] = PoolProxy.new(cp)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def spec_for(config)
|
|
74
|
+
begin
|
|
75
|
+
require "active_record/connection_adapters/#{config['adapter']}_adapter"
|
|
76
|
+
rescue LoadError => e
|
|
77
|
+
raise "Please install the #{config['adapter']} adapter: `gem install activerecord-#{config['adapter']}-adapter` (#{e})"
|
|
78
|
+
end
|
|
79
|
+
adapter_method = "#{config['adapter']}_connection"
|
|
80
|
+
ActiveRecord::Base::ConnectionSpecification.new(config, adapter_method)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def clear_all_connections!
|
|
84
|
+
turntable_connections.values.each do |pool|
|
|
85
|
+
pool.disconnect!
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def sequencer
|
|
90
|
+
class_attribute :turntable_sequencer
|
|
91
|
+
self.turntable_sequencer_enabled = true
|
|
92
|
+
self.turntable_sequencer = ActiveRecord::Turntable::Sequencer.build(self)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def turntable_enabled?
|
|
96
|
+
turntable_enabled
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def sequencer_enabled?
|
|
100
|
+
turntable_sequencer_enabled
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def current_sequence
|
|
104
|
+
connection.current_sequence_value(self.sequence_name) if sequencer_enabled?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def current_last_shard
|
|
108
|
+
turntable_cluster.select_shard(current_sequence) if sequencer_enabled?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def weighted_random_shard_with(*klasses, &block)
|
|
112
|
+
shards_weight = self.turntable_cluster.weighted_shards
|
|
113
|
+
sum = shards_weight.values.inject(&:+)
|
|
114
|
+
idx = rand(sum)
|
|
115
|
+
shard, weight = shards_weight.find {|k,v|
|
|
116
|
+
(idx -= v) < 0
|
|
117
|
+
}
|
|
118
|
+
self.connection.with_recursive_shards(shard.name, *klasses, &block)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def shards_transaction(options = {}, &block)
|
|
123
|
+
self.class.shards_transaction(options, &block)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def turntable_shard
|
|
127
|
+
turntable_cluster.select_shard(self.send(turntable_shard_key))
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|