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 ADDED
@@ -0,0 +1,5 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
4
+ .rvmrc
5
+ tags
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in redis_ring_client.gemspec
4
+ gemspec
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,5 @@
1
+ module RedisRing
2
+ module Client
3
+ VERSION = "0.0.1"
4
+ end
5
+ 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,15 @@
1
+ class FakeRingMetaData
2
+
3
+ attr_accessor :ring_size
4
+ attr_accessor :shards
5
+
6
+ def initialize(ring_size)
7
+ @ring_size = ring_size
8
+ @shards = {}
9
+ end
10
+
11
+ def shard(shard_no)
12
+ return @shards[shard_no]
13
+ end
14
+
15
+ 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
@@ -0,0 +1,10 @@
1
+ $:.push File.expand_path("../../lib", __FILE__)
2
+
3
+ require 'redis_ring/client'
4
+
5
+ require File.expand_path('../fakes/fake_ring_meta_data', __FILE__)
6
+
7
+ RSpec.configure do |c|
8
+ c.color_enabled = true
9
+ c.mock_with :mocha
10
+ end
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
+