activerecord-turntable 3.1.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -0
  3. data/.travis.yml +25 -9
  4. data/CHANGELOG.md +25 -0
  5. data/Gemfile +2 -0
  6. data/Guardfile +1 -1
  7. data/README.md +202 -8
  8. data/Rakefile +42 -67
  9. data/activerecord-turntable.gemspec +3 -2
  10. data/gemfiles/rails5_0_0.gemfile +2 -0
  11. data/gemfiles/rails5_0_1.gemfile +2 -0
  12. data/gemfiles/rails5_0_2.gemfile +2 -0
  13. data/gemfiles/rails5_0_3.gemfile +2 -0
  14. data/gemfiles/rails5_0_4.gemfile +8 -0
  15. data/gemfiles/rails5_0_5.gemfile +8 -0
  16. data/gemfiles/rails5_1_0.gemfile +2 -0
  17. data/gemfiles/rails5_1_1.gemfile +2 -0
  18. data/gemfiles/rails5_1_2.gemfile +9 -0
  19. data/gemfiles/rails5_1_3.gemfile +9 -0
  20. data/gemfiles/rails5_1_4.gemfile +9 -0
  21. data/gemfiles/rails5_1_5.gemfile +9 -0
  22. data/lib/active_record/turntable.rb +10 -20
  23. data/lib/active_record/turntable/active_record_ext.rb +1 -1
  24. data/lib/active_record/turntable/active_record_ext/log_subscriber.rb +5 -1
  25. data/lib/active_record/turntable/active_record_ext/sequencer.rb +11 -11
  26. data/lib/active_record/turntable/active_record_ext/transactions.rb +5 -4
  27. data/lib/active_record/turntable/algorithm.rb +12 -0
  28. data/lib/active_record/turntable/algorithm/base.rb +6 -2
  29. data/lib/active_record/turntable/algorithm/hash_slot_algorithm.rb +35 -0
  30. data/lib/active_record/turntable/algorithm/modulo_algorithm.rb +3 -7
  31. data/lib/active_record/turntable/algorithm/range_algorithm.rb +3 -34
  32. data/lib/active_record/turntable/algorithm/range_bsearch_algorithm.rb +16 -28
  33. data/lib/active_record/turntable/base.rb +43 -39
  34. data/lib/active_record/turntable/cluster.rb +31 -29
  35. data/lib/active_record/turntable/cluster_helper_methods.rb +12 -2
  36. data/lib/active_record/turntable/cluster_registry.rb +7 -0
  37. data/lib/active_record/turntable/configuration.rb +50 -0
  38. data/lib/active_record/turntable/configuration/dsl.rb +79 -0
  39. data/lib/active_record/turntable/configuration/loader.rb +10 -0
  40. data/lib/active_record/turntable/configuration/loader/dsl.rb +23 -0
  41. data/lib/active_record/turntable/configuration/loader/yaml.rb +62 -0
  42. data/lib/active_record/turntable/configuration_methods.rb +26 -0
  43. data/lib/active_record/turntable/connection_proxy.rb +51 -40
  44. data/lib/active_record/turntable/{master_shard.rb → default_shard.rb} +6 -2
  45. data/lib/active_record/turntable/deprecation.rb +8 -0
  46. data/lib/active_record/turntable/error.rb +2 -1
  47. data/lib/active_record/turntable/migration.rb +3 -7
  48. data/lib/active_record/turntable/mixer.rb +10 -10
  49. data/lib/active_record/turntable/mixer/fader.rb +1 -1
  50. data/lib/active_record/turntable/pool_proxy.rb +5 -3
  51. data/lib/active_record/turntable/railtie.rb +11 -4
  52. data/lib/active_record/turntable/seq_shard.rb +8 -9
  53. data/lib/active_record/turntable/sequencer.rb +18 -43
  54. data/lib/active_record/turntable/sequencer/api.rb +3 -5
  55. data/lib/active_record/turntable/sequencer/barrage.rb +1 -2
  56. data/lib/active_record/turntable/sequencer/katsubushi.rb +27 -0
  57. data/lib/active_record/turntable/sequencer/mysql.rb +14 -6
  58. data/lib/active_record/turntable/sequencer_registry.rb +30 -0
  59. data/lib/active_record/turntable/shard.rb +31 -10
  60. data/lib/active_record/turntable/shard_registry.rb +36 -0
  61. data/lib/active_record/turntable/slave_registry.rb +21 -0
  62. data/lib/active_record/turntable/slave_shard.rb +9 -0
  63. data/lib/active_record/turntable/version.rb +1 -1
  64. data/lib/generators/templates/turntable.rb +50 -0
  65. data/lib/generators/templates/turntable.yml +18 -0
  66. metadata +54 -20
  67. data/lib/active_record/turntable/config.rb +0 -26
