activerecord-shard_for 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +28 -0
  5. data/.travis.yml +4 -0
  6. data/Appraisals +26 -0
  7. data/CODE_OF_CONDUCT.md +49 -0
  8. data/Gemfile +5 -0
  9. data/Guardfile +21 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +296 -0
  12. data/Rakefile +10 -0
  13. data/activerecord-shard_for.gemspec +40 -0
  14. data/bin/console +15 -0
  15. data/bin/setup +8 -0
  16. data/gemfiles/.bundle/config +1 -0
  17. data/gemfiles/ar_4.1.0.gemfile +7 -0
  18. data/gemfiles/ar_4.1.0.gemfile.lock +186 -0
  19. data/gemfiles/ar_4.1.7.gemfile +7 -0
  20. data/gemfiles/ar_4.1.7.gemfile.lock +186 -0
  21. data/gemfiles/ar_4.1.8.gemfile +7 -0
  22. data/gemfiles/ar_4.1.8.gemfile.lock +186 -0
  23. data/gemfiles/ar_4.2.gemfile +7 -0
  24. data/gemfiles/ar_4.2.gemfile.lock +186 -0
  25. data/gemfiles/ar_5.0.gemfile +7 -0
  26. data/gemfiles/ar_5.0.gemfile.lock +182 -0
  27. data/gemfiles/rails_edge.gemfile +8 -0
  28. data/gemfiles/rails_edge.gemfile.lock +193 -0
  29. data/lib/activerecord/shard_for.rb +32 -0
  30. data/lib/activerecord/shard_for/all_shards_in_parallel.rb +43 -0
  31. data/lib/activerecord/shard_for/cluster_config.rb +32 -0
  32. data/lib/activerecord/shard_for/config.rb +45 -0
  33. data/lib/activerecord/shard_for/connection_router.rb +33 -0
  34. data/lib/activerecord/shard_for/database_tasks.rb +185 -0
  35. data/lib/activerecord/shard_for/errors.rb +14 -0
  36. data/lib/activerecord/shard_for/hash_modulo_router.rb +18 -0
  37. data/lib/activerecord/shard_for/model.rb +134 -0
  38. data/lib/activerecord/shard_for/railtie.rb +10 -0
  39. data/lib/activerecord/shard_for/replication_mapping.rb +37 -0
  40. data/lib/activerecord/shard_for/shard_repogitory.rb +69 -0
  41. data/lib/activerecord/shard_for/version.rb +5 -0
  42. data/lib/activerecord/tasks/activerecord_shard_for.rake +42 -0
  43. metadata +323 -0
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", :github => "rails/rails"
6
+ gem "arel", :github => "rails/arel"
7
+
8
+ gemspec :path => "../"
@@ -0,0 +1,193 @@
1
+ GIT
2
+ remote: git://github.com/rails/arel.git
3
+ revision: 09827f361d4bc73e6ff157d9feb74a465e4e4cdd
4
+ specs:
5
+ arel (7.1.1)
6
+
7
+ GIT
8
+ remote: git://github.com/rails/rails.git
9
+ revision: 815b730b1b79158511f9f4c8465c476b9fe9b7e0
10
+ specs:
11
+ activemodel (5.1.0.alpha)
12
+ activesupport (= 5.1.0.alpha)
13
+ activerecord (5.1.0.alpha)
14
+ activemodel (= 5.1.0.alpha)
15
+ activesupport (= 5.1.0.alpha)
16
+ arel (~> 7.0)
17
+ activesupport (5.1.0.alpha)
18
+ concurrent-ruby (~> 1.0, >= 1.0.2)
19
+ i18n (~> 0.7)
20
+ minitest (~> 5.1)
21
+ tzinfo (~> 1.1)
22
+
23
+ PATH
24
+ remote: ../
25
+ specs:
26
+ activerecord-shard_for (0.1.0)
27
+ activerecord (>= 4.1.0)
28
+ activesupport (>= 4.1.0)
29
+ expeditor (>= 0.1.0)
30
+
31
+ GEM
32
+ remote: https://rubygems.org/
33
+ specs:
34
+ abstract_type (0.0.7)
35
+ adamantium (0.2.0)
36
+ ice_nine (~> 0.11.0)
37
+ memoizable (~> 0.4.0)
38
+ appraisal (2.1.0)
39
+ bundler
40
+ rake
41
+ thor (>= 0.14.0)
42
+ ast (2.3.0)
43
+ binding_of_caller (0.7.2)
44
+ debug_inspector (>= 0.0.1)
45
+ coderay (1.1.1)
46
+ concord (0.1.5)
47
+ adamantium (~> 0.2.0)
48
+ equalizer (~> 0.0.9)
49
+ concurrent-ruby (1.0.2)
50
+ concurrent-ruby-ext (1.0.2)
51
+ concurrent-ruby (~> 1.0.2)
52
+ debug_inspector (0.0.2)
53
+ diff-lcs (1.2.5)
54
+ equalizer (0.0.11)
55
+ expeditor (0.4.0)
56
+ concurrent-ruby (~> 1.0.0)
57
+ concurrent-ruby-ext (~> 1.0.0)
58
+ retryable (> 1.0)
59
+ ffi (1.9.14)
60
+ formatador (0.2.5)
61
+ guard (2.14.0)
62
+ formatador (>= 0.2.4)
63
+ listen (>= 2.7, < 4.0)
64
+ lumberjack (~> 1.0)
65
+ nenv (~> 0.1)
66
+ notiffany (~> 0.0)
67
+ pry (>= 0.9.12)
68
+ shellany (~> 0.0)
69
+ thor (>= 0.18.1)
70
+ guard-compat (1.2.1)
71
+ guard-rspec (4.7.3)
72
+ guard (~> 2.1)
73
+ guard-compat (~> 1.1)
74
+ rspec (>= 2.99.0, < 4.0)
75
+ guard-rubocop (1.2.0)
76
+ guard (~> 2.0)
77
+ rubocop (~> 0.20)
78
+ i18n (0.7.0)
79
+ ice_nine (0.11.2)
80
+ interception (0.5)
81
+ listen (3.1.5)
82
+ rb-fsevent (~> 0.9, >= 0.9.4)
83
+ rb-inotify (~> 0.9, >= 0.9.7)
84
+ ruby_dep (~> 1.2)
85
+ lumberjack (1.0.10)
86
+ memoizable (0.4.2)
87
+ thread_safe (~> 0.3, >= 0.3.1)
88
+ method_source (0.8.2)
89
+ minitest (5.9.0)
90
+ nenv (0.3.0)
91
+ notiffany (0.1.1)
92
+ nenv (~> 0.1)
93
+ shellany (~> 0.0)
94
+ parser (2.3.1.2)
95
+ ast (~> 2.2)
96
+ powerpack (0.1.1)
97
+ proc_to_ast (0.1.0)
98
+ coderay
99
+ parser
100
+ unparser
101
+ procto (0.0.3)
102
+ pry (0.10.4)
103
+ coderay (~> 1.1.0)
104
+ method_source (~> 0.8.1)
105
+ slop (~> 3.4)
106
+ pry-doc (0.9.0)
107
+ pry (~> 0.9)
108
+ yard (~> 0.8)
109
+ pry-inline (1.0.2)
110
+ pry (~> 0.10.0)
111
+ unicode (~> 0.4.4)
112
+ pry-rescue (1.4.4)
113
+ interception (>= 0.5)
114
+ pry
115
+ pry-stack_explorer (0.4.9.2)
116
+ binding_of_caller (>= 0.7)
117
+ pry (>= 0.9.11)
118
+ rainbow (2.1.0)
119
+ rake (10.5.0)
120
+ rb-fsevent (0.9.7)
121
+ rb-inotify (0.9.7)
122
+ ffi (>= 0.5.0)
123
+ retryable (2.0.4)
124
+ rspec (3.5.0)
125
+ rspec-core (~> 3.5.0)
126
+ rspec-expectations (~> 3.5.0)
127
+ rspec-mocks (~> 3.5.0)
128
+ rspec-core (3.5.2)
129
+ rspec-support (~> 3.5.0)
130
+ rspec-expectations (3.5.0)
131
+ diff-lcs (>= 1.2.0, < 2.0)
132
+ rspec-support (~> 3.5.0)
133
+ rspec-mocks (3.5.0)
134
+ diff-lcs (>= 1.2.0, < 2.0)
135
+ rspec-support (~> 3.5.0)
136
+ rspec-parameterized (0.3.0)
137
+ binding_of_caller
138
+ parser
139
+ proc_to_ast
140
+ rspec (>= 2.13, < 4)
141
+ unparser
142
+ rspec-support (3.5.0)
143
+ rubocop (0.42.0)
144
+ parser (>= 2.3.1.1, < 3.0)
145
+ powerpack (~> 0.1)
146
+ rainbow (>= 1.99.1, < 3.0)
147
+ ruby-progressbar (~> 1.7)
148
+ unicode-display_width (~> 1.0, >= 1.0.1)
149
+ ruby-progressbar (1.8.1)
150
+ ruby_dep (1.3.1)
151
+ shellany (0.0.1)
152
+ slop (3.6.0)
153
+ sqlite3 (1.3.11)
154
+ thor (0.19.1)
155
+ thread_safe (0.3.5)
156
+ tzinfo (1.2.2)
157
+ thread_safe (~> 0.1)
158
+ unicode (0.4.4.2)
159
+ unicode-display_width (1.1.0)
160
+ unparser (0.2.5)
161
+ abstract_type (~> 0.0.7)
162
+ adamantium (~> 0.2.0)
163
+ concord (~> 0.1.5)
164
+ diff-lcs (~> 1.2.5)
165
+ equalizer (~> 0.0.9)
166
+ parser (~> 2.3.0)
167
+ procto (~> 0.0.2)
168
+ yard (0.9.5)
169
+
170
+ PLATFORMS
171
+ ruby
172
+
173
+ DEPENDENCIES
174
+ activerecord!
175
+ activerecord-shard_for!
176
+ appraisal
177
+ arel!
178
+ bundler (~> 1.11)
179
+ guard-rspec
180
+ guard-rubocop
181
+ pry
182
+ pry-doc
183
+ pry-inline
184
+ pry-rescue
185
+ pry-stack_explorer
186
+ rake (~> 10.0)
187
+ rspec (~> 3.0)
188
+ rspec-parameterized
189
+ rubocop
190
+ sqlite3
191
+
192
+ BUNDLED WITH
193
+ 1.12.5
@@ -0,0 +1,32 @@
1
+ require 'active_record'
2
+ require 'expeditor'
3
+ require 'activerecord/shard_for/version'
4
+ require 'activerecord/shard_for/config'
5
+ require 'activerecord/shard_for/cluster_config'
6
+ require 'activerecord/shard_for/model'
7
+ require 'activerecord/shard_for/errors'
8
+ require 'activerecord/shard_for/cluster_router'
9
+ require 'activerecord/shard_for/hash_modulo_router'
10
+ require 'activerecord/shard_for/database_tasks'
11
+ require 'activerecord/shard_for/shard_repogitory'
12
+ require 'activerecord/shard_for/all_shards_in_parallel'
13
+ require 'activerecord/shard_for/replication_mapping'
14
+ require 'activerecord/railtie' if defined?(Rails5)
15
+
16
+ module ActiveRecord
17
+ module ShardFor
18
+ class << self
19
+ # @return [Activerecord::ShardFor::Config]
20
+ def config
21
+ @config ||= Config.new
22
+ end
23
+
24
+ # @yield [Activerecord::ShardFor::Config]
25
+ def configure(&block)
26
+ config.instance_eval(&block)
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ ActiveRecord::ShardFor.config.register_cluster_router(:hash_modulo, ActiveRecord::ShardFor::HashModuloRouter)
@@ -0,0 +1,43 @@
1
+ module ActiveRecord
2
+ # Support parallel execution with each shard and deal with AR connection
3
+ # management in parallel execution.
4
+ module ShardFor
5
+ class AllShardsInParallel
6
+ # @param [Array<Class>] An array of shard model class
7
+ def initialize(shards)
8
+ @shards = shards
9
+ end
10
+
11
+ # @yield [Class] A shard model class
12
+ # @return [Array] A result
13
+ # @example
14
+ # User.all_shards_in_parallel.map(&:count).reduce(&:+)
15
+ def map
16
+ return [] unless block_given?
17
+
18
+ commands = @shards.map do |m|
19
+ Expeditor::Command.new { m.connection_pool.with_connection { yield m } }
20
+ end
21
+ commands.each(&:start)
22
+ commands.map(&:get)
23
+ end
24
+
25
+ # @yield [Class] A shard model class
26
+ # @return [Array] A result
27
+ # @example
28
+ # User.all_shards_in_parallel.flat_map {|m| m.where(age: 1) }
29
+ def flat_map(&block)
30
+ map(&block).flatten
31
+ end
32
+
33
+ # @yield [Class] A shard model class
34
+ # @return [ActiveRecord::ShardFor::AllShardsInParallel]
35
+ # @example
36
+ # User.all_shards_in_parallel.each {|m| puts m.count }
37
+ def each(&block)
38
+ map(&block) if block_given?
39
+ self
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,32 @@
1
+ module ActiveRecord
2
+ module ShardFor
3
+ class ClusterConfig
4
+ attr_reader :name, :connection_registry
5
+
6
+ # @param [Symbol] name
7
+ def initialize(name)
8
+ @name = name
9
+ @connection_registry = {}
10
+ end
11
+
12
+ # @param [Object] key sharding key object for connection
13
+ # @param [Symbol] connection_name
14
+ # @raise [RuntimeError] when duplicate entry of key
15
+ def register(key, connection_name)
16
+ raise RuntimeError.new, "#{key} is registered" if connection_registry.key?(key)
17
+ connection_registry[key] = connection_name
18
+ end
19
+
20
+ # @return [Array<Symbol>] An array of connection name
21
+ def connections
22
+ connection_registry.values
23
+ end
24
+
25
+ # @param [Object] key
26
+ # @return [Symbol] registered connection name
27
+ def fetch(key)
28
+ connection_registry.fetch(key)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,45 @@
1
+ module ActiveRecord
2
+ module ShardFor
3
+ class Config
4
+ attr_reader :cluster_configs, :routers
5
+
6
+ def initialize
7
+ @cluster_configs = {}
8
+ @routers = {}
9
+ end
10
+
11
+ # Define config for specific cluster.
12
+ # See README.md for example.
13
+ # @param [String] cluster_name
14
+ # @yield [ActiveRecord::ShardFor::ClusterConfig]
15
+ # @return [ActiveRecord::ShardFor::ClusterConfig]
16
+ # raise [RuntimeError] when this cluster config is invalid.
17
+ def define_cluster(cluster_name, &block)
18
+ cluster_config = ClusterConfig.new(cluster_name)
19
+ cluster_config.instance_eval(&block)
20
+ cluster_configs[cluster_name] = cluster_config
21
+ end
22
+
23
+ # @param [Symbol] cluster_name
24
+ # @return [ActiveRecord::ShardFor::ClusterConfig]
25
+ # @raise [KeyError] when not registered key given
26
+ def fetch_cluster_config(cluster_name)
27
+ cluster_configs.fetch(cluster_name)
28
+ end
29
+
30
+ # Register router for ActiveRecord::ShardFor
31
+ # See README.md for example.
32
+ # @param [Symbol] router_name
33
+ # @router_class [Class] router_class
34
+ def register_cluster_router(router_name, router_class)
35
+ routers[router_name] = router_class
36
+ end
37
+
38
+ # @param [Symbol] router_name
39
+ # @return [Class] registered class by [#register_router]
40
+ def fetch_cluster_router(router_name)
41
+ routers[router_name]
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,33 @@
1
+ module ActiveRecord
2
+ module ShardFor
3
+ # @abstract Subclass and override [#route] to inplement
4
+ class ConnectionRouter
5
+ attr_reader :cluster_config
6
+
7
+ # @param [ActiveRecord::ShardFor::ClusterConfig]
8
+ def initialize(cluster_config)
9
+ @cluster_config = cluster_config
10
+ end
11
+
12
+ # Fetch shard by sharding key
13
+ # @param [Object] key routing key
14
+ def fetch_connection_name(key)
15
+ cluster_config.fetch route(key)
16
+ end
17
+
18
+ # Decide routing for shard.
19
+ # Override this method in subclass.
20
+ # @param [Object] key sharding key
21
+ def route(_key)
22
+ raise NotImplementedError.new, 'Please impement this method'
23
+ end
24
+
25
+ private
26
+
27
+ # @return [Integer] count of registered connection
28
+ def connection_count
29
+ cluster_config.connections.count
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,185 @@
1
+ module ActiveRecord
2
+ module ShardFor
3
+ module DatabaseTasks
4
+ module_function
5
+
6
+ # @return [Boolean]
7
+ def ar5?
8
+ ActiveRecord::VERSION::MAJOR == 5
9
+ end
10
+
11
+ # @return [Boolean]
12
+ def ar4?
13
+ ActiveRecord::VERSION::MAJOR == 4
14
+ end
15
+
16
+ # @return [Boolean]
17
+ def ar42?
18
+ ar4? && ActiveRecord::VERSION::MINOR == 2
19
+ end
20
+
21
+ # @return [Boolean]
22
+ def ar41?
23
+ ar4? && ActiveRecord::VERSION::MINOR == 1
24
+ end
25
+
26
+ # @return [Boolean]
27
+ def ar417_above?
28
+ ar41? && ActiveRecord::VERSION::TINY > 7
29
+ end
30
+
31
+ # Show information of database sharding config.
32
+ def info
33
+ puts 'All clusters registered to ActiveRecord::ShardFor'
34
+ puts
35
+ clusters.each do |cluster|
36
+ puts "= Cluster: #{cluster.name} ="
37
+ cluster.connections.each do |name|
38
+ puts "- #{name}"
39
+ end
40
+ puts
41
+ end
42
+ end
43
+
44
+ # @private
45
+ # @param [String] task_name
46
+ # @return [Rake::Task]
47
+ def to_rake_task(task_name)
48
+ Rake::Task[task_name]
49
+ end
50
+
51
+ # @private
52
+ # @return [Array<Symbol>]
53
+ def cluster_names
54
+ ActiveRecord::ShardFor.config.cluster_configs.keys
55
+ end
56
+
57
+ # @private
58
+ # @return [Array<ActiveRecord::ShardFor::ClusterConfig>]
59
+ def clusters
60
+ ActiveRecord::ShardFor.config.cluster_configs.values
61
+ end
62
+
63
+ # @private
64
+ # @return [ActiveRecord::ShardFor::ClusterConfig]
65
+ # @raise [KeyError]
66
+ def fetch_cluster_config(cluster_name)
67
+ ActiveRecord::ShardFor.config.fetch_cluster_config(cluster_name)
68
+ end
69
+
70
+ # For mock-ablity
71
+ # @private
72
+ def exit_with_error
73
+ exit 1
74
+ end
75
+
76
+ module TasksForMultipleClusters
77
+ # @param [String] task_name
78
+ def invoke_task_for_all_clusters(task_name)
79
+ cluster_names.each do |cluster_name|
80
+ invoke_task(task_name, cluster_name)
81
+ end
82
+ end
83
+
84
+ # @private
85
+ # @param [String] name
86
+ # @param [Symbol] cluster_name
87
+ def invoke_task(name, cluster_name)
88
+ task_name = "activerecord:shard_for:#{name}"
89
+ to_rake_task(task_name).invoke(cluster_name.to_s)
90
+ to_rake_task(task_name).reenable
91
+ end
92
+ end
93
+ extend TasksForMultipleClusters
94
+
95
+ # Organize cluster config and handle error for invalid args, call single
96
+ # cluster task with each single connection config.
97
+ module TaskOrganizerForSingleClusterTask
98
+ # @param [Hash{Symbol => String}] args
99
+ def create_all_databases(args)
100
+ exec_task_for_all_databases('create', args)
101
+ end
102
+
103
+ # @param [Hash{Symbol => String}] args
104
+ def drop_all_databases(args)
105
+ exec_task_for_all_databases('drop', args)
106
+ end
107
+
108
+ # @param [Hash{Symbol => String}] args
109
+ def load_schema_all_databases(args)
110
+ exec_task_for_all_databases('load_schema', args)
111
+ end
112
+
113
+ private
114
+
115
+ # @param [String] task_name
116
+ # @param [Hash{Symbol => String}] args
117
+ def exec_task_for_all_databases(task_name, args)
118
+ cluster_name = cluster_name_or_error(task_name, args)
119
+ cluster = cluster_or_error(cluster_name)
120
+ cluster.connections.each do |connection_name|
121
+ __send__(task_name, connection_name.to_s)
122
+ end
123
+ end
124
+
125
+ # @param [String] name A task name
126
+ # @param [Hash{Symbol => String}] args
127
+ # @return [String]
128
+ def cluster_name_or_error(name, args)
129
+ cluster_name = args[:cluster_name]
130
+ return cluster_name if cluster_name
131
+
132
+ $stderr.puts <<-MSG
133
+ Missing cluster_name. Find cluster_name via `rake activerecord:shard_for:info` then call `rake "activerecord:shard_for:#{name}[$cluster_name]"`.
134
+ MSG
135
+ exit_with_error
136
+ end
137
+
138
+ # @param [String] cluster_name
139
+ # @return [ActiveRecord::ShardFor::ClusterConfig]
140
+ def cluster_or_error(cluster_name)
141
+ fetch_cluster_config(cluster_name.to_sym)
142
+ rescue KeyError
143
+ $stderr.puts %(!cluster name "#{cluster_name}" not found.!)
144
+ exit_with_error
145
+ end
146
+ end
147
+ extend TaskOrganizerForSingleClusterTask
148
+
149
+ # Create, drop, load_schema for single connection config.
150
+ module TasksForSingleConnection
151
+ # @param [String] connection_name
152
+ def create(connection_name)
153
+ configuration = ActiveRecord::Base.configurations[connection_name]
154
+ ActiveRecord::Tasks::DatabaseTasks.create(configuration)
155
+ # Re-configure using configuration with database
156
+ ActiveRecord::Base.establish_connection(configuration)
157
+ end
158
+
159
+ # @param [String] connection_name
160
+ def drop(connection_name)
161
+ configuration = ActiveRecord::Base.configurations[connection_name]
162
+ ActiveRecord::Tasks::DatabaseTasks.drop(configuration)
163
+ end
164
+
165
+ # @param [String] connection_name
166
+ def load_schema(connection_name)
167
+ configuration = ActiveRecord::Base.configurations[connection_name]
168
+
169
+ case
170
+ when ar5?
171
+ ActiveRecord::Tasks::DatabaseTasks.load_schema(configuration, :ruby)
172
+ when ar42? || ar417_above?
173
+ ActiveRecord::Tasks::DatabaseTasks.load_schema_for(configuration, :ruby)
174
+ when ar41?
175
+ ActiveRecord::Base.establish_connection(configuration)
176
+ ActiveRecord::Tasks::DatabaseTasks.load_schema(:ruby)
177
+ else
178
+ raise "This version of ActiveRecord is not supported: v#{ActiveRecord::VERSION::STRING}"
179
+ end
180
+ end
181
+ end
182
+ extend TasksForSingleConnection
183
+ end
184
+ end
185
+ end