activerecord-turntable 3.1.0 → 4.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 (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