activerecord-sharding 0.1.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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.hound.yml +2 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +50 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +10 -0
  8. data/Gemfile +4 -0
  9. data/README.md +184 -0
  10. data/Rakefile +6 -0
  11. data/activerecord-sharding.gemspec +35 -0
  12. data/bin/benchmark_sequencer.rb +71 -0
  13. data/bin/console +10 -0
  14. data/bin/setup +7 -0
  15. data/lib/active_record/sharding.rb +0 -0
  16. data/lib/active_record/sharding/abstract_repository.rb +29 -0
  17. data/lib/active_record/sharding/cluster_config.rb +32 -0
  18. data/lib/active_record/sharding/config.rb +34 -0
  19. data/lib/active_record/sharding/database_tasks.rb +234 -0
  20. data/lib/active_record/sharding/errors.rb +9 -0
  21. data/lib/active_record/sharding/model.rb +59 -0
  22. data/lib/active_record/sharding/modulo_router.rb +14 -0
  23. data/lib/active_record/sharding/railtie.rb +9 -0
  24. data/lib/active_record/sharding/sequencer.rb +40 -0
  25. data/lib/active_record/sharding/sequencer_config.rb +26 -0
  26. data/lib/active_record/sharding/sequencer_repository.rb +22 -0
  27. data/lib/active_record/sharding/shard_repository.rb +31 -0
  28. data/lib/active_record/sharding/version.rb +5 -0
  29. data/lib/activerecord-sharding.rb +30 -0
  30. data/lib/tasks/activerecord-sharding.rake +88 -0
  31. data/spec/active_record/sharding/abstract_repository_spec.rb +15 -0
  32. data/spec/active_record/sharding/cluster_config_spec.rb +41 -0
  33. data/spec/active_record/sharding/errors_spec.rb +9 -0
  34. data/spec/active_record/sharding/model_spec.rb +90 -0
  35. data/spec/active_record/sharding/modulo_router_spec.rb +22 -0
  36. data/spec/active_record/sharding/sequencer_spec.rb +31 -0
  37. data/spec/active_record/sharding/shard_repository_spec.rb +21 -0
  38. data/spec/active_record_sharding_spec.rb +15 -0
  39. data/spec/models.rb +51 -0
  40. data/spec/schema.rb +17 -0
  41. data/spec/spec_helper.rb +63 -0
  42. data/spec/tasks/activerecord-sharding_spec.rb +74 -0
  43. metadata +254 -0
