activerecord-shard_for 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 +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