@@ -5,20 +5,33 @@ module ActiveRecord::Turntable
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do
8
- class_attribute :turntable_connections, :turntable_clusters,
9
- :turntable_enabled, :turntable_sequencer_enabled
8
+ class_attribute :turntable_connections, :turntable_clusters, :turntable_sequencers,
9
+ :turntable_enabled, :turntable_sequencer_enabled, :turntable_configuration
10
10
 
11
11
  self.turntable_connections = {}
12
12
  self.turntable_clusters = {}.with_indifferent_access
13
+ self.turntable_sequencers = {}.with_indifferent_access
13
14
  self.turntable_enabled = false
14
15
  self.turntable_sequencer_enabled = false
16
+
15
17
  class << self
16
18
  delegate :shards_transaction, :with_all, to: :connection
17
- end
18
19
 
19
- ActiveSupport.on_load(:turntable_config_loaded) do
20
- self.initialize_clusters!
20
+ def reset_turntable_configuration(configuration, reset = true)
21
+ old = self.turntable_configuration
22
+ self.turntable_configuration = configuration
23
+
24
+ old.release! if old
25
+
26
+ if reset
27
+ # TODO: replace exitsting connection_pool when configurations reloaded
28
+ self.turntable_clusters = turntable_configuration.clusters
29
+ self.turntable_sequencers = turntable_configuration.sequencers
30
+ ActiveSupport.run_load_hooks(:turntable_configuration_loaded, ActiveRecord::Base)
31
+ end
32
+ end
21
33
  end
34
+
22
35
  include ClusterHelperMethods
23
36
  end
24
37
 
@@ -27,20 +40,18 @@ module ActiveRecord::Turntable
27
40
  # @param [Symbol] shard_key_name shard key attribute name
28
41
  # @param [Hash] options
29
42
  def turntable(cluster_name, shard_key_name, options = {})
30
- class_attribute :turntable_shard_key,
31
- :turntable_cluster, :turntable_cluster_name
43
+ class_attribute :turntable_shard_key, :turntable_cluster_name
32
44
 
33
45
  self.turntable_enabled = true
34
46
  self.turntable_cluster_name = cluster_name
35
47
  self.turntable_shard_key = shard_key_name
36
- self.turntable_cluster =
37
- self.turntable_clusters[cluster_name] ||= Cluster.new(
38
- turntable_config[:clusters][cluster_name],
39
- options
40
- )
41
48
  turntable_replace_connection_pool
42
49
  end
43
50
 
51
+ def turntable_cluster
52
+ turntable_clusters[turntable_cluster_name]
53
+ end
54
+
44
55
  def turntable_replace_connection_pool
45
56
  ch = connection_handler
46
57
  cp = ConnectionProxy.new(self, turntable_cluster)
@@ -50,31 +61,22 @@ module ActiveRecord::Turntable
50
61
  ch.send(:owner_to_pool)[connection_specification_name] = pp
51
62
  end
52
63
 
53
- def initialize_clusters!
54
- turntable_config[:clusters].each do |name, spec|
55
- self.turntable_clusters[name] ||= Cluster.new(spec)
56
- end
57
- end
58
-
59
- def spec_for(config)
60
- begin
61
- require "active_record/connection_adapters/#{config['adapter']}_adapter"
62
- rescue LoadError => e
63
- raise "Please install the #{config['adapter']} adapter: `gem install activerecord-#{config['adapter']}-adapter` (#{e})"
64
- end
65
- adapter_method = "#{config['adapter']}_connection"
66
- ActiveRecord::ConnectionAdapters::ConnectionSpecification.new(config, adapter_method)
67
- end
68
-
69
64
  def clear_all_connections!
70
65
  turntable_connections.values.each(&:disconnect!)
71
66
  end
72
67
 
73
- def sequencer(sequence_name, *args)
74
- class_attribute :turntable_sequencer
68
+ def sequencer(sequencer_name, *args)
69
+ class_attribute :turntable_sequencer_name
70
+ class << self
71
+ prepend ActiveRecordExt::Sequencer
72
+ end
75
73
 
76
74
  self.turntable_sequencer_enabled = true
