mixed_gauge 0.1.1 → 0.1.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +80 -17
- data/lib/mixed_gauge/config.rb +26 -2
- data/lib/mixed_gauge/errors.rb +4 -0
- data/lib/mixed_gauge/model.rb +77 -7
- data/lib/mixed_gauge/routing.rb +1 -1
- data/lib/mixed_gauge/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: af67019109e77025f6dcc080900efb12622b9f7e
|
4
|
+
data.tar.gz: be1c25c01c401d06286e26e1075bbd29f5df5d6c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 70a434108a0fecee84f08f63d79a74a751c91396352dac7682be3031fbd80249a7dac288cdd2a137993411ff40b375697889d4c01a0409686bc4486421e88bf7
|
7
|
+
data.tar.gz: 0b415db64f93f88422fab59c290d504a65e546a2e4d49cb33bf75178f5f825a4e1fb4590ff199c25c6342407622412b700bfc01448c1d3f06400b8127d210028
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
## 0.1.2
|
2
|
+
- Enable to register before hook on `.put!`.
|
3
|
+
- Enable to define arbitrary methods to model class.
|
4
|
+
- Arbitrary hash function registeration.
|
5
|
+
- `.get!` to raise error when record not found.
|
6
|
+
|
1
7
|
## 0.1.1
|
2
8
|
- Fix NoMethodError bug on MixedGauge::Model.
|
3
9
|
- Improve tests, measure coverage.
|
data/README.md
CHANGED
@@ -3,22 +3,6 @@
|
|
3
3
|
|
4
4
|
An ActiveRecord extension for database sharding.
|
5
5
|
|
6
|
-
## Installation
|
7
|
-
|
8
|
-
Add this line to your application's Gemfile:
|
9
|
-
|
10
|
-
```ruby
|
11
|
-
gem 'mixed_gauge'
|
12
|
-
```
|
13
|
-
|
14
|
-
And then execute:
|
15
|
-
|
16
|
-
$ bundle
|
17
|
-
|
18
|
-
Or install it yourself as:
|
19
|
-
|
20
|
-
$ gem install mixed_gauge
|
21
|
-
|
22
6
|
## Usage
|
23
7
|
|
24
8
|
Add additional database connection config to `database.yml`.
|
@@ -81,9 +65,88 @@ alice = User.get('alice@example.com')
|
|
81
65
|
alice.age = 1
|
82
66
|
alice.save!
|
83
67
|
|
84
|
-
User.all_shards.flat_map {|m| m.
|
68
|
+
User.all_shards.flat_map {|m| m.find_by(name: 'alice') }.compact
|
85
69
|
```
|
86
70
|
|
71
|
+
When you want find by non-distkey, not recomended though, you can define finder
|
72
|
+
methods to model class.
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
class User < ActiveRecord::Base
|
76
|
+
include MixedGauge::Model
|
77
|
+
use_cluster :user
|
78
|
+
def_distkey :email
|
79
|
+
|
80
|
+
parent_methods do
|
81
|
+
def find_from_all_by_name(name)
|
82
|
+
all_shards.map {|m| m.find_by(name: name) }.compact.first
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
alice = User.find_from_all_by_name('Alice')
|
88
|
+
alice.age = 0
|
89
|
+
alice.save!
|
90
|
+
```
|
91
|
+
|
92
|
+
Sometimes you want to generates distkey value before validation. Since mixed_gauge
|
93
|
+
generates sub class of your models, AR's callback is not usesless for this usecase,
|
94
|
+
so mixed_gauge offers its own callback method.
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
class AccessToken < ActiveRecord::Base
|
98
|
+
include MixedGauge::Model
|
99
|
+
use_cluster :access_token
|
100
|
+
def_distkey :token
|
101
|
+
|
102
|
+
validates :token, presence: true
|
103
|
+
|
104
|
+
def self.generate_token
|
105
|
+
SecureRandom.uuid
|
106
|
+
end
|
107
|
+
|
108
|
+
before_put do |attributes|
|
109
|
+
unless attributes[:token] || attributes['token']
|
110
|
+
attributes[:token] = generate_token
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
access_token = AccessToken.put!
|
116
|
+
access_token.token #=> a generated token
|
117
|
+
```
|
118
|
+
|
119
|
+
## Advanced configuration
|
120
|
+
### Hash fucntion
|
121
|
+
Register arbitrary hash function. Hash function must be a proc and
|
122
|
+
must return integer.
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
# gem install fnv
|
126
|
+
require "fnv"
|
127
|
+
Mixedgauge.configure do |config|
|
128
|
+
config.register_hash_function do |key|
|
129
|
+
FNV.new.fnv1a_64(key)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
## Installation
|
135
|
+
|
136
|
+
Add this line to your application's Gemfile:
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
gem 'mixed_gauge'
|
140
|
+
```
|
141
|
+
|
142
|
+
And then execute:
|
143
|
+
|
144
|
+
$ bundle
|
145
|
+
|
146
|
+
Or install it yourself as:
|
147
|
+
|
148
|
+
$ gem install mixed_gauge
|
149
|
+
|
87
150
|
## Development
|
88
151
|
|
89
152
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/lib/mixed_gauge/config.rb
CHANGED
@@ -1,14 +1,22 @@
|
|
1
1
|
module MixedGauge
|
2
2
|
class Config
|
3
|
+
DEFAULT_HASH_FUNCTION = -> (key) { Digest::MD5.hexdigest(key).to_i(16) }
|
4
|
+
|
5
|
+
attr_reader :hash_proc
|
6
|
+
|
3
7
|
def initialize
|
4
8
|
@cluster_configs = {}
|
9
|
+
@hash_proc = DEFAULT_HASH_FUNCTION
|
5
10
|
end
|
6
11
|
|
12
|
+
# Define config for specific cluster.
|
7
13
|
# @param [Symbol] cluster_name
|
8
14
|
# @return [nil]
|
9
15
|
# @example
|
10
|
-
# config.define_cluster(:user) do |
|
11
|
-
#
|
16
|
+
# config.define_cluster(:user) do |cluster|
|
17
|
+
# cluster.define_slots(1..1048576)
|
18
|
+
# cluster.register(1..524288, :production_user_001)
|
19
|
+
# cluster.register(524289..1048576, :production_user_002)
|
12
20
|
# end
|
13
21
|
def define_cluster(cluster_name, &block)
|
14
22
|
cluster_config = ClusterConfig.new(cluster_name)
|
@@ -22,5 +30,21 @@ module MixedGauge
|
|
22
30
|
def fetch_cluster_config(cluster_name)
|
23
31
|
@cluster_configs.fetch(cluster_name)
|
24
32
|
end
|
33
|
+
|
34
|
+
# Register arbitrary hash function. Hash function must be a proc and
|
35
|
+
# must return integer.
|
36
|
+
# @example
|
37
|
+
# # gem install fnv
|
38
|
+
# require "fnv"
|
39
|
+
# Mixedgauge.configure do |config|
|
40
|
+
# config.register_hash_function do |key|
|
41
|
+
# FNV.new.fnv1a_64(key)
|
42
|
+
# end
|
43
|
+
# end
|
44
|
+
def register_hash_function(&block)
|
45
|
+
raise ArgumentError if block.arity != 1
|
46
|
+
raise ArgumentError unless block.call('test value').is_a? Integer
|
47
|
+
@hash_proc = block
|
48
|
+
end
|
25
49
|
end
|
26
50
|
end
|
data/lib/mixed_gauge/errors.rb
CHANGED
@@ -5,4 +5,8 @@ module MixedGauge
|
|
5
5
|
# Raised when try to put new record without distkey attribute.
|
6
6
|
class MissingDistkeyAttribute < Error
|
7
7
|
end
|
8
|
+
|
9
|
+
# Inherit from AR::RecordNotFound to enable to handle as AR's one.
|
10
|
+
class RecordNotFound < ActiveRecord::RecordNotFound
|
11
|
+
end
|
8
12
|
end
|
data/lib/mixed_gauge/model.rb
CHANGED
@@ -25,6 +25,7 @@ module MixedGauge
|
|
25
25
|
end
|
26
26
|
|
27
27
|
module ClassMethods
|
28
|
+
# The cluster config must be defined before `use_cluster`.
|
28
29
|
# @param [Symbol] A cluster name which is set by MixedGauge.configure
|
29
30
|
def use_cluster(name)
|
30
31
|
config = MixedGauge.config.fetch_cluster_config(name)
|
@@ -33,20 +34,22 @@ module MixedGauge
|
|
33
34
|
self.abstract_class = true
|
34
35
|
end
|
35
36
|
|
37
|
+
# Distkey is a column. mixed_gauge hashes that value and determine which
|
38
|
+
# shard to store.
|
36
39
|
# @param [Symbol] column
|
37
40
|
def def_distkey(column)
|
38
41
|
self.distkey = column.to_sym
|
39
42
|
end
|
40
43
|
|
41
|
-
#
|
42
|
-
#
|
43
|
-
|
44
|
-
shard_for(key.to_s).find_by(distkey => key)
|
45
|
-
end
|
46
|
-
|
44
|
+
# Create new record with given attributes in proper shard for given key.
|
45
|
+
# When distkey value is empty, raises MixedGauge::MissingDistkeyAttribute
|
46
|
+
# error.
|
47
47
|
# @param [Hash] attributes
|
48
48
|
# @return [ActiveRecord::Base] A sub class instance of included model
|
49
|
+
# @raise [MixedGauge::MissingDistkeyAttribute]
|
49
50
|
def put!(attributes)
|
51
|
+
@before_put_callback.call(attributes) if @before_put_callback
|
52
|
+
|
50
53
|
if key = attributes[distkey] || attributes[distkey.to_s]
|
51
54
|
shard_for(key).create!(attributes)
|
52
55
|
else
|
@@ -54,17 +57,84 @@ module MixedGauge
|
|
54
57
|
end
|
55
58
|
end
|
56
59
|
|
57
|
-
#
|
60
|
+
# Returns nil when not found. Except that, is same as `.get!`.
|
61
|
+
# @param [String] key
|
62
|
+
# @return [ActiveRecord::Base, nil] A sub model instance of included model
|
63
|
+
def get(key)
|
64
|
+
raise 'key must be a String' unless key.is_a?(String)
|
65
|
+
shard_for(key.to_s).find_by(distkey => key)
|
66
|
+
end
|
67
|
+
|
68
|
+
# `.get!` raises MixedGauge::RecordNotFound which is child class of
|
69
|
+
# `ActiveRecord::RecordNotFound` so you can rescue that exception as same
|
70
|
+
# as AR's RecordNotFound.
|
71
|
+
# @param [String] key
|
72
|
+
# @return [ActiveRecord::Base] A sub model instance of included model
|
73
|
+
# @raise [MixedGauge::RecordNotFound]
|
74
|
+
def get!(key)
|
75
|
+
get(key) or raise MixedGauge::RecordNotFound
|
76
|
+
end
|
77
|
+
|
78
|
+
# Register hook to assign auto-generated distkey or something.
|
79
|
+
# Sometimes you want to generates distkey value before validation. Since
|
80
|
+
# mixed_gauge generates sub class of your models, AR's callback is not
|
81
|
+
# usesless for this usecase, so mixed_gauge offers its own callback method.
|
82
|
+
# @example
|
83
|
+
# class User
|
84
|
+
# include MixedGauge::Model
|
85
|
+
# use_cluster :user
|
86
|
+
# def_distkey :name
|
87
|
+
# before_put do |attributes|
|
88
|
+
# attributes[:name] = generate_name unless attributes[:name]
|
89
|
+
# end
|
90
|
+
# end
|
91
|
+
def before_put(&block)
|
92
|
+
@before_put_callback = block
|
93
|
+
end
|
94
|
+
|
95
|
+
# Returns a generated sub class of this model which is connected proper
|
96
|
+
# shard for given key.
|
97
|
+
# @param [String] key A value of distkey
|
58
98
|
# @return [Class] A sub model for this distkey value
|
59
99
|
def shard_for(key)
|
60
100
|
connection_name = cluster_routing.route(key.to_s)
|
61
101
|
sub_model_repository.fetch(connection_name)
|
62
102
|
end
|
63
103
|
|
104
|
+
# Returns all generated sub class of this model. Useful to query to
|
105
|
+
# all shards.
|
64
106
|
# @return [Array<Class>] An array of sub models
|
107
|
+
# @example
|
108
|
+
# User.all_shards.flat_map {|m| m.find_by(name: 'alice') }.compact
|
65
109
|
def all_shards
|
66
110
|
sub_model_repository.all
|
67
111
|
end
|
112
|
+
|
113
|
+
# Define utility methods which uses all shards or specific shard.
|
114
|
+
# These methods can be called from included model class.
|
115
|
+
# @example
|
116
|
+
# class User
|
117
|
+
# include MixedGauge::Model
|
118
|
+
# use_cluster :user
|
119
|
+
# def_distkey :name
|
120
|
+
# parent_methods do
|
121
|
+
# def all_count
|
122
|
+
# all_shards.map {|m| m.count }.reduce(&:+)
|
123
|
+
# end
|
124
|
+
#
|
125
|
+
# def find_from_all_by(condition)
|
126
|
+
# all_shards.flat_map {|m m.find_by(condition) }.compact.first
|
127
|
+
# end
|
128
|
+
# end
|
129
|
+
# end
|
130
|
+
#
|
131
|
+
# User.put!(email: 'a@m.com', name: 'a')
|
132
|
+
# User.put!(email: 'b@m.com', name: 'b')
|
133
|
+
# User.all_count #=> 2
|
134
|
+
# User.find_from_all_by(name: 'b') #=> User b
|
135
|
+
def parent_methods(&block)
|
136
|
+
instance_eval(&block)
|
137
|
+
end
|
68
138
|
end
|
69
139
|
end
|
70
140
|
end
|
data/lib/mixed_gauge/routing.rb
CHANGED
data/lib/mixed_gauge/version.rb
CHANGED