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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 63902d23b9848938b672498a54cbd5585c1606b5
4
+ data.tar.gz: f02378bc48f50bf8aff0c6de70d8d36e6d952770
5
+ SHA512:
6
+ metadata.gz: 1dfd3d667c3a89ef8d2b375bbdec286c3135d5384fe4f8179773e4828ba8bc75f136d536f4d9084d6b2c19f1342f5ff1e3e02a3984473ccc18c198b0bb9da9f1
7
+ data.tar.gz: 0651cf764cb9c49db9ca299487090b12a5437be918b888e662e7eae2626c397bbe015aa99103a45030040e327e634bc2166f39c64877669ff67bfc0bb272ac98
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /*.sqlite3
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,28 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.3
3
+ Exclude:
4
+ - activerecord-shard_for.gemspec
5
+ - Guardfile
6
+ - lib/activerecord/tasks/activerecord_shard_for.rake
7
+ DisplayCopNames: true
8
+
9
+ Style/FrozenStringLiteralComment:
10
+ Enabled: false
11
+
12
+ Style/Documentation:
13
+ Enabled: false
14
+
15
+ Metrics/LineLength:
16
+ Max: 128
17
+
18
+ Style/EmptyCaseCondition:
19
+ Enabled: false
20
+
21
+ Metrics/MethodLength:
22
+ Max: 15
23
+
24
+ Style/ParallelAssignment:
25
+ Enabled: false
26
+
27
+ Style/Alias:
28
+ EnforcedStyle: prefer_alias_method
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.3.0
4
+ before_install: gem install bundler -v 1.11.2
data/Appraisals ADDED
@@ -0,0 +1,26 @@
1
+ # vim: set ft=ruby
2
+
3
+ appraise 'ar-4.1.0' do
4
+ gem 'activerecord', '4.1.0'
5
+ end
6
+
7
+ appraise 'ar-4.1.7' do
8
+ gem 'activerecord', '4.1.7'
9
+ end
10
+
11
+ appraise 'ar-4.1.8' do
12
+ gem 'activerecord', '4.1.8'
13
+ end
14
+
15
+ appraise 'ar-4.2' do
16
+ gem 'activerecord', '~> 4.2'
17
+ end
18
+
19
+ appraise 'ar-5.0' do
20
+ gem 'activerecord', '~> 5.0.0'
21
+ end
22
+
23
+ appraise 'rails-edge' do
24
+ gem 'activerecord', github: 'rails/rails'
25
+ gem 'arel', github: 'rails/arel'
26
+ end
@@ -0,0 +1,49 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, and in the interest of
4
+ fostering an open and welcoming community, we pledge to respect all people who
5
+ contribute through reporting issues, posting feature requests, updating
6
+ documentation, submitting pull requests or patches, and other activities.
7
+
8
+ We are committed to making participation in this project a harassment-free
9
+ experience for everyone, regardless of level of experience, gender, gender
10
+ identity and expression, sexual orientation, disability, personal appearance,
11
+ body size, race, ethnicity, age, religion, or nationality.
12
+
13
+ Examples of unacceptable behavior by participants include:
14
+
15
+ * The use of sexualized language or imagery
16
+ * Personal attacks
17
+ * Trolling or insulting/derogatory comments
18
+ * Public or private harassment
19
+ * Publishing other's private information, such as physical or electronic
20
+ addresses, without explicit permission
21
+ * Other unethical or unprofessional conduct
22
+
23
+ Project maintainers have the right and responsibility to remove, edit, or
24
+ reject comments, commits, code, wiki edits, issues, and other contributions
25
+ that are not aligned to this Code of Conduct, or to ban temporarily or
26
+ permanently any contributor for other behaviors that they deem inappropriate,
27
+ threatening, offensive, or harmful.
28
+
29
+ By adopting this Code of Conduct, project maintainers commit themselves to
30
+ fairly and consistently applying these principles to every aspect of managing
31
+ this project. Project maintainers who do not follow or enforce the Code of
32
+ Conduct may be permanently removed from the project team.
33
+
34
+ This code of conduct applies both within project spaces and in public spaces
35
+ when an individual is representing the project or its community.
36
+
37
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
38
+ reported by contacting a project maintainer at yuemori@aiming-inc.com. All
39
+ complaints will be reviewed and investigated and will result in a response that
40
+ is deemed necessary and appropriate to the circumstances. Maintainers are
41
+ obligated to maintain confidentiality with regard to the reporter of an
42
+ incident.
43
+
44
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
45
+ version 1.3.0, available at
46
+ [http://contributor-covenant.org/version/1/3/0/][version]
47
+
48
+ [homepage]: http://contributor-covenant.org
49
+ [version]: http://contributor-covenant.org/version/1/3/0/
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ source 'https://rubygems.org'
3
+
4
+ # Specify your gem's dependencies in activerecord-shard_for.gemspec
5
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,21 @@
1
+ guard :rspec, cmd: 'bundle exec appraisal rspec' do
2
+ require 'guard/rspec/dsl'
3
+ dsl = Guard::RSpec::Dsl.new(self)
4
+
5
+ # Feel free to open issues for suggestions and improvements
6
+
7
+ # RSpec files
8
+ rspec = dsl.rspec
9
+ watch(rspec.spec_helper) { rspec.spec_dir }
10
+ watch(rspec.spec_support) { rspec.spec_dir }
11
+ watch(rspec.spec_files)
12
+
13
+ # Ruby files
14
+ ruby = dsl.ruby
15
+ dsl.watch_spec_files_for(ruby.lib_files)
16
+ end
17
+
18
+ guard :rubocop, cmd: 'bundle exec rubocop' do
19
+ watch(/.+\.rb$/)
20
+ watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) }
21
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 yuemori
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,296 @@
1
+ # ActiveRecord::ShardFor
2
+
3
+ This is Sharding Library for ActiveRecord, inspire and import codes from [mixed_gauge](https://github.com/taiki45/mixed_gauge) and [activerecord-sharding](https://github.com/hirocaster/activerecord-sharding) (Thanks!).
4
+
5
+ ## Concept
6
+
7
+ `activerecord-shard_for` have 3 concepts.
8
+
9
+ - simple
10
+ - small
11
+ - pluggable
12
+
13
+ ### Simple
14
+
15
+ This idea principal take over from `mixed_gauge`, and `activerecord-sharding`. `activerecord-shard_for` needs minimum and simply of settings.
16
+
17
+ ### Small
18
+
19
+ - Small Dependency: To facilitate version up (important!).
20
+ - Small code: Easy to read code when cause trouble.
21
+
22
+ ### pluggable
23
+
24
+ Default sharding rule is [modulo with hashed key](https://github.com/yuemori/activerecord-shard_for/blob/master/lib/activerecord/shard_for/hash_modulo_router.rb). But, `activerecord-shard_for` adopt pluggable structer. This rule can be easy to change!
25
+
26
+ ## Installation
27
+
28
+ Add this line to your application's Gemfile:
29
+
30
+ ```ruby
31
+ gem 'activerecord-shard_for'
32
+ ```
33
+
34
+ And then execute:
35
+
36
+ $ bundle
37
+
38
+ Or install it yourself as:
39
+
40
+ $ gem install activerecord-shard_for
41
+
42
+ ## Usage
43
+
44
+ Add additional database connection config to database.yml.
45
+
46
+ ```yaml
47
+ # database.yml
48
+ production_user_001:
49
+ adapter: mysql2
50
+ username: user_writable
51
+ host: db-user-001
52
+ production_user_002:
53
+ adapter: mysql2
54
+ username: user_writable
55
+ host: db-user-002
56
+ production_user_003:
57
+ adapter: mysql2
58
+ username: user_writable
59
+ host: db-user-003
60
+ production_user_004:
61
+ adapter: mysql2
62
+ username: user_writable
63
+ host: db-user-004
64
+ ```
65
+
66
+ Define cluster in initializers (e.g `initializers/active_record_shard_for.rb`)
67
+
68
+ ```ruby
69
+ ActiveRecord::ShardFor.configure do |config|
70
+ config.define_cluster(:user) do |cluster|
71
+ # unique identifier, connection name
72
+ cluster.register(0, :production_user_001)
73
+ cluster.register(1, :production_user_002)
74
+ cluster.register(2, :production_user_003)
75
+ cluster.register(3, :production_user_004)
76
+ end
77
+ end
78
+ ```
79
+
80
+ Include `ActiveRecord::ShardFor::Model` to your model class, specify cluster name and router name for the model, specify distkey which determines node to store.
81
+
82
+ ```ruby
83
+ class User < ActiveRecord::Base
84
+ include ActiveRecord::ShardFor::Model
85
+ use_cluster :user, :hash_modulo # hash_modulo is presented by this library.
86
+ def_distkey :email
87
+ end
88
+ ```
89
+
90
+ Use `.get` to retrive single record which is connected to proper database node. Use .put! to create new record to proper database node.
91
+
92
+ `.all_shards` returns each model class which is connected to proper database node. You can query with these models and aggregate result.
93
+
94
+ ```ruby
95
+ User.put!(email: 'alice@example.com', name: 'alice')
96
+
97
+ alice = User.get('alice@example.com')
98
+ alice.age = 1
99
+ alice.save!
100
+
101
+ User.all_shards.flat_map {|m| m.find_by(name: 'alice') }.compact
102
+ ```
103
+
104
+ When you want to execute queries in all nodes in parallel, use .all_shards_in_parallel. It returns `ActiveRecord::ShardFor::AllShardsInParallel` and it offers some collection actions which runs in parallel. It is aliased to .parallel.
105
+
106
+ ```ruby
107
+ User.all_shards_in_parallel.map(&count) #=> 1
108
+ User.parallel.flat_map {|m| m.where(age: 1) }.size #=> 1
109
+ ```
110
+
111
+
112
+ Sometimes you want to generates distkey value before validation. Since activerecord-shard_for generates sub class of your models, AR's callback is usesless for this usecase, so activerecord-shard_for offers its own callback method.
113
+
114
+ ```ruby
115
+ class AccessToken < ActiveRecord::Base
116
+ include ActiveRecord::ShardFor::Model
117
+ use_cluster :access_token
118
+ def_distkey :token
119
+
120
+ validates :token, presence: true
121
+
122
+ def self.generate_token
123
+ SecureRandom.uuid
124
+ end
125
+
126
+ before_put do |attributes|
127
+ unless attributes[:token] || attributes['token']
128
+ attributes[:token] = generate_token
129
+ end
130
+ end
131
+ end
132
+
133
+ access_token = AccessToken.put!
134
+ access_token.token #=> a generated token
135
+ ```
136
+
137
+ ## Sharding with Replication
138
+
139
+ activerecord-shard_for also supports replication.
140
+
141
+ In case you have 2 shards in cluster and each shard have read replica.
142
+
143
+ - db-user-101 --replicated--> db-user-102
144
+ - db-user-201 --replicated--> db-user-202
145
+
146
+ Your database connection configuration might be like this:
147
+
148
+ ```yaml
149
+ # database.yml
150
+ production_user_001:
151
+ adapter: mysql2
152
+ username: user_writable
153
+ host: db-user-101
154
+ production_user_002:
155
+ adapter: mysql2
156
+ username: user_writable
157
+ host: db-user-201
158
+ production_user_readonly_001:
159
+ adapter: mysql2
160
+ username: user_readonly
161
+ host: db-user-102
162
+ production_user_readonly_002:
163
+ adapter: mysql2
164
+ username: user_writable
165
+ host: db-user-202
166
+ ```
167
+
168
+ Your initializer for activerecord-shard_for might be like this:
169
+
170
+ ```ruby
171
+ ActiveRecord::ShardFor.configure do |config|
172
+ config.define_cluster(:user) do |cluster|
173
+ cluster.register(0, :production_user_001)
174
+ cluster.register(1, :production_user_002)
175
+ end
176
+
177
+ config.define_cluster(:user_readonly) do |cluster|
178
+ # give same key of master
179
+ cluster.register(0, :production_user_readonly_001)
180
+ cluster.register(1, :production_user_readonly_002)
181
+ end
182
+ end
183
+ ```
184
+
185
+ You can split read/write by defining AR model class for each connection:
186
+
187
+ ```ruby
188
+ class User < ActiveRecord::Base
189
+ include ActiveRecord::ShardFor::Model
190
+ use_cluster :user
191
+ def_distkey :email
192
+ end
193
+
194
+ class UserReadonly < ActiveRecord::Base
195
+ self.table_name = 'users'
196
+
197
+ include ActiveRecord::ShardFor::Model
198
+ use_cluster :user_readonly
199
+ def_distkey :email
200
+ end
201
+
202
+ User.put!(name: 'Alice', email: 'alice@example.com')
203
+ UserReadonly.get('alice@example.com')
204
+ ```
205
+
206
+ If you want to switch specific shard to another shard in another cluster, define mapping between each model:
207
+
208
+ ```ruby
209
+ class User < ActiveRecord::Base
210
+ include ActiveRecord::ShardFor::Model
211
+ use_cluster :user, :hash_modulo
212
+ def_distkey :email
213
+
214
+ replicates_with slave: :UserReadonly
215
+ end
216
+
217
+ class UserReadonly < ActiveRecord::Base
218
+ self.table_name = 'users'
219
+
220
+ include ActiveRecord::ShardFor::Model
221
+ use_cluster :user_readonly, :hash_modulo
222
+ def_distkey :email
223
+
224
+ replicates_with master: :User
225
+ end
226
+ ```
227
+
228
+ You can switch to another model which have connection to the shard by calling .switch:
229
+
230
+ ```ruby
231
+ UserReadonly.all_shards do |readonly|
232
+ target_ids = readonly.where(age: 0).pluck(:id)
233
+ readonly.switch(:master) do |writable|
234
+ writable.where(id: target_ids).delete_all
235
+ end
236
+ end
237
+ ```
238
+
239
+ ## Plugin of cluster router
240
+
241
+ If you need to advanced cluster routing, implement router class and register this.
242
+
243
+ Reference a interface to [HashModuloRouter](https://github.com/yuemori/activerecord-shard_for/blob/master/lib/activerecord/shard_for/hash_modulo_router.rb) and [ConnectionRouter](https://github.com/yuemori/activerecord-shard_for/blob/master/lib/activerecord/shard_for/connection_router.rb).
244
+
245
+ Example, simple modulo router:
246
+
247
+ ```ruby
248
+ class SimpleModuloRouter < ActiveRecord::ShardFor::ConnectionRouter
249
+ def route(key)
250
+ key.to_i % connection_count
251
+ end
252
+ end
253
+ ```
254
+
255
+ Your initializer for activerecord-shard_for might be like this:
256
+
257
+ ```ruby
258
+ ActiveRecord::ShardFor.configure do |config|
259
+ config.register_cluster_router(:modulo, SimpleModuloRouter)
260
+ end
261
+ ```
262
+
263
+ And specify router in your AR model.
264
+
265
+ ```ruby
266
+ class User < ActiveRecord::Base
267
+ include ActiveRecord::ShardFor::Model
268
+ use_cluster :user, :modulo
269
+ def_distkey :id
270
+
271
+ def self.generate_unique_id
272
+ # Implement to generate unique id
273
+ end
274
+
275
+ before_put do |attributes|
276
+ attributes[:id] = generate_unique_id unless attributes[:id]
277
+ end
278
+ end
279
+ ```
280
+
281
+ ## Contributing with ActiveRecord::ShardFor
282
+
283
+ Contributors are welcome! This is what you need to setup your Octopus development environment:
284
+
285
+ ```sh
286
+ $ git clone https://github.com/yuemori/activerecord-shard_for
287
+ $ cd activerecord-shard_for
288
+ $ bundle install
289
+ $ bundle exec rake appraisal:install
290
+ $ bundle exec rake spec
291
+ ```
292
+
293
+ ## License
294
+
295
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
296
+