redis_ring_client 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|