77
- self.turntable_sequencer = ActiveRecord::Turntable::Sequencer.build(self, sequence_name, *args)
75
+ self.turntable_sequencer_name = sequencer_name
76
+ end
77
+
78
+ def turntable_sequencer
79
+ turntable_sequencers[turntable_sequencer_name]
78
80
  end
79
81
 
80
82
  def turntable_enabled?
@@ -85,12 +87,8 @@ module ActiveRecord::Turntable
85
87
  turntable_sequencer_enabled
86
88
  end
87
89
 
88
- def current_sequence
89
- connection.current_sequence_value(self.sequence_name) if sequencer_enabled?
90
- end
91
-
92
90
  def current_last_shard
93
- turntable_cluster.select_shard(current_sequence) if sequencer_enabled?
91
+ turntable_cluster.select_shard(current_sequence_value) if sequencer_enabled?
94
92
  end
95
93
 
96
94
  def with_shard(any_shard)
@@ -104,12 +102,18 @@ module ActiveRecord::Turntable
104
102
  end
105
103
  connection.with_shard(shard) { yield }
106
104
  end
107
- end
108
105
 
109
- def shards_transaction(options = {}, &block)
110
- self.class.shards_transaction(options, &block)
106
+ def with_slave
107
+ connection.with_slave { yield }
108
+ end
109
+
110
+ def with_master
111
+ connection.with_master { yield }
112
+ end
111
113
  end
112
114
 
115
+ delegate :shards_transaction, :turntable_cluster, to: :class
116
+
113
117
  # @return [ActiveRecord::Turntable::Shard] current shard for self
114
118
  def turntable_shard
115
119
  turntable_cluster.shard_for(self.send(turntable_shard_key))
@@ -1,4 +1,5 @@
1
1
  require "active_support/core_ext/hash/indifferent_access"
2
+ require "concurrent/atomic/thread_local_var"
2
3
 
3
4
  module ActiveRecord::Turntable
4
5
  class Cluster
@@ -7,40 +8,27 @@ module ActiveRecord::Turntable
7
8
  "algorithm" => "range",
8
9
  }.with_indifferent_access
9
10
 
10
- def initialize(cluster_spec, options = {})
11
- @config = DEFAULT_CONFIG.merge(cluster_spec)
12
- @options = options.with_indifferent_access
13
- @shards = {}.with_indifferent_access
11
+ attr_accessor :algorithm, :shard_registry, :sequencer_registry
14
12
 
15
- # setup sequencer
16
- seq = (@options[:seq] || @config[:seq])
17
- if seq
18
- if seq.values.size > 0 && seq.values.first[:seq_type] == "mysql"
19
- @seq_shard = SeqShard.new(seq.values.first)
20
- end
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)
13
+ def initialize
14
+ @slave_enabled = Concurrent::ThreadLocalVar.new(false)
31
15
  end
32
16
 
33
- def seq
34
- @seq_shard
17
+ def self.build(sequencer_registry)
18
+ self.new.tap do |instance|
19
+ instance.shard_registry = ShardRegistry.new(instance)
20
+ instance.sequencer_registry = sequencer_registry
21
+ yield instance
22
+ end
35
23
  end
36
24
 
37
- attr_reader :shards
25
+ delegate :shards, :shard_maps, :release!, to: :shard_registry
38
26
 
39
27
  def shard_for(key)
40
- @shards[@algorithm.calculate(key)]
28
+ algorithm.choose(shard_maps, key)
41
29
  rescue
42
30
  raise ActiveRecord::Turntable::CannotSpecifyShardError,
43
- "cannot select_shard for key:#{key}"
31
+ "cannot select shard for key:#{key.inspect}"
44
32
  end
45
33
 
46
34
  def select_shard(key)
@@ -52,7 +40,7 @@ module ActiveRecord::Turntable
52
40
  unless in_recursion
53
41
  shards = Array.wrap(shards).dup
54
42
  if shards.blank?
55
- shards = @shards.values.dup
43
+ shards = self.shards.dup
56
44
  end
57
45
  end
58
46
  shard = to_shard(shards.shift)
@@ -83,10 +71,24 @@ module ActiveRecord::Turntable
83
71
  end
84
72
  end
85
73
 
74
+ def slave_enabled?
75
+ @slave_enabled.value
76
+ end
77
+
78
+ def set_slave_enabled(enabled)
79
+ @slave_enabled.value = enabled
80
+ end
81
+
82
+ def sequencers
83
+ sequencer_registry.all
84
+ end
85
+
86
+ def sequencer(name)
87
+ sequencers[name]
88
+ end
89
+
86
90
  def weighted_shards(key = nil)
