consistent_hashing 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/License.txt +20 -0
- data/Manifest.txt +15 -0
- data/README.txt +5 -0
- data/Rakefile +139 -0
- data/lib/consistent_hashing.rb +111 -0
- data/lib/consistent_hashing/jenkins_one_at_a_time.rb +25 -0
- data/lib/consistent_hashing/version.rb +9 -0
- data/lib/mem_cache_with_consistent_hashing.rb +67 -0
- data/setup.rb +1585 -0
- data/spec/consistent_hashing_spec.rb +143 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +40 -0
- data/test/benchmark.rb +32 -0
- data/test/test_mem_cache.rb +586 -0
- metadata +64 -0
@@ -0,0 +1,143 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../lib/consistent_hashing'
|
2
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
3
|
+
|
4
|
+
describe "Servers list with a duplicate" do
|
5
|
+
it "should raise a collision error" do
|
6
|
+
|
7
|
+
server = new_mock_server("127.0.0.1:11211")
|
8
|
+
lambda {
|
9
|
+
m = ConsistentHashing::Continuum.new([server, server])
|
10
|
+
}.should raise_error(ConsistentHashing::CollisionError)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "A continuum (with no collisions)" do
|
15
|
+
before do
|
16
|
+
servers = [
|
17
|
+
"192.168.0.1:11211",
|
18
|
+
"192.168.0.2:11211",
|
19
|
+
"192.168.0.3:11211",
|
20
|
+
"192.168.0.4:11211",
|
21
|
+
].map{|address| new_mock_server(address)}
|
22
|
+
@continuum = ConsistentHashing::Continuum.new(servers)
|
23
|
+
@first_anchor = @continuum.anchors.first
|
24
|
+
@last_anchor = @continuum.anchors.last
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should use the first anchor for a search value in (last anchor value, 1]" do
|
28
|
+
search_values = some_values_in_segment(@last_anchor.value, 1.0)
|
29
|
+
|
30
|
+
search_values.each do |search_value|
|
31
|
+
responsible_anchor(@continuum, search_value).should == @first_anchor
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should use the first anchor for a search value in (0, first anchor value]" do
|
36
|
+
search_values = some_values_in_segment(0.0, @first_anchor.value)
|
37
|
+
|
38
|
+
search_values.each do |search_value|
|
39
|
+
responsible_anchor(@continuum, search_value).should == @first_anchor
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should find the correct server for search value in (first anchor value, last anchor value]" do
|
44
|
+
search_values = some_values_in_segment(@first_anchor.value, @last_anchor.value)
|
45
|
+
|
46
|
+
search_values.each do |search_value|
|
47
|
+
responsible_anchor = responsible_anchor(@continuum, search_value)
|
48
|
+
|
49
|
+
previous_anchor = previous_anchor(responsible_anchor, @continuum)
|
50
|
+
|
51
|
+
previous_anchor.value.should < search_value
|
52
|
+
search_value.should <= responsible_anchor.value
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should have a method to return the server reponsible for a key" do
|
57
|
+
search_value = @continuum.hash_to_unit_circle("foo")
|
58
|
+
responsible_anchor = responsible_anchor(@continuum, search_value)
|
59
|
+
|
60
|
+
@continuum.find_server(search_value).should == responsible_anchor.server
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should be able to calculate the expected load" do
|
64
|
+
servers = []
|
65
|
+
3.times do |i|
|
66
|
+
server = mock("server-#{i}")
|
67
|
+
server.stub!(:address).and_return("localhost:1121#{i}")
|
68
|
+
servers << server
|
69
|
+
end
|
70
|
+
|
71
|
+
# server[0]: [ 0, 0.1], (0.4, 0.5], (0.9, 1.0] => 0.1 + 0.1 + 0.1 = 0.3
|
72
|
+
# server[1]: (0.1, 0.2], (0.5, 0.7] => 0.1 + 0.2 = 0.3
|
73
|
+
# server[2]: (0.2, 0.4], (0.7, 0.9] => 0.2 + 0.2 = 0.4
|
74
|
+
value_server_pairs = [
|
75
|
+
[0.1, servers[0]],
|
76
|
+
[0.2, servers[1]],
|
77
|
+
[0.4, servers[2]],
|
78
|
+
[0.5, servers[0]],
|
79
|
+
[0.7, servers[1]],
|
80
|
+
[0.9, servers[2]],
|
81
|
+
]
|
82
|
+
|
83
|
+
anchors = value_server_pairs.map do |value, server|
|
84
|
+
ConsistentHashing::Anchor.new(value, server)
|
85
|
+
end
|
86
|
+
|
87
|
+
address_to_expected_load = {
|
88
|
+
servers[0].address => 0.3,
|
89
|
+
servers[1].address => 0.3,
|
90
|
+
servers[2].address => 0.4,
|
91
|
+
}
|
92
|
+
|
93
|
+
@continuum.stub!(:anchors).and_return(anchors)
|
94
|
+
expected_load = @continuum.expected_load
|
95
|
+
|
96
|
+
# all (key, value) pairs in the hash should be correct
|
97
|
+
expected_load.each do |address, load|
|
98
|
+
load.should be_close(address_to_expected_load[address], 0.0001)
|
99
|
+
end
|
100
|
+
|
101
|
+
# the hash should be complete
|
102
|
+
expected_load.size.should == address_to_expected_load.size
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should have a to_s method" do
|
106
|
+
expected_lines = @continuum.anchors.map{|anchor| anchor.to_s}
|
107
|
+
@continuum.to_s.split("\n").should == expected_lines
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe "A Continuum::Anchor" do
|
112
|
+
before do
|
113
|
+
@value = 0.123
|
114
|
+
@address = "localhost:11211"
|
115
|
+
|
116
|
+
@server = mock('server')
|
117
|
+
@server.stub!(:address).and_return(@address)
|
118
|
+
|
119
|
+
@anchor = ConsistentHashing::Anchor.new(@value, @server)
|
120
|
+
end
|
121
|
+
|
122
|
+
it "should have a to_s method that displays teh value and server address" do
|
123
|
+
@anchor.to_s.should == "value:#{@value}, server:#{@server.address}"
|
124
|
+
end
|
125
|
+
|
126
|
+
it "should have an inspect method" do
|
127
|
+
@anchor.inspect.should ==
|
128
|
+
"<ConsistentHashing::Anchor | value:#{@value}, server:#{@server.address}>"
|
129
|
+
end
|
130
|
+
|
131
|
+
it "should implement the <=> operator by comparing the value of the anchors" do
|
132
|
+
# Unfortunately, I cannot do
|
133
|
+
# @anchor.value.should_receieve(:<=>).with(some_value)
|
134
|
+
# So I will settle for a randomized test
|
135
|
+
10.times do
|
136
|
+
random_value = rand
|
137
|
+
other_anchor = mock("other_anchor")
|
138
|
+
other_anchor.should_receive(:value).and_return(random_value)
|
139
|
+
|
140
|
+
(@anchor <=> other_anchor).should == (@anchor.value <=> random_value)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'spec'
|
3
|
+
|
4
|
+
# For the purposes of testing, I want access to the anchor, not just the server
|
5
|
+
# so that I can easily verify that the correct anchor was returned.
|
6
|
+
def responsible_anchor(continuum, search_value)
|
7
|
+
continuum.send(:closest_anchor, search_value)
|
8
|
+
end
|
9
|
+
|
10
|
+
# Get the anchor responsible for the previous segment.
|
11
|
+
def previous_anchor(anchor, continuum)
|
12
|
+
index = continuum.anchors.index(anchor)
|
13
|
+
previous_index = index - 1
|
14
|
+
continuum.anchors[previous_index]
|
15
|
+
end
|
16
|
+
|
17
|
+
# NOTE: I use the word 'segment' to mean a range that excludes the lower bound
|
18
|
+
# but includes the upper bound. For example, (0, 10] is a segment containing
|
19
|
+
# the numbers 1-10.
|
20
|
+
|
21
|
+
# Get an array of values in the segment. The values will include some edge
|
22
|
+
# cases, and a few random values.
|
23
|
+
def some_values_in_segment(exclusive_lower_bound, inclusive_upper_bound)
|
24
|
+
raise "invalid segment" unless exclusive_lower_bound < inclusive_upper_bound
|
25
|
+
|
26
|
+
difference = inclusive_upper_bound - exclusive_lower_bound
|
27
|
+
values = [ 0.01, 0.25, 0.50, 0.75, 0.99 ].map do |r|
|
28
|
+
exclusive_lower_bound + r*difference
|
29
|
+
end
|
30
|
+
values << inclusive_upper_bound
|
31
|
+
values
|
32
|
+
end
|
33
|
+
|
34
|
+
def new_mock_server(address, weight=1)
|
35
|
+
server = Object.new
|
36
|
+
server.stub!(:address).and_return(address)
|
37
|
+
server.stub!(:weight).and_return(weight)
|
38
|
+
server
|
39
|
+
end
|
40
|
+
|
data/test/benchmark.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'benchmark'
|
3
|
+
|
4
|
+
require 'memcache'
|
5
|
+
require File.dirname(__FILE__) + '/../lib/mem_cache_with_consistent_hashing'
|
6
|
+
|
7
|
+
class MemCache::Server
|
8
|
+
def alive?
|
9
|
+
true
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
servers = [*"localhost:11211".."localhost:11230"]
|
14
|
+
|
15
|
+
original_client = MemCache.new(servers)
|
16
|
+
consistent_hashing_client = MemCacheWithConsistentHashing.new(servers)
|
17
|
+
|
18
|
+
num_keys = 10_000
|
19
|
+
keys = [*0...num_keys].map{|i| rand.to_s}
|
20
|
+
|
21
|
+
time_for_original, time_for_consistent_hashing =
|
22
|
+
[original_client, consistent_hashing_client].map do |client|
|
23
|
+
Benchmark.realtime do
|
24
|
+
for key in keys
|
25
|
+
client.get_server_for_key(key)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
printf("%4.2f <= time_for_original\n", time_for_original)
|
31
|
+
printf("%4.2f <= time_for_consistent_hashing\n", time_for_consistent_hashing)
|
32
|
+
printf("%4.2f <= time_for_original/time_for_consistent_hashing\n", time_for_original/time_for_consistent_hashing)
|
@@ -0,0 +1,586 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'rubygems'
|
4
|
+
require 'test/zentest_assertions'
|
5
|
+
|
6
|
+
$TESTING = true
|
7
|
+
|
8
|
+
require File.dirname(__FILE__) + '/../lib/mem_cache_with_consistent_hashing'
|
9
|
+
|
10
|
+
class MemCache
|
11
|
+
|
12
|
+
attr_reader :servers
|
13
|
+
attr_writer :namespace
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
class MemCacheWithConsistentHashing
|
18
|
+
|
19
|
+
def add_server(server)
|
20
|
+
@continuum.anchors = (@continuum.anchors + @continuum.send(:create_anchors_for_server, server).sort)
|
21
|
+
@servers << server
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
class FakeSocket
|
27
|
+
|
28
|
+
attr_reader :written, :data
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
@written = StringIO.new
|
32
|
+
@data = StringIO.new
|
33
|
+
end
|
34
|
+
|
35
|
+
def write(data)
|
36
|
+
@written.write data
|
37
|
+
end
|
38
|
+
|
39
|
+
def gets
|
40
|
+
@data.gets
|
41
|
+
end
|
42
|
+
|
43
|
+
def read(arg)
|
44
|
+
@data.read arg
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
class FakeServer
|
50
|
+
|
51
|
+
attr_reader :socket
|
52
|
+
|
53
|
+
@@port = 11211
|
54
|
+
|
55
|
+
def initialize(socket = nil)
|
56
|
+
@port = @@port
|
57
|
+
@@port += 1
|
58
|
+
@socket = socket || FakeSocket.new
|
59
|
+
@closed = false
|
60
|
+
end
|
61
|
+
|
62
|
+
def close
|
63
|
+
@closed = true
|
64
|
+
end
|
65
|
+
|
66
|
+
def alive?
|
67
|
+
!@closed
|
68
|
+
end
|
69
|
+
|
70
|
+
def weight
|
71
|
+
1
|
72
|
+
end
|
73
|
+
|
74
|
+
def address
|
75
|
+
"localhost:#{@port}"
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
class TestMemCache < Test::Unit::TestCase
|
81
|
+
|
82
|
+
def setup
|
83
|
+
@cache = MemCacheWithConsistentHashing.new 'localhost:1', :namespace => 'my_namespace'
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_cache_get
|
87
|
+
server = util_setup_fake_server
|
88
|
+
|
89
|
+
assert_equal "\004\b\"\0170123456789",
|
90
|
+
@cache.cache_get(server, 'my_namespace:key')
|
91
|
+
|
92
|
+
assert_equal "get my_namespace:key\r\n",
|
93
|
+
server.socket.written.string
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_cache_get_bad_state
|
97
|
+
server = FakeServer.new
|
98
|
+
server.socket.data.write "bogus response\r\n"
|
99
|
+
server.socket.data.rewind
|
100
|
+
|
101
|
+
@cache.servers = []
|
102
|
+
@cache.add_server(server)
|
103
|
+
|
104
|
+
e = assert_raise MemCache::MemCacheError do
|
105
|
+
@cache.cache_get(server, 'my_namespace:key')
|
106
|
+
end
|
107
|
+
|
108
|
+
assert_equal "unexpected response \"bogus response\\r\\n\"", e.message
|
109
|
+
|
110
|
+
deny server.alive?
|
111
|
+
|
112
|
+
assert_equal "get my_namespace:key\r\n",
|
113
|
+
server.socket.written.string
|
114
|
+
end
|
115
|
+
|
116
|
+
def test_cache_get_miss
|
117
|
+
socket = FakeSocket.new
|
118
|
+
socket.data.write "END\r\n"
|
119
|
+
socket.data.rewind
|
120
|
+
server = FakeServer.new socket
|
121
|
+
|
122
|
+
assert_equal nil, @cache.cache_get(server, 'my_namespace:key')
|
123
|
+
|
124
|
+
assert_equal "get my_namespace:key\r\n",
|
125
|
+
socket.written.string
|
126
|
+
end
|
127
|
+
|
128
|
+
def test_cache_get_multi_bad_state
|
129
|
+
server = FakeServer.new
|
130
|
+
server.socket.data.write "bogus response\r\n"
|
131
|
+
server.socket.data.rewind
|
132
|
+
|
133
|
+
@cache.servers = []
|
134
|
+
@cache.add_server(server)
|
135
|
+
|
136
|
+
e = assert_raise MemCache::MemCacheError do
|
137
|
+
@cache.cache_get_multi(server, ['my_namespace:key'])
|
138
|
+
end
|
139
|
+
|
140
|
+
assert_equal "unexpected response \"bogus response\\r\\n\"", e.message
|
141
|
+
|
142
|
+
deny server.alive?
|
143
|
+
|
144
|
+
assert_equal "get my_namespace:key\r\n",
|
145
|
+
server.socket.written.string
|
146
|
+
end
|
147
|
+
|
148
|
+
def test_crc32_ITU_T
|
149
|
+
assert_equal 0, ''.crc32_ITU_T
|
150
|
+
assert_equal 1260851911, 'my_namespace:key'.crc32_ITU_T
|
151
|
+
end
|
152
|
+
|
153
|
+
def test_initialize
|
154
|
+
cache = MemCacheWithConsistentHashing.new :namespace => 'my_namespace', :readonly => true
|
155
|
+
|
156
|
+
assert_equal 'my_namespace', cache.namespace
|
157
|
+
assert_equal true, cache.readonly?
|
158
|
+
assert_equal true, cache.servers.empty?
|
159
|
+
end
|
160
|
+
|
161
|
+
def test_initialize_compatible
|
162
|
+
cache = MemCacheWithConsistentHashing.new ['localhost:11211', 'localhost:11212'],
|
163
|
+
:namespace => 'my_namespace', :readonly => true
|
164
|
+
|
165
|
+
assert_equal 'my_namespace', cache.namespace
|
166
|
+
assert_equal true, cache.readonly?
|
167
|
+
assert_equal false, cache.servers.empty?
|
168
|
+
end
|
169
|
+
|
170
|
+
def test_initialize_compatible_no_hash
|
171
|
+
cache = MemCacheWithConsistentHashing.new ['localhost:11211', 'localhost:11212']
|
172
|
+
|
173
|
+
assert_equal nil, cache.namespace
|
174
|
+
assert_equal false, cache.readonly?
|
175
|
+
assert_equal false, cache.servers.empty?
|
176
|
+
end
|
177
|
+
|
178
|
+
def test_initialize_compatible_one_server
|
179
|
+
cache = MemCacheWithConsistentHashing.new 'localhost:11211'
|
180
|
+
|
181
|
+
assert_equal nil, cache.namespace
|
182
|
+
assert_equal false, cache.readonly?
|
183
|
+
assert_equal false, cache.servers.empty?
|
184
|
+
end
|
185
|
+
|
186
|
+
def test_initialize_compatible_bad_arg
|
187
|
+
e = assert_raise ArgumentError do
|
188
|
+
cache = MemCacheWithConsistentHashing.new Object.new
|
189
|
+
end
|
190
|
+
|
191
|
+
assert_equal 'first argument must be Array, Hash or String', e.message
|
192
|
+
end
|
193
|
+
|
194
|
+
def test_initialize_multiple_servers
|
195
|
+
cache = MemCacheWithConsistentHashing.new %w[localhost:11211 localhost:11212],
|
196
|
+
:namespace => 'my_namespace', :readonly => true
|
197
|
+
|
198
|
+
assert_equal 'my_namespace', cache.namespace
|
199
|
+
assert_equal true, cache.readonly?
|
200
|
+
assert_equal false, cache.servers.empty?
|
201
|
+
deny_empty cache.instance_variable_get(:@buckets)
|
202
|
+
end
|
203
|
+
|
204
|
+
def test_initialize_too_many_args
|
205
|
+
assert_raises ArgumentError do
|
206
|
+
MemCacheWithConsistentHashing.new 1, 2, 3
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def test_decr
|
211
|
+
server = FakeServer.new
|
212
|
+
server.socket.data.write "5\r\n"
|
213
|
+
server.socket.data.rewind
|
214
|
+
|
215
|
+
@cache.servers = []
|
216
|
+
@cache.add_server(server)
|
217
|
+
|
218
|
+
value = @cache.decr 'key'
|
219
|
+
|
220
|
+
assert_equal "decr my_namespace:key 1\r\n",
|
221
|
+
@cache.servers.first.socket.written.string
|
222
|
+
|
223
|
+
assert_equal 5, value
|
224
|
+
end
|
225
|
+
|
226
|
+
def test_decr_not_found
|
227
|
+
server = FakeServer.new
|
228
|
+
server.socket.data.write "NOT_FOUND\r\n"
|
229
|
+
server.socket.data.rewind
|
230
|
+
|
231
|
+
@cache.servers = []
|
232
|
+
@cache.add_server(server)
|
233
|
+
|
234
|
+
value = @cache.decr 'key'
|
235
|
+
|
236
|
+
assert_equal "decr my_namespace:key 1\r\n",
|
237
|
+
@cache.servers.first.socket.written.string
|
238
|
+
|
239
|
+
assert_equal nil, value
|
240
|
+
end
|
241
|
+
|
242
|
+
def test_decr_space_padding
|
243
|
+
server = FakeServer.new
|
244
|
+
server.socket.data.write "5 \r\n"
|
245
|
+
server.socket.data.rewind
|
246
|
+
|
247
|
+
@cache.servers = []
|
248
|
+
@cache.add_server(server)
|
249
|
+
|
250
|
+
value = @cache.decr 'key'
|
251
|
+
|
252
|
+
assert_equal "decr my_namespace:key 1\r\n",
|
253
|
+
@cache.servers.first.socket.written.string
|
254
|
+
|
255
|
+
assert_equal 5, value
|
256
|
+
end
|
257
|
+
|
258
|
+
def test_get
|
259
|
+
util_setup_fake_server
|
260
|
+
|
261
|
+
value = @cache.get 'key'
|
262
|
+
|
263
|
+
assert_equal "get my_namespace:key\r\n",
|
264
|
+
@cache.servers.first.socket.written.string
|
265
|
+
|
266
|
+
assert_equal '0123456789', value
|
267
|
+
end
|
268
|
+
|
269
|
+
def test_get_bad_key
|
270
|
+
util_setup_fake_server
|
271
|
+
assert_raise ArgumentError do @cache.get 'k y' end
|
272
|
+
|
273
|
+
util_setup_fake_server
|
274
|
+
assert_raise ArgumentError do @cache.get 'k' * 250 end
|
275
|
+
end
|
276
|
+
|
277
|
+
def test_get_cache_get_IOError
|
278
|
+
socket = Object.new
|
279
|
+
def socket.write(arg) raise IOError, 'some io error'; end
|
280
|
+
server = FakeServer.new socket
|
281
|
+
|
282
|
+
@cache.servers = []
|
283
|
+
@cache.add_server(server)
|
284
|
+
|
285
|
+
e = assert_raise MemCache::MemCacheError do
|
286
|
+
@cache.get 'my_namespace:key'
|
287
|
+
end
|
288
|
+
|
289
|
+
assert_equal 'some io error', e.message
|
290
|
+
end
|
291
|
+
|
292
|
+
def test_get_cache_get_SystemCallError
|
293
|
+
socket = Object.new
|
294
|
+
def socket.write(arg) raise SystemCallError, 'some syscall error'; end
|
295
|
+
server = FakeServer.new socket
|
296
|
+
|
297
|
+
@cache.servers = []
|
298
|
+
@cache.add_server(server)
|
299
|
+
|
300
|
+
e = assert_raise MemCache::MemCacheError do
|
301
|
+
@cache.get 'my_namespace:key'
|
302
|
+
end
|
303
|
+
|
304
|
+
assert_equal 'unknown error - some syscall error', e.message
|
305
|
+
end
|
306
|
+
|
307
|
+
def test_get_no_connection
|
308
|
+
@cache.servers = 'localhost:1'
|
309
|
+
e = assert_raise MemCache::MemCacheError do
|
310
|
+
@cache.get 'key'
|
311
|
+
end
|
312
|
+
|
313
|
+
assert_equal 'No connection to server', e.message
|
314
|
+
end
|
315
|
+
|
316
|
+
def test_get_no_servers
|
317
|
+
@cache.servers = []
|
318
|
+
e = assert_raise MemCache::MemCacheError do
|
319
|
+
@cache.get 'key'
|
320
|
+
end
|
321
|
+
|
322
|
+
assert_equal 'No active servers', e.message
|
323
|
+
end
|
324
|
+
|
325
|
+
def test_get_multi
|
326
|
+
server = FakeServer.new
|
327
|
+
server.socket.data.write "VALUE my_namespace:key 0 14\r\n"
|
328
|
+
server.socket.data.write "\004\b\"\0170123456789\r\n"
|
329
|
+
server.socket.data.write "VALUE my_namespace:keyb 0 14\r\n"
|
330
|
+
server.socket.data.write "\004\b\"\0179876543210\r\n"
|
331
|
+
server.socket.data.write "END\r\n"
|
332
|
+
server.socket.data.rewind
|
333
|
+
|
334
|
+
@cache.servers = []
|
335
|
+
@cache.add_server(server)
|
336
|
+
|
337
|
+
values = @cache.get_multi 'key', 'keyb'
|
338
|
+
|
339
|
+
assert_equal "get my_namespace:key my_namespace:keyb\r\n",
|
340
|
+
server.socket.written.string
|
341
|
+
|
342
|
+
expected = { 'key' => '0123456789', 'keyb' => '9876543210' }
|
343
|
+
|
344
|
+
assert_equal expected.sort, values.sort
|
345
|
+
end
|
346
|
+
|
347
|
+
def test_get_raw
|
348
|
+
server = FakeServer.new
|
349
|
+
server.socket.data.write "VALUE my_namespace:key 0 10\r\n"
|
350
|
+
server.socket.data.write "0123456789\r\n"
|
351
|
+
server.socket.data.write "END\r\n"
|
352
|
+
server.socket.data.rewind
|
353
|
+
|
354
|
+
@cache.servers = []
|
355
|
+
@cache.add_server(server)
|
356
|
+
|
357
|
+
|
358
|
+
value = @cache.get 'key', true
|
359
|
+
|
360
|
+
assert_equal "get my_namespace:key\r\n",
|
361
|
+
@cache.servers.first.socket.written.string
|
362
|
+
|
363
|
+
assert_equal '0123456789', value
|
364
|
+
end
|
365
|
+
|
366
|
+
def test_get_server_for_key
|
367
|
+
server = @cache.get_server_for_key 'key'
|
368
|
+
assert_equal 'localhost', server.host
|
369
|
+
assert_equal 1, server.port
|
370
|
+
end
|
371
|
+
|
372
|
+
def test_get_server_for_key_multiple
|
373
|
+
s1 = util_setup_server @cache, 'one.example.com', ''
|
374
|
+
s2 = util_setup_server @cache, 'two.example.com', ''
|
375
|
+
@cache.instance_variable_set :@servers, [s1, s2]
|
376
|
+
@cache.instance_variable_set :@buckets, [s1, s2]
|
377
|
+
|
378
|
+
server = @cache.get_server_for_key 'keya'
|
379
|
+
assert_equal 'two.example.com', server.host
|
380
|
+
server = @cache.get_server_for_key 'keyb'
|
381
|
+
assert_equal 'two.example.com', server.host
|
382
|
+
end
|
383
|
+
|
384
|
+
def test_get_server_for_key_no_servers
|
385
|
+
@cache.servers = []
|
386
|
+
|
387
|
+
e = assert_raise MemCache::MemCacheError do
|
388
|
+
@cache.get_server_for_key 'key'
|
389
|
+
end
|
390
|
+
|
391
|
+
assert_equal 'No servers available', e.message
|
392
|
+
end
|
393
|
+
|
394
|
+
def test_get_server_for_key_spaces
|
395
|
+
e = assert_raise ArgumentError do
|
396
|
+
@cache.get_server_for_key 'space key'
|
397
|
+
end
|
398
|
+
assert_equal 'illegal character in key "space key"', e.message
|
399
|
+
end
|
400
|
+
|
401
|
+
def test_get_server_for_key_length
|
402
|
+
@cache.get_server_for_key 'x' * 250
|
403
|
+
long_key = 'x' * 251
|
404
|
+
e = assert_raise ArgumentError do
|
405
|
+
@cache.get_server_for_key long_key
|
406
|
+
end
|
407
|
+
assert_equal "key too long #{long_key.inspect}", e.message
|
408
|
+
end
|
409
|
+
|
410
|
+
def test_incr
|
411
|
+
server = FakeServer.new
|
412
|
+
server.socket.data.write "5\r\n"
|
413
|
+
server.socket.data.rewind
|
414
|
+
|
415
|
+
@cache.servers = []
|
416
|
+
@cache.add_server(server)
|
417
|
+
|
418
|
+
value = @cache.incr 'key'
|
419
|
+
|
420
|
+
assert_equal "incr my_namespace:key 1\r\n",
|
421
|
+
@cache.servers.first.socket.written.string
|
422
|
+
|
423
|
+
assert_equal 5, value
|
424
|
+
end
|
425
|
+
|
426
|
+
def test_incr_not_found
|
427
|
+
server = FakeServer.new
|
428
|
+
server.socket.data.write "NOT_FOUND\r\n"
|
429
|
+
server.socket.data.rewind
|
430
|
+
|
431
|
+
@cache.servers = []
|
432
|
+
@cache.add_server(server)
|
433
|
+
|
434
|
+
value = @cache.incr 'key'
|
435
|
+
|
436
|
+
assert_equal "incr my_namespace:key 1\r\n",
|
437
|
+
@cache.servers.first.socket.written.string
|
438
|
+
|
439
|
+
assert_equal nil, value
|
440
|
+
end
|
441
|
+
|
442
|
+
def test_incr_space_padding
|
443
|
+
server = FakeServer.new
|
444
|
+
server.socket.data.write "5 \r\n"
|
445
|
+
server.socket.data.rewind
|
446
|
+
|
447
|
+
@cache.servers = []
|
448
|
+
@cache.add_server(server)
|
449
|
+
|
450
|
+
value = @cache.incr 'key'
|
451
|
+
|
452
|
+
assert_equal "incr my_namespace:key 1\r\n",
|
453
|
+
@cache.servers.first.socket.written.string
|
454
|
+
|
455
|
+
assert_equal 5, value
|
456
|
+
end
|
457
|
+
|
458
|
+
def test_make_cache_key
|
459
|
+
assert_equal 'my_namespace:key', @cache.make_cache_key('key')
|
460
|
+
@cache.namespace = nil
|
461
|
+
assert_equal 'key', @cache.make_cache_key('key')
|
462
|
+
end
|
463
|
+
|
464
|
+
def test_servers_equals_type_error
|
465
|
+
e = assert_raise TypeError do
|
466
|
+
@cache.servers = [Object.new]
|
467
|
+
end
|
468
|
+
|
469
|
+
assert_equal 'cannot convert Object into MemCache::Server', e.message
|
470
|
+
end
|
471
|
+
|
472
|
+
def test_set
|
473
|
+
server = FakeServer.new
|
474
|
+
server.socket.data.write "STORED\r\n"
|
475
|
+
server.socket.data.rewind
|
476
|
+
@cache.servers = []
|
477
|
+
@cache.add_server(server)
|
478
|
+
|
479
|
+
@cache.set 'key', 'value'
|
480
|
+
|
481
|
+
expected = "set my_namespace:key 0 0 9\r\n\004\b\"\nvalue\r\n"
|
482
|
+
assert_equal expected, server.socket.written.string
|
483
|
+
end
|
484
|
+
|
485
|
+
def test_set_expiry
|
486
|
+
server = FakeServer.new
|
487
|
+
server.socket.data.write "STORED\r\n"
|
488
|
+
server.socket.data.rewind
|
489
|
+
@cache.servers = []
|
490
|
+
@cache.add_server(server)
|
491
|
+
|
492
|
+
@cache.set 'key', 'value', 5
|
493
|
+
|
494
|
+
expected = "set my_namespace:key 0 5 9\r\n\004\b\"\nvalue\r\n"
|
495
|
+
assert_equal expected, server.socket.written.string
|
496
|
+
end
|
497
|
+
|
498
|
+
def test_set_raw
|
499
|
+
server = FakeServer.new
|
500
|
+
server.socket.data.write "STORED\r\n"
|
501
|
+
server.socket.data.rewind
|
502
|
+
@cache.servers = []
|
503
|
+
@cache.add_server(server)
|
504
|
+
|
505
|
+
@cache.set 'key', 'value', 0, true
|
506
|
+
|
507
|
+
expected = "set my_namespace:key 0 0 5\r\nvalue\r\n"
|
508
|
+
assert_equal expected, server.socket.written.string
|
509
|
+
end
|
510
|
+
|
511
|
+
def test_set_readonly
|
512
|
+
cache = MemCacheWithConsistentHashing.new :readonly => true
|
513
|
+
|
514
|
+
e = assert_raise MemCache::MemCacheError do
|
515
|
+
cache.set 'key', 'value'
|
516
|
+
end
|
517
|
+
|
518
|
+
assert_equal 'Update of readonly cache', e.message
|
519
|
+
end
|
520
|
+
|
521
|
+
def test_stats
|
522
|
+
socket = FakeSocket.new
|
523
|
+
socket.data.write "STAT pid 20188\r\nSTAT total_items 32\r\rEND\r\n"
|
524
|
+
socket.data.rewind
|
525
|
+
server = FakeServer.new socket
|
526
|
+
def server.host() "localhost"; end
|
527
|
+
def server.port() 11211; end
|
528
|
+
|
529
|
+
@cache.servers = []
|
530
|
+
@cache.add_server(server)
|
531
|
+
|
532
|
+
expected = {"localhost:11211"=>{"pid"=>"20188", "total_items"=>"32"}}
|
533
|
+
assert_equal expected, @cache.stats
|
534
|
+
|
535
|
+
assert_equal "stats\r\n", socket.written.string
|
536
|
+
end
|
537
|
+
|
538
|
+
def test_basic_threaded_operations_should_work
|
539
|
+
cache = MemCacheWithConsistentHashing.new :multithread => true,
|
540
|
+
:namespace => 'my_namespace',
|
541
|
+
:readonly => false
|
542
|
+
server = util_setup_server cache, 'example.com', "OK\r\n"
|
543
|
+
cache.instance_variable_set :@servers, [server]
|
544
|
+
|
545
|
+
assert_nothing_raised do
|
546
|
+
cache.set "test", "test value"
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
def test_basic_unthreaded_operations_should_work
|
551
|
+
cache = MemCacheWithConsistentHashing.new :multithread => false,
|
552
|
+
:namespace => 'my_namespace',
|
553
|
+
:readonly => false
|
554
|
+
server = util_setup_server cache, 'example.com', "OK\r\n"
|
555
|
+
cache.instance_variable_set :@servers, [server]
|
556
|
+
|
557
|
+
assert_nothing_raised do
|
558
|
+
cache.set "test", "test value"
|
559
|
+
end
|
560
|
+
end
|
561
|
+
|
562
|
+
def util_setup_fake_server
|
563
|
+
server = FakeServer.new
|
564
|
+
server.socket.data.write "VALUE my_namespace:key 0 14\r\n"
|
565
|
+
server.socket.data.write "\004\b\"\0170123456789\r\n"
|
566
|
+
server.socket.data.write "END\r\n"
|
567
|
+
server.socket.data.rewind
|
568
|
+
|
569
|
+
@cache.servers = []
|
570
|
+
@cache.add_server(server)
|
571
|
+
|
572
|
+
return server
|
573
|
+
end
|
574
|
+
|
575
|
+
def util_setup_server(memcache, host, responses)
|
576
|
+
server = MemCache::Server.new memcache, host
|
577
|
+
server.instance_variable_set :@sock, StringIO.new(responses)
|
578
|
+
|
579
|
+
@cache.servers = []
|
580
|
+
@cache.add_server(server)
|
581
|
+
|
582
|
+
return server
|
583
|
+
end
|
584
|
+
|
585
|
+
end
|
586
|
+
|