redis_ring_client 0.0.1
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.
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +30 -0
- data/MIT-LICENSE.txt +20 -0
- data/Rakefile +11 -0
- data/lib/redis_ring/client/operation_definitions.rb +147 -0
- data/lib/redis_ring/client/ring_meta_data.rb +72 -0
- data/lib/redis_ring/client/ring_proxy.rb +188 -0
- data/lib/redis_ring/client/shard_connection_pool.rb +33 -0
- data/lib/redis_ring/client/sharder.rb +30 -0
- data/lib/redis_ring/client/version.rb +5 -0
- data/lib/redis_ring/client.rb +10 -0
- data/redis_ring_client.gemspec +27 -0
- data/spec/fakes/fake_ring_meta_data.rb +15 -0
- data/spec/redis_ring/client/ring_meta_data_spec.rb +60 -0
- data/spec/redis_ring/client/ring_proxy_spec.rb +158 -0
- data/spec/redis_ring/client/shard_connection_pool_spec.rb +27 -0
- data/spec/redis_ring/client/sharder_spec.rb +40 -0
- data/spec/spec_helper.rb +10 -0
- metadata +134 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
redis_ring_client (0.0.1)
|
5
|
+
json
|
6
|
+
redis
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: http://rubygems.org/
|
10
|
+
specs:
|
11
|
+
diff-lcs (1.1.2)
|
12
|
+
json (1.5.1)
|
13
|
+
mocha (0.9.12)
|
14
|
+
redis (2.1.1)
|
15
|
+
rspec (2.5.0)
|
16
|
+
rspec-core (~> 2.5.0)
|
17
|
+
rspec-expectations (~> 2.5.0)
|
18
|
+
rspec-mocks (~> 2.5.0)
|
19
|
+
rspec-core (2.5.1)
|
20
|
+
rspec-expectations (2.5.0)
|
21
|
+
diff-lcs (~> 1.1.2)
|
22
|
+
rspec-mocks (2.5.0)
|
23
|
+
|
24
|
+
PLATFORMS
|
25
|
+
ruby
|
26
|
+
|
27
|
+
DEPENDENCIES
|
28
|
+
mocha
|
29
|
+
redis_ring_client!
|
30
|
+
rspec
|
data/MIT-LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Adam Pohorecki, http://adam.pohorecki.pl/
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
require 'rspec/core'
|
5
|
+
require 'rspec/core/rake_task'
|
6
|
+
|
7
|
+
desc "Run all specs"
|
8
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
9
|
+
t.pattern = "./spec/**/*_spec.rb"
|
10
|
+
t.rspec_opts = ["--profile --color"]
|
11
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
module RedisRing
|
2
|
+
module Client
|
3
|
+
|
4
|
+
class UnsupportedOperationError < StandardError; end
|
5
|
+
class MultiShardOperationError < UnsupportedOperationError; end
|
6
|
+
|
7
|
+
module OperationDefinitions
|
8
|
+
|
9
|
+
def self.included(klass)
|
10
|
+
klass.send(:include, InstanceMethods)
|
11
|
+
klass.send(:include, GatherOperations)
|
12
|
+
klass.extend(ClassMethods)
|
13
|
+
end
|
14
|
+
|
15
|
+
module GatherOperations
|
16
|
+
|
17
|
+
def last_result(array)
|
18
|
+
return array.last
|
19
|
+
end
|
20
|
+
|
21
|
+
def sum(array)
|
22
|
+
return array.reduce(:+)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
module InstanceMethods
|
28
|
+
|
29
|
+
def single_key_operation(name, first_arg, *rest)
|
30
|
+
connection = connection_for_key(first_arg)
|
31
|
+
return connection.send(name, first_arg, *rest)
|
32
|
+
end
|
33
|
+
|
34
|
+
def scather_gather_operation(name, gather, *args, &block)
|
35
|
+
results = []
|
36
|
+
each_connection do |conn|
|
37
|
+
results << conn.send(name, *args, &block)
|
38
|
+
end
|
39
|
+
return send(gather, results)
|
40
|
+
end
|
41
|
+
|
42
|
+
def unsupported_operation(name)
|
43
|
+
raise UnsupportedOperationError.new("Operation #{name} is not supported by RedisRing!")
|
44
|
+
end
|
45
|
+
|
46
|
+
def random_shard_operation(name, *args, &block)
|
47
|
+
shard_no = rand(ring_meta_data.ring_size)
|
48
|
+
return connection_pool.connection(shard_no).send(name, *args, &block)
|
49
|
+
end
|
50
|
+
|
51
|
+
def single_connection_operation(name, keys, *args, &block)
|
52
|
+
shard_numbers = keys.map { |key| sharder.shard_for_key(key) }
|
53
|
+
unless shard_numbers.uniq.size == 1
|
54
|
+
raise MultiShardOperationError.new("Multi-shard atomic operations are not allowed. Try using {shard_secifier} suffix if you really need them. Operation: #{name}, Keys: #{keys.join(', ')}")
|
55
|
+
end
|
56
|
+
return connection_for_key(keys.first).send(name, *args, &block)
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
module ClassMethods
|
62
|
+
|
63
|
+
def single_key_operation(name)
|
64
|
+
self.class_eval <<-RUBY
|
65
|
+
|
66
|
+
def #{name}(key, *args)
|
67
|
+
return single_key_operation(:#{name}, key, *args)
|
68
|
+
end
|
69
|
+
|
70
|
+
RUBY
|
71
|
+
end
|
72
|
+
|
73
|
+
def scather_gather_operation(name, gather_function)
|
74
|
+
self.class_eval <<-RUBY
|
75
|
+
|
76
|
+
def #{name}(*args, &block)
|
77
|
+
return scather_gather_operation(:#{name}, :#{gather_function}, *args, &block)
|
78
|
+
end
|
79
|
+
|
80
|
+
RUBY
|
81
|
+
end
|
82
|
+
|
83
|
+
def unsupported_operation(name)
|
84
|
+
self.class_eval <<-RUBY
|
85
|
+
|
86
|
+
def #{name}(*args)
|
87
|
+
unsupported_operation(:#{name})
|
88
|
+
end
|
89
|
+
|
90
|
+
RUBY
|
91
|
+
end
|
92
|
+
|
93
|
+
def random_shard_operation(name)
|
94
|
+
self.class_eval <<-RUBY
|
95
|
+
|
96
|
+
def #{name}(*args, &block)
|
97
|
+
random_shard_operation(:#{name}, *args, &block)
|
98
|
+
end
|
99
|
+
|
100
|
+
RUBY
|
101
|
+
end
|
102
|
+
|
103
|
+
def multi_key_operation(name)
|
104
|
+
self.class_eval <<-RUBY
|
105
|
+
|
106
|
+
def #{name}(*keys, &block)
|
107
|
+
return single_connection_operation(:#{name}, keys, *keys, &block)
|
108
|
+
end
|
109
|
+
|
110
|
+
RUBY
|
111
|
+
end
|
112
|
+
|
113
|
+
def mapped_set_operation(name)
|
114
|
+
self.class_eval <<-RUBY
|
115
|
+
|
116
|
+
def #{name}(hash, &block)
|
117
|
+
return single_connection_operation(:#{name}, hash.keys, hash, &block)
|
118
|
+
end
|
119
|
+
|
120
|
+
RUBY
|
121
|
+
end
|
122
|
+
|
123
|
+
def regular_set_operation(name)
|
124
|
+
self.class_eval <<-RUBY
|
125
|
+
|
126
|
+
def #{name}(*keys_and_values, &block)
|
127
|
+
return single_connection_operation(:#{name}, Hash[*keys_and_values].keys, *keys_and_values, &block)
|
128
|
+
end
|
129
|
+
|
130
|
+
RUBY
|
131
|
+
end
|
132
|
+
|
133
|
+
def multi_zstore_operation(name)
|
134
|
+
self.class_eval <<-RUBY
|
135
|
+
|
136
|
+
def #{name}(destination, keys, options = {})
|
137
|
+
return single_connection_operation(:#{name}, [destination] + keys, destination, keys, options)
|
138
|
+
end
|
139
|
+
|
140
|
+
RUBY
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module RedisRing
|
2
|
+
module Client
|
3
|
+
|
4
|
+
class UnknownShardError < StandardError; end
|
5
|
+
|
6
|
+
class RingMetaData
|
7
|
+
|
8
|
+
attr_reader :host, :port
|
9
|
+
|
10
|
+
def initialize(host, port)
|
11
|
+
@host = host
|
12
|
+
@port = port
|
13
|
+
@loaded = false
|
14
|
+
end
|
15
|
+
|
16
|
+
def reload!
|
17
|
+
json = get_shards_json_string
|
18
|
+
hash = JSON.parse(json)
|
19
|
+
|
20
|
+
@ring_size = hash['count']
|
21
|
+
@shards = (0...@ring_size).map{|n| ShardMetaData.from_json(hash['shards'][n.to_s])}
|
22
|
+
|
23
|
+
@loaded = true
|
24
|
+
end
|
25
|
+
|
26
|
+
def ring_size
|
27
|
+
reload! if should_reload?
|
28
|
+
|
29
|
+
return @ring_size
|
30
|
+
end
|
31
|
+
|
32
|
+
def shard(shard_number)
|
33
|
+
reload! if should_reload?
|
34
|
+
|
35
|
+
unless shard_number >= 0 && shard_number < ring_size
|
36
|
+
raise UnknownShardError.new("Shard number invalid: #{shard_number}. Ring size: #{ring_size}")
|
37
|
+
end
|
38
|
+
|
39
|
+
return @shards[shard_number]
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
|
44
|
+
def should_reload?
|
45
|
+
!@loaded
|
46
|
+
end
|
47
|
+
|
48
|
+
def get_shards_json_string
|
49
|
+
Net::HTTP.get(host, '/shards', port)
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
class ShardMetaData
|
55
|
+
|
56
|
+
attr_reader :host, :port, :status
|
57
|
+
|
58
|
+
def initialize(host, port, status)
|
59
|
+
@host = host
|
60
|
+
@port = port
|
61
|
+
@status = status
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.from_json(hash)
|
65
|
+
new(hash['host'], hash['port'].to_i, hash['status'].to_sym)
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
@@ -0,0 +1,188 @@
|
|
1
|
+
module RedisRing
|
2
|
+
module Client
|
3
|
+
|
4
|
+
class RingProxy
|
5
|
+
|
6
|
+
include OperationDefinitions
|
7
|
+
|
8
|
+
single_key_operation :[]
|
9
|
+
single_key_operation :[]=
|
10
|
+
single_key_operation :append
|
11
|
+
single_key_operation :decr
|
12
|
+
single_key_operation :decrby
|
13
|
+
single_key_operation :exists
|
14
|
+
single_key_operation :expire
|
15
|
+
single_key_operation :expireat
|
16
|
+
single_key_operation :get
|
17
|
+
single_key_operation :getset
|
18
|
+
single_key_operation :hdel
|
19
|
+
single_key_operation :hexists
|
20
|
+
single_key_operation :hget
|
21
|
+
single_key_operation :hgetall
|
22
|
+
single_key_operation :hincrby
|
23
|
+
single_key_operation :hkeys
|
24
|
+
single_key_operation :hlen
|
25
|
+
single_key_operation :hmget
|
26
|
+
single_key_operation :hmset
|
27
|
+
single_key_operation :hset
|
28
|
+
single_key_operation :hsetnx
|
29
|
+
single_key_operation :hvals
|
30
|
+
single_key_operation :incr
|
31
|
+
single_key_operation :incrby
|
32
|
+
single_key_operation :lindex
|
33
|
+
single_key_operation :linsert
|
34
|
+
single_key_operation :llen
|
35
|
+
single_key_operation :lpop
|
36
|
+
single_key_operation :lpush
|
37
|
+
single_key_operation :lpushx
|
38
|
+
single_key_operation :lrange
|
39
|
+
single_key_operation :lrem
|
40
|
+
single_key_operation :lset
|
41
|
+
single_key_operation :ltrim
|
42
|
+
single_key_operation :mapped_hmget
|
43
|
+
single_key_operation :mapped_hmset
|
44
|
+
single_key_operation :move
|
45
|
+
single_key_operation :persist
|
46
|
+
single_key_operation :publish
|
47
|
+
single_key_operation :rpop
|
48
|
+
single_key_operation :rpush
|
49
|
+
single_key_operation :rpushx
|
50
|
+
single_key_operation :sadd
|
51
|
+
single_key_operation :scard
|
52
|
+
single_key_operation :set
|
53
|
+
single_key_operation :setex
|
54
|
+
single_key_operation :setnx
|
55
|
+
single_key_operation :sismember
|
56
|
+
single_key_operation :smembers
|
57
|
+
single_key_operation :sort
|
58
|
+
single_key_operation :spop
|
59
|
+
single_key_operation :srandmember
|
60
|
+
single_key_operation :srem
|
61
|
+
single_key_operation :strlen
|
62
|
+
single_key_operation :substr
|
63
|
+
single_key_operation :ttl
|
64
|
+
single_key_operation :type
|
65
|
+
single_key_operation :zadd
|
66
|
+
single_key_operation :zcard
|
67
|
+
single_key_operation :zcount
|
68
|
+
single_key_operation :zincrby
|
69
|
+
single_key_operation :zrange
|
70
|
+
single_key_operation :zrangebyscore
|
71
|
+
single_key_operation :zrank
|
72
|
+
single_key_operation :zrem
|
73
|
+
single_key_operation :zremrangebyrank
|
74
|
+
single_key_operation :zremrangebyscore
|
75
|
+
single_key_operation :zrevrange
|
76
|
+
single_key_operation :zrevrank
|
77
|
+
single_key_operation :zscore
|
78
|
+
|
79
|
+
scather_gather_operation :auth, :last_result
|
80
|
+
scather_gather_operation :bgrewriteaof, :last_result
|
81
|
+
scather_gather_operation :bgsave, :last_result
|
82
|
+
scather_gather_operation :config, :last_result
|
83
|
+
scather_gather_operation :dbsize, :sum
|
84
|
+
scather_gather_operation :flushall, :last_result
|
85
|
+
scather_gather_operation :flushdb, :last_result
|
86
|
+
scather_gather_operation :keys, :sum
|
87
|
+
scather_gather_operation :quit, :last_result
|
88
|
+
scather_gather_operation :save, :last_result
|
89
|
+
scather_gather_operation :select, :last_result
|
90
|
+
scather_gather_operation :shutdown, :last_result
|
91
|
+
|
92
|
+
# it might be useful to combine those, but it would break the interface
|
93
|
+
unsupported_operation :info
|
94
|
+
|
95
|
+
# could be used in a single server, but it's complicated
|
96
|
+
# maybe a TODO for the future
|
97
|
+
unsupported_operation :multi
|
98
|
+
unsupported_operation :watch
|
99
|
+
unsupported_operation :unwatch
|
100
|
+
unsupported_operation :discard
|
101
|
+
unsupported_operation :exec
|
102
|
+
unsupported_operation :pipelined
|
103
|
+
|
104
|
+
# there's no good way to scather_gather this
|
105
|
+
unsupported_operation :monitor
|
106
|
+
|
107
|
+
# maybe max or min from the shards?
|
108
|
+
unsupported_operation :lastsave
|
109
|
+
|
110
|
+
unsupported_operation :debug
|
111
|
+
|
112
|
+
# no way to determine which shards they fall into
|
113
|
+
unsupported_operation :psubscribe
|
114
|
+
unsupported_operation :punsubscribe
|
115
|
+
unsupported_operation :subscribed?
|
116
|
+
|
117
|
+
unsupported_operation :sync
|
118
|
+
unsupported_operation :slaveof
|
119
|
+
|
120
|
+
random_shard_operation :echo
|
121
|
+
random_shard_operation :ping
|
122
|
+
random_shard_operation :randomkey
|
123
|
+
|
124
|
+
multi_key_operation :blpop
|
125
|
+
multi_key_operation :brpop
|
126
|
+
multi_key_operation :del
|
127
|
+
multi_key_operation :mapped_mget
|
128
|
+
multi_key_operation :mget
|
129
|
+
multi_key_operation :rename
|
130
|
+
multi_key_operation :renamenx
|
131
|
+
multi_key_operation :rpoplpush
|
132
|
+
multi_key_operation :sdiff
|
133
|
+
multi_key_operation :sdiffstore
|
134
|
+
multi_key_operation :sinter
|
135
|
+
multi_key_operation :sinterstore
|
136
|
+
multi_key_operation :subscribe
|
137
|
+
multi_key_operation :sunion
|
138
|
+
multi_key_operation :sunionstore
|
139
|
+
multi_key_operation :unsubscribe
|
140
|
+
|
141
|
+
mapped_set_operation :mapped_mset
|
142
|
+
mapped_set_operation :mapped_msetnx
|
143
|
+
regular_set_operation :mset
|
144
|
+
regular_set_operation :msetnx
|
145
|
+
|
146
|
+
multi_zstore_operation :zinterstore
|
147
|
+
multi_zstore_operation :zunionstore
|
148
|
+
|
149
|
+
def smove(source, destination, member)
|
150
|
+
return single_connection_operation(:smove, [source, destination], source, destination, member)
|
151
|
+
end
|
152
|
+
|
153
|
+
def initialize(opts = {})
|
154
|
+
@host = opts[:host] || 'localhost'
|
155
|
+
@port = opts[:port] || 6400
|
156
|
+
@db = opts[:db] || 0
|
157
|
+
@password = opts[:password]
|
158
|
+
end
|
159
|
+
|
160
|
+
def connection_for_key(key)
|
161
|
+
shard = sharder.shard_for_key(key)
|
162
|
+
return connection_pool.connection(shard)
|
163
|
+
end
|
164
|
+
|
165
|
+
def each_connection(&block)
|
166
|
+
ring_meta_data.ring_size.times do |shard_no|
|
167
|
+
block.call(connection_pool.connection(shard_no))
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
protected
|
172
|
+
|
173
|
+
def ring_meta_data
|
174
|
+
@ring_meta_data ||= RingMetaData.new(@host, @port)
|
175
|
+
end
|
176
|
+
|
177
|
+
def sharder
|
178
|
+
@sharder ||= Sharder.new(ring_meta_data)
|
179
|
+
end
|
180
|
+
|
181
|
+
def connection_pool
|
182
|
+
@connection_pool = ShardConnectionPool.new(ring_meta_data, @password, @db)
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module RedisRing
|
2
|
+
module Client
|
3
|
+
|
4
|
+
class ShardConnectionPool
|
5
|
+
|
6
|
+
attr_reader :metadata, :password, :db
|
7
|
+
|
8
|
+
def initialize(metadata, password, db)
|
9
|
+
@metadata = metadata
|
10
|
+
@password = password
|
11
|
+
@db = db
|
12
|
+
@connections = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def connection(shard_number)
|
16
|
+
@connections[shard_number] ||= new_connection_to_shard(shard_number)
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def new_connection_to_shard(shard_number)
|
22
|
+
shard_metadata = metadata.shard(shard_number)
|
23
|
+
new_connection(shard_metadata.host, shard_metadata.port, db, password)
|
24
|
+
end
|
25
|
+
|
26
|
+
def new_connection(host, port, db, password)
|
27
|
+
Redis.new(:host => host, :port => port, :db => db, :password => password)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module RedisRing
|
2
|
+
module Client
|
3
|
+
|
4
|
+
class Sharder
|
5
|
+
|
6
|
+
attr_reader :metadata
|
7
|
+
|
8
|
+
def initialize(metadata)
|
9
|
+
@metadata = metadata
|
10
|
+
end
|
11
|
+
|
12
|
+
def shard_for_key(key)
|
13
|
+
crc = Zlib.crc32(hashable_part(key.to_s))
|
14
|
+
return crc % metadata.ring_size
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def hashable_part(key)
|
20
|
+
if key =~ /{([^}]*)}$/
|
21
|
+
return Regexp.last_match(1)
|
22
|
+
else
|
23
|
+
return key
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'zlib'
|
2
|
+
|
3
|
+
require 'redis'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
require 'redis_ring/client/operation_definitions'
|
7
|
+
require 'redis_ring/client/ring_proxy'
|
8
|
+
require 'redis_ring/client/sharder'
|
9
|
+
require 'redis_ring/client/ring_meta_data'
|
10
|
+
require 'redis_ring/client/shard_connection_pool'
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "redis_ring/client/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "redis_ring_client"
|
7
|
+
s.version = RedisRing::Client::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Adam Pohorecki"]
|
10
|
+
s.email = ["adam@pohorecki.pl"]
|
11
|
+
s.homepage = "http://github.com/psyho/redis_ring_client"
|
12
|
+
s.summary = %q{Client for RedisRing}
|
13
|
+
s.description = %q{The client counterpart to the RedisRing gem.}
|
14
|
+
|
15
|
+
s.rubyforge_project = "redis_ring_client"
|
16
|
+
|
17
|
+
s.add_dependency 'redis'
|
18
|
+
s.add_dependency 'json'
|
19
|
+
|
20
|
+
s.add_development_dependency 'rspec'
|
21
|
+
s.add_development_dependency 'mocha'
|
22
|
+
|
23
|
+
s.files = `git ls-files`.split("\n")
|
24
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
25
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
26
|
+
s.require_paths = ["lib"]
|
27
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require File.expand_path('../../../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe RedisRing::Client::RingMetaData do
|
4
|
+
|
5
|
+
def sample_shards_hash
|
6
|
+
{
|
7
|
+
:count => 10,
|
8
|
+
:shards => {
|
9
|
+
0 => {:host => '192.168.1.1', :port => 6401, :status => :running},
|
10
|
+
1 => {:host => '192.168.1.1', :port => 6402, :status => :running},
|
11
|
+
2 => {:host => '192.168.1.1', :port => 6403, :status => :running},
|
12
|
+
3 => {:host => '192.168.1.1', :port => 6404, :status => :running},
|
13
|
+
4 => {:host => '192.168.1.1', :port => 6405, :status => :running},
|
14
|
+
5 => {:host => '192.168.1.1', :port => 6406, :status => :running},
|
15
|
+
6 => {:host => '192.168.1.1', :port => 6407, :status => :running},
|
16
|
+
7 => {:host => '192.168.1.1', :port => 6408, :status => :running},
|
17
|
+
8 => {:host => '192.168.1.1', :port => 6409, :status => :running},
|
18
|
+
9 => {:host => '192.168.1.1', :port => 6410, :status => :running}
|
19
|
+
}
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def sample_shard_json
|
24
|
+
sample_shards_hash.to_json
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should download json lazily" do
|
28
|
+
@metadata = RedisRing::Client::RingMetaData.new('host', 666)
|
29
|
+
|
30
|
+
Net::HTTP.expects(:get).with('host', '/shards', 666).returns(sample_shard_json)
|
31
|
+
|
32
|
+
@metadata.ring_size.should == 10
|
33
|
+
end
|
34
|
+
|
35
|
+
context "with sample shards json" do
|
36
|
+
before(:each) do
|
37
|
+
Net::HTTP.stubs(:get => sample_shard_json)
|
38
|
+
|
39
|
+
@metadata = RedisRing::Client::RingMetaData.new('host', 666)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should have ring_size of 10" do
|
43
|
+
@metadata.ring_size.should == 10
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should have 10 shards" do
|
47
|
+
10.times do |n|
|
48
|
+
@metadata.shard(n).host.should == '192.168.1.1'
|
49
|
+
@metadata.shard(n).port.should == 6401 + n
|
50
|
+
@metadata.shard(n).status.should == :running
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should raise an exception when trying to acces unexisting shard metadata" do
|
55
|
+
lambda {
|
56
|
+
@metadata.shard(10)
|
57
|
+
}.should raise_exception(RedisRing::Client::UnknownShardError)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
require File.expand_path('../../../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe RedisRing::Client::RingProxy do
|
4
|
+
|
5
|
+
def readable_params(params)
|
6
|
+
translated = params.map do |type, name|
|
7
|
+
case type
|
8
|
+
when :req then name.to_s
|
9
|
+
when :rest then "*#{name}"
|
10
|
+
when :block then "&#{name}"
|
11
|
+
when :opt then "#{name} = some_value"
|
12
|
+
else
|
13
|
+
raise "Unknown parameter type #{type}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
return translated.join(", ")
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should have the same public interface as Redis" do
|
21
|
+
difference = Redis.public_instance_methods - RedisRing::Client::RingProxy.public_instance_methods
|
22
|
+
|
23
|
+
ignored = [:client, :id, :method_missing]
|
24
|
+
|
25
|
+
difference -= ignored
|
26
|
+
|
27
|
+
unless difference == []
|
28
|
+
puts "#{difference.size} missing methods:"
|
29
|
+
|
30
|
+
difference.sort.each do |method_name|
|
31
|
+
puts "#{method_name}(#{readable_params(Redis.instance_method(method_name).parameters)})"
|
32
|
+
end
|
33
|
+
|
34
|
+
fail("Not all methods implemented")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context "with real RedisRing" do
|
39
|
+
before(:each) do
|
40
|
+
@proxy = RedisRing::Client::RingProxy.new
|
41
|
+
@proxy.flushdb
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "single key operations" do
|
45
|
+
it "should have simple key operations implemented" do
|
46
|
+
@proxy.set('foo', 1)
|
47
|
+
|
48
|
+
@proxy.get('foo').should == '1'
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should have string operations implemented" do
|
52
|
+
3.times { @proxy.append('foo', 'bar') }
|
53
|
+
@proxy.incr('bar')
|
54
|
+
@proxy.incrby('bar', 10)
|
55
|
+
@proxy.decrby('bar', 3)
|
56
|
+
|
57
|
+
@proxy.strlen('foo').should == 9
|
58
|
+
@proxy.get('bar').should == '8'
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should have list operations implemented" do
|
62
|
+
3.times { |n| @proxy.lpush('foo', "bar#{n}") }
|
63
|
+
|
64
|
+
@proxy.llen('foo').should == 3
|
65
|
+
@proxy.lpop('foo').should == 'bar2'
|
66
|
+
@proxy.rpop('foo').should == 'bar0'
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should have set operations implemented" do
|
70
|
+
3.times { |n| @proxy.sadd('foo', "bar#{n}") }
|
71
|
+
|
72
|
+
@proxy.scard('foo').should == 3
|
73
|
+
@proxy.smembers('foo').sort.should == ['bar0', 'bar1', 'bar2']
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should have zset operations implemented" do
|
77
|
+
@proxy.zadd 'foo', 1, 'bar'
|
78
|
+
@proxy.zadd 'foo', 3, 'baz'
|
79
|
+
@proxy.zadd 'foo', 2, 'bam'
|
80
|
+
|
81
|
+
@proxy.zcard('foo').should == 3
|
82
|
+
@proxy.zcount('foo', 1, 2).should == 2
|
83
|
+
@proxy.zrange('foo', 0, -1).should == ['bar', 'bam', 'baz']
|
84
|
+
@proxy.zrange('foo', 0, -1, :with_scores => true).should == ['bar', '1', 'bam', '2', 'baz', '3']
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should have hash operations implemented" do
|
88
|
+
@proxy.hset 'foo', 'bar', 'hello'
|
89
|
+
3.times { @proxy.hincrby 'foo', 'baz', 2 }
|
90
|
+
|
91
|
+
@proxy.hkeys('foo').should == ['bar', 'baz']
|
92
|
+
@proxy.hgetall('foo').should == {'bar' => 'hello', 'baz' => '6'}
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe "multi key operations" do
|
97
|
+
it "should allow multi key operations as long as they operate on the same shard" do
|
98
|
+
@proxy.mset 'foo{one}', 1, 'bar{one}', 2
|
99
|
+
|
100
|
+
@proxy.mget('foo{one}', 'bar{one}').should == ['1', '2']
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should raise an exception if running an operation on multiple shards" do
|
104
|
+
lambda{
|
105
|
+
@proxy.mapped_mset 'foo{one}' => 1, 'bar{two}' => 2
|
106
|
+
}.should raise_exception(RedisRing::Client::MultiShardOperationError)
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should work with multi key zset operations" do
|
110
|
+
@proxy.zadd 'foo{one}', 1, 1
|
111
|
+
@proxy.zadd 'foo{one}', 1, 2
|
112
|
+
@proxy.zadd 'foo{one}', 1, 3
|
113
|
+
|
114
|
+
@proxy.zadd 'bar{one}', 1, 2
|
115
|
+
@proxy.zadd 'bar{one}', 1, 3
|
116
|
+
@proxy.zadd 'bar{one}', 1, 4
|
117
|
+
|
118
|
+
@proxy.zinterstore 'baz{one}', ['foo{one}', 'bar{one}']
|
119
|
+
|
120
|
+
@proxy.zrange('baz{one}', 0, -1).should == ['2', '3']
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe "scather-gather operations" do
|
125
|
+
it "should sum dbsize and similar operations" do
|
126
|
+
keys = %w{ala ma kota kot ali nazywa sie as}
|
127
|
+
keys.each_with_index { |key, idx| @proxy.set(key, idx) }
|
128
|
+
|
129
|
+
@proxy.keys('*').sort.should == keys.sort
|
130
|
+
@proxy.dbsize.should == keys.size
|
131
|
+
end
|
132
|
+
|
133
|
+
it "should return last_result for operations where result is not important" do
|
134
|
+
@proxy.bgsave.should == "Background saving started"
|
135
|
+
@proxy.flushdb.should == "OK"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
describe "unsupported operations" do
|
140
|
+
it "should raise an exception when using an operation with undefined behavior in RedisRing" do
|
141
|
+
lambda {
|
142
|
+
@proxy.multi do
|
143
|
+
@proxy.set 'foo{one}', 1
|
144
|
+
@proxy.set 'foo{two}', 2
|
145
|
+
end
|
146
|
+
}.should raise_exception(RedisRing::Client::UnsupportedOperationError)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
describe "random shard operations" do
|
151
|
+
it "should execute the operation on a random shard" do
|
152
|
+
@proxy.ping.should == "PONG"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require File.expand_path('../../../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe RedisRing::Client::ShardConnectionPool do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@metadata = FakeRingMetaData.new(5)
|
7
|
+
5.times do |n|
|
8
|
+
@metadata.shards[n] = RedisRing::Client::ShardMetaData.new("host#{n}", 666 + n, :running)
|
9
|
+
end
|
10
|
+
|
11
|
+
@connection_pool = RedisRing::Client::ShardConnectionPool.new(@metadata, @password = nil, @db = 10)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should create a new connection when there was no shard connection before" do
|
15
|
+
@connection_pool.expects(:new_connection).with("host1", 667, @db, @password).returns(:foo).once
|
16
|
+
|
17
|
+
@connection_pool.connection(1).should == :foo
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should cache connections" do
|
21
|
+
@connection_pool.expects(:new_connection).with("host1", 667, @db, @password).returns(:foo).once
|
22
|
+
|
23
|
+
@connection_pool.connection(1).should == :foo
|
24
|
+
@connection_pool.connection(1).should == :foo
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require File.expand_path('../../../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe RedisRing::Client::Sharder do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@sharder = RedisRing::Client::Sharder.new(@meta_data = FakeRingMetaData.new(1024))
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should hash the same value always to the same shard" do
|
10
|
+
shards = (0..9).map{|n| @sharder.shard_for_key("some_key")}
|
11
|
+
|
12
|
+
shards.uniq.size.should == 1
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should never return less than 0 or more than ring_size - 1" do
|
16
|
+
['foo', 'bar', 'baz'].product(['0', '1', '2']).product(['a', 'b', 'c']).each do |arr|
|
17
|
+
str = arr.flatten.join('_')
|
18
|
+
shard = @sharder.shard_for_key(str)
|
19
|
+
|
20
|
+
shard.should >= 0
|
21
|
+
shard.should < 1024
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should be sensitive to ring_size change" do
|
26
|
+
old_val = @sharder.shard_for_key('foo')
|
27
|
+
@meta_data.ring_size = 100
|
28
|
+
@sharder.shard_for_key('foo').should_not == old_val
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should return different shards for slightly different values" do
|
32
|
+
@sharder.shard_for_key('foo1').should_not == @sharder.shard_for_key('foo2')
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should take advantage of the {shard} specifier" do
|
36
|
+
@sharder.shard_for_key('foo1{one}').should == @sharder.shard_for_key('foo2{one}')
|
37
|
+
@sharder.shard_for_key('foo1{one}').should == @sharder.shard_for_key('one')
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: redis_ring_client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: 0.0.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Adam Pohorecki
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2011-03-08 00:00:00 +01:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: redis
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
version: "0"
|
31
|
+
type: :runtime
|
32
|
+
version_requirements: *id001
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: json
|
35
|
+
prerelease: false
|
36
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 0
|
43
|
+
version: "0"
|
44
|
+
type: :runtime
|
45
|
+
version_requirements: *id002
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
prerelease: false
|
49
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
segments:
|
55
|
+
- 0
|
56
|
+
version: "0"
|
57
|
+
type: :development
|
58
|
+
version_requirements: *id003
|
59
|
+
- !ruby/object:Gem::Dependency
|
60
|
+
name: mocha
|
61
|
+
prerelease: false
|
62
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
segments:
|
68
|
+
- 0
|
69
|
+
version: "0"
|
70
|
+
type: :development
|
71
|
+
version_requirements: *id004
|
72
|
+
description: The client counterpart to the RedisRing gem.
|
73
|
+
email:
|
74
|
+
- adam@pohorecki.pl
|
75
|
+
executables: []
|
76
|
+
|
77
|
+
extensions: []
|
78
|
+
|
79
|
+
extra_rdoc_files: []
|
80
|
+
|
81
|
+
files:
|
82
|
+
- .gitignore
|
83
|
+
- Gemfile
|
84
|
+
- Gemfile.lock
|
85
|
+
- MIT-LICENSE.txt
|
86
|
+
- Rakefile
|
87
|
+
- lib/redis_ring/client.rb
|
88
|
+
- lib/redis_ring/client/operation_definitions.rb
|
89
|
+
- lib/redis_ring/client/ring_meta_data.rb
|
90
|
+
- lib/redis_ring/client/ring_proxy.rb
|
91
|
+
- lib/redis_ring/client/shard_connection_pool.rb
|
92
|
+
- lib/redis_ring/client/sharder.rb
|
93
|
+
- lib/redis_ring/client/version.rb
|
94
|
+
- redis_ring_client.gemspec
|
95
|
+
- spec/fakes/fake_ring_meta_data.rb
|
96
|
+
- spec/redis_ring/client/ring_meta_data_spec.rb
|
97
|
+
- spec/redis_ring/client/ring_proxy_spec.rb
|
98
|
+
- spec/redis_ring/client/shard_connection_pool_spec.rb
|
99
|
+
- spec/redis_ring/client/sharder_spec.rb
|
100
|
+
- spec/spec_helper.rb
|
101
|
+
has_rdoc: true
|
102
|
+
homepage: http://github.com/psyho/redis_ring_client
|
103
|
+
licenses: []
|
104
|
+
|
105
|
+
post_install_message:
|
106
|
+
rdoc_options: []
|
107
|
+
|
108
|
+
require_paths:
|
109
|
+
- lib
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
111
|
+
none: false
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
segments:
|
116
|
+
- 0
|
117
|
+
version: "0"
|
118
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
119
|
+
none: false
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
segments:
|
124
|
+
- 0
|
125
|
+
version: "0"
|
126
|
+
requirements: []
|
127
|
+
|
128
|
+
rubyforge_project: redis_ring_client
|
129
|
+
rubygems_version: 1.3.7
|
130
|
+
signing_key:
|
131
|
+
specification_version: 3
|
132
|
+
summary: Client for RedisRing
|
133
|
+
test_files: []
|
134
|
+
|