87
- Hash[@algorithm.calculate_used_shards_with_weight(key).map do |k, v|
88
- [@shards[k], v]
89
- end]
91
+ @algorithm.shard_weights(shard_maps, key)
90
92
  end
91
93
  end
92
94
  end
@@ -3,7 +3,7 @@ module ActiveRecord::Turntable
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
- ActiveSupport.on_load(:turntable_config_loaded) do
6
+ ActiveSupport.on_load(:turntable_configuration_loaded) do
7
7
  turntable_clusters.each do |name, _cluster|
8
8
  turntable_define_cluster_methods(name)
9
9
  end
@@ -42,8 +42,18 @@ module ActiveRecord::Turntable
42
42
  end
43
43
  end
44
44
 
45
+ def spec_for(config)
46
+ begin
47
+ require "active_record/connection_adapters/#{config["adapter"]}_adapter"
48
+ rescue LoadError => e
49
+ raise "Please install the #{config["adapter"]} adapter: `gem install activerecord-#{config["adapter"]}-adapter` (#{e})"
50
+ end
51
+ adapter_method = "#{config["adapter"]}_connection"
52
+ ActiveRecord::ConnectionAdapters::ConnectionSpecification.new(config, adapter_method)
53
+ end
54
+
45
55
  def weighted_random_shard_with(*klasses, &block)
46
- shards_weight = self.turntable_cluster.weighted_shards(self.current_sequence)
56
+ shards_weight = self.turntable_cluster.weighted_shards(self.current_sequence_value(sequence_name))
47
57
  sum = shards_weight.values.inject(&:+)
48
58
  idx = rand(sum)