@@ -0,0 +1,32 @@
1
+ module ActiveRecord
2
+ module Sharding
3
+ class ClusterConfig
4
+ attr_reader :name
5
+
6
+ def initialize(name)
7
+ @name = name
8
+ @connection_registry = []
9
+ end
10
+
11
+ def register_connection(connection_name)
12
+ @connection_registry << connection_name
13
+ end
14
+
15
+ def fetch(modulo_key)
16
+ @connection_registry[modulo_key]
17
+ end
18
+
19
+ def registerd_connection_count
20
+ @connection_registry.count
21
+ end
22
+
23
+ def validate_config!
24
+ fail "Nothing registerd connections." if registerd_connection_count == 0
25
+ end
26
+
27
+ def connections
28
+ @connection_registry
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,34 @@
1
+ module ActiveRecord
2
+ module Sharding
3
+ class Config
4
+ attr_reader :cluster_configs, :sequencer_configs
5
+
6
+ def initialize
7
+ @cluster_configs = {}
8
+ @sequencer_configs = {}
9
+ end
10
+
11
+ def define_cluster(cluster_name, &block)
12
+ cluster_config = ClusterConfig.new(cluster_name)
13
+ cluster_config.instance_eval(&block)
14
+ cluster_config.validate_config!
15
+ @cluster_configs[cluster_name] = cluster_config
16
+ end
17
+
18
+ def fetch_cluster_config(cluster_name)
19
+ @cluster_configs.fetch cluster_name
20
+ end
21
+
22
+ def define_sequencer(sequencer_name, &block)
23
+ sequencer_config = SequencerConfig.new sequencer_name
24
+ sequencer_config.instance_eval(&block)
25
+ sequencer_config.validate_config!
26
+ @sequencer_configs[sequencer_name] = sequencer_config
27
+ end
28
+
29
+ def fetch_sequencer_config(sequencer_name)
30
+ @sequencer_configs.fetch sequencer_name
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,234 @@
1
+ module ActiveRecord
2
+ module Sharding
3
+ module DatabaseTasks
4
+ extend self
5
+
6
+ def info
7
+ puts "All clusters registered to activerecord-sharding"
8
+ puts
9
+ clusters.each do |cluster|
10
+ puts "= Cluster: #{cluster.name} ="
11
+ cluster.connections.each do |name|
12
+ puts "- #{name}"
13
+ end
14
+ puts
15
+ end
16
+ puts_sequencers
17
+ end
18
+
19
+ def puts_sequencers
20
+ return unless sequencers
21
+
22
+ puts "All sequencers registered to activerecord-sharding"
23
+ puts
24
+ sequencers.each do |sequencer|
25
+ puts "= Sequencer: #{sequencer.name} ="
26
+ puts "- Connection:#{sequencer.connection_name} Table:#{sequencer.table_name}"
27
+ puts
28
+ end
29
+ end
30
+
31
+ def ar5?
32
+ ActiveRecord::VERSION::MAJOR == 5
33
+ end
34
+
35
+ def ar4?
36
+ ActiveRecord::VERSION::MAJOR == 4
37
+ end
38
+
39
+ def ar42?
40
+ ar4? && ActiveRecord::VERSION::MINOR == 2
41
+ end
42
+
43
+ def ar41?
44
+ ar4? && ActiveRecord::VERSION::MINOR == 1
45
+ end
46
+
47
+ def ar417_above?
48
+ ar41? && ActiveRecord::VERSION::TINY > 7
49
+ end
50
+
51
+ def clusters
52
+ ActiveRecord::Sharding.config.cluster_configs.values
53
+ end
54
+
55
+ def cluster_names
56
+ ActiveRecord::Sharding.config.cluster_configs.keys
57
+ end
58
+
59
+ def sequencer_names
60
+ ActiveRecord::Sharding.config.sequencer_configs.keys
61
+ end
62
+
63
+ def fetch_cluster_config(cluster_name)
64
+ ActiveRecord::Sharding.config.fetch_cluster_config cluster_name
65
+ end
66
+
67
+ def sequencers
68
+ ActiveRecord::Sharding.config.sequencer_configs.values
69
+ end
70
+
71
+ def fetch_sequencer_config(sequencer_name)
72
+ ActiveRecord::Sharding.config.fetch_sequencer_config sequencer_name
73
+ end
74
+
75
+ def to_rake_task(task_name)
76
+ Rake::Task[task_name]
77
+ end
78
+
79
+ module TasksForMultipleClusters
80
+ def invoke_task_for_all_clusters(task_name)
81
+ cluster_names.each do |cluster_name|
82
+ invoke_task task_name, cluster_name
83
+ end
84
+ end
85
+
86
+ def invoke_task(name, cluster_name)
87
+ task_name = "active_record:sharding:#{name}"
88
+ to_rake_task(task_name).invoke cluster_name.to_s
89
+ to_rake_task(task_name).reenable
90
+ end
91
+
92
+ def invoke_task_for_all_sequencers(task_name)
93
+ sequencer_names.each do |sequencer_name|
94
+ invoke_task_for_sequencer task_name, sequencer_name
95
+ end
96
+ end
97
+
98
+ def invoke_task_for_sequencer(name, sequencer_name)
99
+ task_name = "active_record:sharding:sequencer:#{name}"
100
+ to_rake_task(task_name).invoke sequencer_name.to_s
101
+ to_rake_task(task_name).reenable
102
+ end
103
+ end
104
+ extend TasksForMultipleClusters
105
+
106
+ module TaskOrganizerForSingleClusterTask
107
+ def create_all_databases(args)
108
+ exec_task_for_all_databases "create", args
109
+ end
110
+
111
+ def drop_all_databases(args)
112
+ exec_task_for_all_databases "drop", args
113
+ end
114
+
115
+ def load_schema_all_databases(args)
116
+ exec_task_for_all_databases "load_schema", args
117
+ end
118
+
119
+ private
120
+
121
+ def exec_task_for_all_databases(task_name, args)
122
+ cluster_name = cluster_name_or_error task_name, args
123
+ cluster = cluster_or_error cluster_name
124
+ cluster.connections.each do |connection_name|
125
+ __send__ task_name, connection_name.to_s
126
+ end
127
+ end
128
+
129
+ def cluster_name_or_error(name, args)
130
+ unless cluster_name = args[:cluster_name]
131
+ $stderr.puts <<-MSG
132
+ Missing cluster_name. Find cluster_name via `rake active_record:sharding:info` then call `rake "active_record:sharding:#{name}[$cluster_name]"`.
133
+ MSG
134
+ exit
135
+ end
136
+ cluster_name
137
+ end
138
+
139
+ def cluster_or_error(cluster_name)
140
+ fetch_cluster_config cluster_name.to_sym
141
+ rescue KeyError
142
+ $stderr.puts %(cluster name "#{cluster_name}" not found.)
143
+ exit
144
+ end
145
+ end
146
+ extend TaskOrganizerForSingleClusterTask
147
+
148
+ module TasksForSingleConnection
149
+ def create(connection_name)
150
+ configuration = ActiveRecord::Base.configurations[connection_name]
151
+ ActiveRecord::Tasks::DatabaseTasks.create(configuration)
152
+ ActiveRecord::Base.establish_connection(configuration)
153
+ end
154
+
155
+ def drop(connection_name)
156
+ configuration = ActiveRecord::Base.configurations[connection_name]
157
+ ActiveRecord::Tasks::DatabaseTasks.drop configuration
158
+ end
159
+
160
+ def execute(connection_name, sql)
161
+ configuration = ActiveRecord::Base.configurations[connection_name]
162
+ ActiveRecord::Base.establish_connection(configuration).connection.execute sql
163
+ end
164
+
165
+ def load_schema(connection_name)
166
+ configuration = ActiveRecord::Base.configurations[connection_name]
167
+
168
+ case
169
+ when ar5?
170
+ ActiveRecord::Tasks::DatabaseTasks.load_schema configuration, :ruby
171
+ when ar42? || ar417_above?
172
+ ActiveRecord::Tasks::DatabaseTasks.load_schema_for configuration, :ruby
173
+ when ar41?
174
+ ActiveRecord::Base.establish_connection configuration
175
+ ActiveRecord::Tasks::DatabaseTasks.load_schema :ruby
176
+ else
177
+ fail "This version of ActiveRecord is not supported: v#{ActiveRecord::VERSION::STRING}"
178
+ end
179
+ end
180
+ end
181
+ extend TasksForSingleConnection
182
+
183
+ module TasksForSingleSequencerTask
184
+ def create_sequencer_database(args)
185
+ exec_task_for_sequencer_database "create", args
186
+ end
187
+
188
+ def drop_sequencer_database(args)
189
+ exec_task_for_sequencer_database "drop", args
190
+ end
191
+
192
+ def create_table_sequencer_database(args)
193
+ sequencer = sequencer_or_error "create_table", args
194
+ create_table_sql = "CREATE TABLE #{sequencer.table_name} (id BIGINT unsigned NOT NULL DEFAULT 0) ENGINE=MyISAM"
195
+ execute sequencer.connection_name.to_s, create_table_sql
196
+ end
197
+
198
+ def insert_initial_record_sequencer_database(args)
199
+ sequencer = sequencer_or_error "insert_initial_record", args
200
+ insert_initial_record_sql = "INSERT INTO #{sequencer.table_name} VALUES (0)"
201
+ execute sequencer.connection_name.to_s, insert_initial_record_sql
202
+ end
203
+
204
+ private
205
+
206
+ def exec_task_for_sequencer_database(task_name, args)
207
+ sequencer = sequencer_or_error task_name, args
208
+ __send__ task_name, sequencer.connection_name.to_s
209
+ end
210
+
211
+ def sequencer_or_error(task_name, args)
212
+ sequencer_name = sequencer_name_or_error task_name, args
213
+ fetch_sequencer_config sequencer_name.to_sym
214
+ rescue KeyError
215
+ $stderr.puts %(sequencer name "#{sequencer_name}" not found.)
216
+ exit
217
+ end
218
+
219
+ def sequencer_name_or_error(task_name, args)
220
+ unless sequencer_name = args[:sequencer_name]
221
+ # rubocop:disable Metrics/LineLength
222
+ $stderr.puts <<-MSG
223
+ Missing sequencer_name. Find sequencer_name via `rake active_record:sharding:info` then call `rake "active_record:sharding:sequencer#{task_name}[$sequencer_name]"`.
224
+ MSG
225
+ exit
226
+ # rubocop:enable Metrics/LineLength
227
+ end
228
+ sequencer_name
229
+ end
230
+ end
231
+ extend TasksForSingleSequencerTask
232
+ end # module DatabaseTasks
233
+ end
234
+ end
@@ -0,0 +1,9 @@
1
+ module ActiveRecord
2
+ module Sharding
3
+ class Error < ::StandardError
4
+ end
5
+
6
+ class MissingShardingKeyAttribute < Error
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,59 @@
1
+ require "active_support/concern"
2
+
3
+ module ActiveRecord
4
+ module Sharding
5
+ module Model
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class_attribute :cluster_router, instance_writer: false
10
+ class_attribute :shard_repository, instance_writer: false
11
+ class_attribute :sharding_key, instance_writer: false
12
+ end
13
+
14
+ module ClassMethods
15
+ def use_sharding(name, algorithm = :modulo)
16
+ config = ActiveRecord::Sharding.config.fetch_cluster_config name
17
+ if algorithm == :modulo
18
+ self.cluster_router = ActiveRecord::Sharding::ModuloRouter.new config
19
+ end
20
+ self.shard_repository = ActiveRecord::Sharding::ShardRepository.new config, self
21
+ self.abstract_class = true
22
+ end
23
+
24
+ def define_sharding_key(column)
25
+ self.sharding_key = column.to_sym
26
+ end
27
+
28
+ def before_put(&block)
29
+ @before_put_callback = block
30
+ end
31
+
32
+ def put!(attributes)
33
+ fail "`sharding_key` is not defined. Use `define_sharding_key`." unless sharding_key
34
+
35
+ @before_put_callback.call(attributes) if @before_put_callback
36
+
37
+ if key = attributes[sharding_key] || attributes[sharding_key.to_s]
38
+ shard_for(key).create!(attributes)
39
+ else
40
+ fail ActiveRecord::Sharding::MissingShardingKeyAttribute
41
+ end
42
+ end
43
+
44
+ def shard_for(key)
45
+ connection_name = cluster_router.route key
46
+ shard_repository.fetch connection_name
47
+ end
48
+
49
+ def all_shards
50
+ shard_repository.all
51
+ end
52
+
53
+ def define_parent_methods(&block)
54
+ instance_eval(&block)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,14 @@
1
+ module ActiveRecord
2
+ module Sharding
3
+ class ModuloRouter
4
+ def initialize(cluster_config)
5
+ @cluster_config = cluster_config
6
+ end
7
+
8
+ def route(id)
9
+ modulo_key = id % @cluster_config.registerd_connection_count
10
+ @cluster_config.fetch modulo_key
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ module ActiveRecord
2
+ module Sharding
3
+ class Railtie < ::Rails::Railtie
4
+ rake_tasks do
5
+ load File.expand_path("../../../tasks/activerecord-sharding.rake", __FILE__)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,40 @@
1
+ require "active_support/concern"
2
+
3
+ module ActiveRecord
4
+ module Sharding
5
+ module Sequencer
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class_attribute :sequencer_repository, instance_writer: false
10
+ class_attribute :sequencer_name, instance_writer: false
11
+ class_attribute :sequencer_config, instance_writer: false
12
+ end
13
+
14
+ module ClassMethods
15
+ def use_sequencer(name)
16
+ self.sequencer_name = name
17
+ self.sequencer_config = ActiveRecord::Sharding.config.fetch_sequencer_config name
18
+ self.sequencer_repository = ActiveRecord::Sharding::SequencerRepository.new sequencer_config, self
19
+ self.abstract_class = true
20
+ end
21
+
22
+ def current_sequence_id
23
+ execute_sql "id"
24
+ end
25
+
26
+ def next_sequence_id
27
+ execute_sql "id +1"
28
+ end
29
+
30
+ def execute_sql(last_insert_id_args)
31
+ connection = sequencer_repository.fetch(sequencer_name).connection
32
+ connection.execute "UPDATE `#{sequencer_config.table_name}` SET id = LAST_INSERT_ID(#{last_insert_id_args})"
33
+ res = connection.execute "SELECT LAST_INSERT_ID()"
34
+ new_id = res.first.first.to_i
35
+ new_id
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,26 @@
1
+ module ActiveRecord
2
+ module Sharding
3
+ class SequencerConfig
4
+ attr_reader :name, :table_name, :connection_name
5
+
6
+ def initialize(name)
7
+ @name = name
8
+ @table_name = nil
9
+ @connection_name = nil
10
+ end
11
+
12
+ def register_connection(connection_name)
13
+ @connection_name = connection_name
14
+ end
15
+
16
+ def register_table_name(table_name)
17
+ @table_name = table_name
18
+ end
19
+
20
+ def validate_config!
21
+ fail "Nothing connection. Please call register_connection" if @connection_name.blank?
22
+ fail "Nothing table_name. Please call register_table_name" if @table_name.blank?
23
+ end
24
+ end
25
+ end
26
+ end