49
59
  shard, _weight = shards_weight.find { |_k, v|
@@ -0,0 +1,7 @@
1
+ module ActiveRecord::Turntable
2
+ class ClusterRegistry < HashWithIndifferentAccess
3
+ def release!
4
+ values.each(&:release!)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,50 @@
1
+ module ActiveRecord::Turntable
2
+ class Configuration
3
+ extend ActiveSupport::Autoload
4
+ autoload :DSL
5
+ autoload :Loader
6
+
7
+ attr_reader :cluster_registry, :sequencer_registry
8
+ attr_accessor :raise_on_not_specified_shard_query,
9
+ :raise_on_not_specified_shard_update
10
+ alias_method :configure, :instance_exec
11
+ alias_method :clusters, :cluster_registry
12
+
13
+ def initialize
14
+ @cluster_registry = ClusterRegistry.new
15
+ @sequencer_registry = SequencerRegistry.new
16
+ end
17
+
18
+ def cluster(name)
19
+ cluster_registry[name]
20
+ end
21
+
22
+ def sequencers
23
+ sequencer_registry.sequencers
24
+ end
25
+
26
+ def sequencer(name)
27
+ sequencer_registry[name]
28
+ end
29
+
30
+ def release!
31
+ cluster_registry.release!
32
+ sequencer_registry.release!
33
+ end
34
+
35
+ def self.configure(&block)
36
+ new.tap { |c| c.configure(&block) }
37
+ end
38
+
39
+ def self.load(path, env)
40
+ case File.extname(path)
41
+ when ".yml"
42
+ Loader::YAML.load(path, env)
43
+ when ".rb"
44
+ Loader::DSL.load(path)
45
+ else
46
+ raise InvalidConfigurationError, "Invalid configuration file path: #{path}"
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,79 @@
1
+ require "active_record/turntable/configuration"
2
+
3
+ module ActiveRecord::Turntable
4
+ class Configuration
5
+ class DSL
6
+ attr_reader :configuration
7
+
8
+ def initialize(configuration = Configuration.new)
9
+ @configuration = configuration
10
+ end
11
+
12
+ def cluster(name, &block)
13
+ cluster_dsl = ClusterDSL.new(configuration).tap { |dsl| dsl.instance_exec(&block) }
14
+ configuration.clusters[name] = cluster_dsl.cluster
15
+ end
16
+
17
+ GLOBAL_SETTINGS_KEYS = %w(
18
+ raise_on_not_specified_shard_query
19
+ raise_on_not_specified_shard_update
20
+ ).freeze
21
+
22
+ GLOBAL_SETTINGS_KEYS.each do |k|
23
+ define_method(k) do |value|
24
+ configuration.send("#{k}=", value)
25
+ end
26
+ end
27
+
28
+ def global_settings_keys
29
+ GLOBAL_SETTINGS_KEYS
30
+ end
31
+
32
+ class ClusterDSL < DSL
33
+ attr_reader :sequencers
34
+
35
+ def initialize(configuration)
36
+ super
37
+ @algorithm = Algorithm.class_for("range").new(nil)
38
+ @shard_settings = []
39
+ @sequencer_settings = []
40
+ end
41
+
42
+ def cluster
43
+ Cluster.build(configuration.sequencer_registry) do |c|
44
+ c.algorithm = @algorithm
45
+ @shard_settings.each do |setting|
46
+ c.shard_registry.add(setting)
47
+ end
48
+ @sequencer_settings.each do |name, type, options|
49
+ c.sequencer_registry.add(name, type, options, c)
50
+ end
51
+ end
52
+ end
53
+
54
+ def algorithm(type, options = {})
55
+ @algorithm = Algorithm.class_for(type.to_s).new(options)
56
+ end
57
+
58
+ def sequencer(sequencer_name, type, options = {})
59
+ @sequencer_settings << [sequencer_name.to_s, type.to_s, options]
60
+ end
61
+
62
+ ShardSetting = Struct.new(:name, :range, :slaves) do
63
+ def range
64
+ case self[:range]
65
+ when Integer
66
+ self[:range]..self[:range]
67
+ else
68
+ self[:range]
69
+ end
70
+ end
71
+ end
72
+
73
+ def shard(range, slaves: [], to:)
74
+ @shard_settings << ShardSetting.new(to.to_s, range, slaves.map(&:to_s))
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,10 @@
1
+ module ActiveRecord::Turntable
2
+ class Configuration
3
+ module Loader
4
+ extend ActiveSupport::Autoload
5
+
6
+ autoload :YAML
7
+ autoload :DSL
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,23 @@
1
+ module ActiveRecord::Turntable
2
+ class Configuration
3
+ class Loader::DSL
4
+ attr_reader :path, :configuration, :dsl
5
+
6
+ def initialize(path, configuration = Configuration.new)
7
+ @path = path
8
+ @configuration = configuration
9
+ @dsl = DSL.new(@configuration)
10
+ end
11
+
12
+ def self.load(path, configuration = Configuration.new)
13
+ new(path, configuration).load
14
+ end
15
+
16
+ def load
17
+ @dsl.instance_eval(File.read(@path), @path, 1)
18
+
19
+ configuration
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,62 @@
1
+ module ActiveRecord::Turntable
2
+ class Configuration
3
+ class Loader::YAML
4
+ attr_reader :path, :configuration, :dsl
5
+
6
+ def initialize(path, configuration = Configuration.new)
7
+ @path = path
8
+ @configuration = configuration
9
+ @dsl = DSL.new(@configuration)
10
+ end
11
+
12
+ def self.load(path, env, configuration = Configuration.new)
13
+ new(path, configuration).load(env)
14
+ end
15
+
16
+ def load(env)
17
+ yaml = YAML.load(ERB.new(IO.read(path)).result).with_indifferent_access[env]
18
+ load_clusters(yaml[:clusters])
19
+ load_global_settings(yaml)
20
+
21
+ configuration
22
+ end
23
+
24
+ private
25
+
26
+ def load_clusters(clusters_config)
27
+ clusters_config.each do |cluster_name, conf|
28
+ @dsl.cluster(cluster_name) do
29
+ algorithm conf[:algorithm] if conf[:algorithm]
30
+
31
+ if conf[:seq]
32
+ conf[:seq].each do |sequence_name, sequence_conf|
33
+ sequencer(sequence_name, (sequence_conf[:seq_type] || :mysql), sequence_conf)
34
+ end
35
+ end
36
+
37
+ if conf[:shards]
38
+ current_lower_limit = 1
39
+ conf[:shards].each do |shard_conf|
40
+ upper_limit = if shard_conf.has_key?(:less_than)
41
+ shard_conf[:less_than] - 1
42
+ else
43
+ current_lower_limit
44
+ end
45
+ shard current_lower_limit..upper_limit, to: shard_conf[:connection], slaves: Array.wrap(shard_conf[:slaves])
46
+ current_lower_limit = upper_limit + 1
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ def load_global_settings(yaml)
54
+ yaml.each do |k, v|
55
+ if @dsl.global_settings_keys.include?(k)
56
+ @dsl.send(k, v)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end