KellyMahan-memcachedb-client 1.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/History.txt +3 -0
- data/LICENSE.txt +28 -0
- data/README.rdoc +28 -0
- data/Rakefile +26 -0
- data/lib/continuum_db.rb +77 -0
- data/lib/memcache_db.rb +896 -0
- data/lib/memcache_util_db.rb +102 -0
- data/test/test_mem_cache_db.rb +950 -0
- metadata +69 -0
@@ -0,0 +1,102 @@
|
|
1
|
+
##
|
2
|
+
# A utility wrapper around the MemCacheDb client to simplify cache access. All
|
3
|
+
# methods silently ignore MemCacheDb errors.
|
4
|
+
|
5
|
+
module CacheDb
|
6
|
+
|
7
|
+
##
|
8
|
+
# Try to return a logger object that does not rely
|
9
|
+
# on ActiveRecord for logging.
|
10
|
+
def self.logger
|
11
|
+
@logger ||= if defined? Rails.logger # Rails 2.1 +
|
12
|
+
Rails.logger
|
13
|
+
elsif defined? RAILS_DEFAULT_LOGGER # Rails 1.2.2 +
|
14
|
+
RAILS_DEFAULT_LOGGER
|
15
|
+
else
|
16
|
+
ActiveRecord::Base.logger # ... very old Rails.
|
17
|
+
end
|
18
|
+
end
|
19
|
+
##
|
20
|
+
# Returns the object at +key+ from the cache if successful, or nil if either
|
21
|
+
# the object is not in the cache or if there was an error attermpting to
|
22
|
+
# access the cache.
|
23
|
+
#
|
24
|
+
# If there is a cache miss and a block is given the result of the block will
|
25
|
+
# be stored in the cache with optional +expiry+, using the +add+ method rather
|
26
|
+
# than +set+.
|
27
|
+
|
28
|
+
def self.get(key, expiry = 0)
|
29
|
+
start_time = Time.now
|
30
|
+
value = CACHE.get key
|
31
|
+
elapsed = Time.now - start_time
|
32
|
+
logger.debug('MemCacheDb Get (%0.6f) %s' % [elapsed, key])
|
33
|
+
if value.nil? and block_given? then
|
34
|
+
value = yield
|
35
|
+
add key, value, expiry
|
36
|
+
end
|
37
|
+
value
|
38
|
+
rescue MemCacheDb::MemCacheDbError => err
|
39
|
+
logger.debug "MemCacheDb Error: #{err.message}"
|
40
|
+
if block_given? then
|
41
|
+
value = yield
|
42
|
+
put key, value, expiry
|
43
|
+
end
|
44
|
+
value
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# Sets +value+ in the cache at +key+, with an optional +expiry+ time in
|
49
|
+
# seconds.
|
50
|
+
|
51
|
+
def self.put(key, value, expiry = 0)
|
52
|
+
start_time = Time.now
|
53
|
+
CACHE.set key, value, expiry
|
54
|
+
elapsed = Time.now - start_time
|
55
|
+
logger.debug('MemCacheDb Set (%0.6f) %s' % [elapsed, key])
|
56
|
+
value
|
57
|
+
rescue MemCacheDb::MemCacheDbError => err
|
58
|
+
ActiveRecord::Base.logger.debug "MemCacheDb Error: #{err.message}"
|
59
|
+
nil
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Sets +value+ in the cache at +key+, with an optional +expiry+ time in
|
64
|
+
# seconds. If +key+ already exists in cache, returns nil.
|
65
|
+
|
66
|
+
def self.add(key, value, expiry = 0)
|
67
|
+
start_time = Time.now
|
68
|
+
response = CACHE.add key, value, expiry
|
69
|
+
elapsed = Time.now - start_time
|
70
|
+
logger.debug('MemCacheDb Add (%0.6f) %s' % [elapsed, key])
|
71
|
+
(response == "STORED\r\n") ? value : nil
|
72
|
+
rescue MemCacheDb::MemCacheDbError => err
|
73
|
+
ActiveRecord::Base.logger.debug "MemCacheDb Error: #{err.message}"
|
74
|
+
nil
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# Deletes +key+ from the cache in +delay+ seconds.
|
79
|
+
|
80
|
+
def self.delete(key, delay = nil)
|
81
|
+
start_time = Time.now
|
82
|
+
CACHE.delete key, delay
|
83
|
+
elapsed = Time.now - start_time
|
84
|
+
logger.debug('MemCacheDb Delete (%0.6f) %s' %
|
85
|
+
[elapsed, key])
|
86
|
+
nil
|
87
|
+
rescue MemCacheDb::MemCacheDbError => err
|
88
|
+
logger.debug "MemCacheDb Error: #{err.message}"
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
|
92
|
+
##
|
93
|
+
# Resets all connections to MemCacheDb servers.
|
94
|
+
|
95
|
+
def self.reset
|
96
|
+
CACHE.reset
|
97
|
+
logger.debug 'MemCacheDb Connections Reset'
|
98
|
+
nil
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
@@ -0,0 +1,950 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'logger'
|
3
|
+
require 'stringio'
|
4
|
+
require 'test/unit'
|
5
|
+
require 'rubygems'
|
6
|
+
begin
|
7
|
+
gem 'flexmock'
|
8
|
+
require 'flexmock/test_unit'
|
9
|
+
rescue LoadError => e
|
10
|
+
puts "Some tests require flexmock, please run `gem install flexmock`"
|
11
|
+
end
|
12
|
+
|
13
|
+
$TESTING = true
|
14
|
+
|
15
|
+
require File.dirname(__FILE__) + '/../lib/memcache'
|
16
|
+
|
17
|
+
class MemCacheDb
|
18
|
+
|
19
|
+
attr_writer :namespace
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
class FakeSocketDb
|
24
|
+
|
25
|
+
attr_reader :written, :data
|
26
|
+
|
27
|
+
def initialize
|
28
|
+
@written = StringIO.new
|
29
|
+
@data = StringIO.new
|
30
|
+
end
|
31
|
+
|
32
|
+
def write(data)
|
33
|
+
@written.write data
|
34
|
+
end
|
35
|
+
|
36
|
+
def gets
|
37
|
+
@data.gets
|
38
|
+
end
|
39
|
+
|
40
|
+
def read(arg)
|
41
|
+
@data.read arg
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
class Test::Unit::TestCase
|
47
|
+
def requirement(bool, msg)
|
48
|
+
if bool
|
49
|
+
yield
|
50
|
+
else
|
51
|
+
puts msg
|
52
|
+
assert true
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def memcached_running?
|
57
|
+
TCPSocket.new('localhost', 11211) rescue false
|
58
|
+
end
|
59
|
+
|
60
|
+
def xprofile(name, &block)
|
61
|
+
a = Time.now
|
62
|
+
block.call
|
63
|
+
Time.now - a
|
64
|
+
end
|
65
|
+
|
66
|
+
def profile(name, &block)
|
67
|
+
require 'ruby-prof'
|
68
|
+
a = Time.now
|
69
|
+
result = RubyProf.profile(&block)
|
70
|
+
time = Time.now - a
|
71
|
+
printer = RubyProf::GraphHtmlPrinter.new(result)
|
72
|
+
File.open("#{name}.html", 'w') do |f|
|
73
|
+
printer.print(f, :min_percent=>1)
|
74
|
+
end
|
75
|
+
time
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
class FakeServerDb
|
81
|
+
|
82
|
+
attr_reader :host, :port, :socket, :weight, :multithread, :status
|
83
|
+
|
84
|
+
def initialize(socket = nil)
|
85
|
+
@closed = false
|
86
|
+
@host = 'example.com'
|
87
|
+
@port = 11211
|
88
|
+
@socket = socket || FakeSocketDb.new
|
89
|
+
@weight = 1
|
90
|
+
@multithread = false
|
91
|
+
@status = "CONNECTED"
|
92
|
+
end
|
93
|
+
|
94
|
+
def close
|
95
|
+
# begin
|
96
|
+
# raise "Already closed"
|
97
|
+
# rescue => e
|
98
|
+
# puts e.backtrace.join("\n")
|
99
|
+
# end
|
100
|
+
@closed = true
|
101
|
+
@socket = nil
|
102
|
+
@status = "NOT CONNECTED"
|
103
|
+
end
|
104
|
+
|
105
|
+
def alive?
|
106
|
+
# puts "I'm #{@closed ? 'dead' : 'alive'}"
|
107
|
+
!@closed
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
class TestMemCacheDb < Test::Unit::TestCase
|
113
|
+
|
114
|
+
def setup
|
115
|
+
@cache = MemCacheDb.new 'localhost:1', :namespace => 'my_namespace'
|
116
|
+
end
|
117
|
+
|
118
|
+
def test_performance
|
119
|
+
requirement(memcached_running?, 'A real memcached server must be running for performance testing') do
|
120
|
+
host = Socket.gethostname
|
121
|
+
|
122
|
+
cache = MemCacheDb.new(['localhost:21201',"#{host}:21201"])
|
123
|
+
cache.add('a', 1, 120)
|
124
|
+
with = xprofile 'get' do
|
125
|
+
1000.times do
|
126
|
+
cache.get('a')
|
127
|
+
end
|
128
|
+
end
|
129
|
+
puts ''
|
130
|
+
puts "1000 gets with socket timeout: #{with} sec"
|
131
|
+
|
132
|
+
cache = MemCacheDb.new(['localhost:21201',"#{host}:21201"], :timeout => nil)
|
133
|
+
cache.add('a', 1, 120)
|
134
|
+
without = xprofile 'get' do
|
135
|
+
1000.times do
|
136
|
+
cache.get('a')
|
137
|
+
end
|
138
|
+
end
|
139
|
+
puts "1000 gets without socket timeout: #{without} sec"
|
140
|
+
|
141
|
+
assert without < with
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def test_consistent_hashing
|
146
|
+
requirement(self.respond_to?(:flexmock), 'Flexmock is required to run this test') do
|
147
|
+
|
148
|
+
flexmock(MemCacheDb::Server).new_instances.should_receive(:alive?).and_return(true)
|
149
|
+
|
150
|
+
# Setup a continuum of two servers
|
151
|
+
@cache.servers = ['mike1', 'mike2', 'mike3']
|
152
|
+
|
153
|
+
keys = []
|
154
|
+
1000.times do |idx|
|
155
|
+
keys << idx.to_s
|
156
|
+
end
|
157
|
+
|
158
|
+
before_continuum = keys.map {|key| @cache.get_server_for_key(key) }
|
159
|
+
|
160
|
+
@cache.servers = ['mike1', 'mike2', 'mike3', 'mike4']
|
161
|
+
|
162
|
+
after_continuum = keys.map {|key| @cache.get_server_for_key(key) }
|
163
|
+
|
164
|
+
same_count = before_continuum.zip(after_continuum).find_all {|a| a[0].host == a[1].host }.size
|
165
|
+
|
166
|
+
# With continuum, we should see about 75% of the keys map to the same server
|
167
|
+
# With modulo, we would see about 25%.
|
168
|
+
assert same_count > 700
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def test_get_multi_with_server_failure
|
173
|
+
@cache = MemCacheDb.new 'localhost:1', :namespace => 'my_namespace', :logger => nil #Logger.new(STDOUT)
|
174
|
+
s1 = FakeServerDb.new
|
175
|
+
s2 = FakeServerDb.new
|
176
|
+
|
177
|
+
# Write two messages to the socket to test failover
|
178
|
+
s1.socket.data.write "VALUE my_namespace:a 0 14\r\n\004\b\"\0170123456789\r\nEND\r\n"
|
179
|
+
s1.socket.data.rewind
|
180
|
+
s2.socket.data.write "bogus response\r\nbogus response\r\n"
|
181
|
+
s2.socket.data.rewind
|
182
|
+
|
183
|
+
@cache.servers = [s1, s2]
|
184
|
+
|
185
|
+
assert s1.alive?
|
186
|
+
assert s2.alive?
|
187
|
+
# a maps to s1, the rest map to s2
|
188
|
+
value = @cache.get_multi(['foo', 'bar', 'a', 'b', 'c'])
|
189
|
+
assert_equal({'a'=>'0123456789'}, value)
|
190
|
+
assert s1.alive?
|
191
|
+
assert !s2.alive?
|
192
|
+
end
|
193
|
+
|
194
|
+
def test_cache_get_with_failover
|
195
|
+
@cache = MemCacheDb.new 'localhost:1', :namespace => 'my_namespace', :logger => nil#Logger.new(STDOUT)
|
196
|
+
s1 = FakeServerDb.new
|
197
|
+
s2 = FakeServerDb.new
|
198
|
+
|
199
|
+
# Write two messages to the socket to test failover
|
200
|
+
s1.socket.data.write "VALUE foo 0 14\r\n\004\b\"\0170123456789\r\n"
|
201
|
+
s1.socket.data.rewind
|
202
|
+
s2.socket.data.write "bogus response\r\nbogus response\r\n"
|
203
|
+
s2.socket.data.rewind
|
204
|
+
|
205
|
+
@cache.instance_variable_set(:@failover, true)
|
206
|
+
@cache.servers = [s1, s2]
|
207
|
+
|
208
|
+
assert s1.alive?
|
209
|
+
assert s2.alive?
|
210
|
+
@cache.get('foo')
|
211
|
+
assert s1.alive?
|
212
|
+
assert !s2.alive?
|
213
|
+
end
|
214
|
+
|
215
|
+
def test_cache_get_without_failover
|
216
|
+
s1 = FakeServerDb.new
|
217
|
+
s2 = FakeServerDb.new
|
218
|
+
|
219
|
+
s1.socket.data.write "VALUE foo 0 14\r\n\004\b\"\0170123456789\r\n"
|
220
|
+
s1.socket.data.rewind
|
221
|
+
s2.socket.data.write "bogus response\r\nbogus response\r\n"
|
222
|
+
s2.socket.data.rewind
|
223
|
+
|
224
|
+
@cache.instance_variable_set(:@failover, false)
|
225
|
+
@cache.servers = [s1, s2]
|
226
|
+
|
227
|
+
assert s1.alive?
|
228
|
+
assert s2.alive?
|
229
|
+
e = assert_raise MemCacheDb::MemCacheDbError do
|
230
|
+
@cache.get('foo')
|
231
|
+
end
|
232
|
+
assert s1.alive?
|
233
|
+
assert !s2.alive?
|
234
|
+
|
235
|
+
assert_equal "No servers available", e.message
|
236
|
+
end
|
237
|
+
|
238
|
+
def test_cache_get
|
239
|
+
server = util_setup_fake_server
|
240
|
+
|
241
|
+
assert_equal "\004\b\"\0170123456789",
|
242
|
+
@cache.cache_get(server, 'my_namespace:key')
|
243
|
+
|
244
|
+
assert_equal "get my_namespace:key\r\n",
|
245
|
+
server.socket.written.string
|
246
|
+
end
|
247
|
+
|
248
|
+
def test_cache_get_EOF
|
249
|
+
server = util_setup_fake_server
|
250
|
+
server.socket.data.string = ''
|
251
|
+
|
252
|
+
e = assert_raise IndexError do
|
253
|
+
@cache.cache_get server, 'my_namespace:key'
|
254
|
+
end
|
255
|
+
|
256
|
+
assert_equal "No connection to server (NOT CONNECTED)", e.message
|
257
|
+
end
|
258
|
+
|
259
|
+
def test_cache_get_bad_state
|
260
|
+
server = FakeServerDb.new
|
261
|
+
|
262
|
+
# Write two messages to the socket to test failover
|
263
|
+
server.socket.data.write "bogus response\r\nbogus response\r\n"
|
264
|
+
server.socket.data.rewind
|
265
|
+
|
266
|
+
@cache.servers = []
|
267
|
+
@cache.servers << server
|
268
|
+
|
269
|
+
e = assert_raise IndexError do
|
270
|
+
@cache.cache_get(server, 'my_namespace:key')
|
271
|
+
end
|
272
|
+
|
273
|
+
assert_match /#{Regexp.quote 'No connection to server (NOT CONNECTED)'}/, e.message
|
274
|
+
|
275
|
+
assert !server.alive?
|
276
|
+
end
|
277
|
+
|
278
|
+
def test_cache_get_miss
|
279
|
+
socket = FakeSocketDb.new
|
280
|
+
socket.data.write "END\r\n"
|
281
|
+
socket.data.rewind
|
282
|
+
server = FakeServerDb.new socket
|
283
|
+
|
284
|
+
assert_equal nil, @cache.cache_get(server, 'my_namespace:key')
|
285
|
+
|
286
|
+
assert_equal "get my_namespace:key\r\n",
|
287
|
+
socket.written.string
|
288
|
+
end
|
289
|
+
|
290
|
+
def test_cache_get_multi
|
291
|
+
server = util_setup_fake_server
|
292
|
+
server.socket.data.write "VALUE foo 0 7\r\n"
|
293
|
+
server.socket.data.write "\004\b\"\bfoo\r\n"
|
294
|
+
server.socket.data.write "VALUE bar 0 7\r\n"
|
295
|
+
server.socket.data.write "\004\b\"\bbar\r\n"
|
296
|
+
server.socket.data.write "END\r\n"
|
297
|
+
server.socket.data.rewind
|
298
|
+
|
299
|
+
result = @cache.cache_get_multi server, 'foo bar baz'
|
300
|
+
|
301
|
+
assert_equal 2, result.length
|
302
|
+
assert_equal "\004\b\"\bfoo", result['foo']
|
303
|
+
assert_equal "\004\b\"\bbar", result['bar']
|
304
|
+
end
|
305
|
+
|
306
|
+
def test_cache_get_multi_EOF
|
307
|
+
server = util_setup_fake_server
|
308
|
+
server.socket.data.string = ''
|
309
|
+
|
310
|
+
e = assert_raise IndexError do
|
311
|
+
@cache.cache_get_multi server, 'my_namespace:key'
|
312
|
+
end
|
313
|
+
|
314
|
+
assert_equal "No connection to server (NOT CONNECTED)", e.message
|
315
|
+
end
|
316
|
+
|
317
|
+
def test_cache_get_multi_bad_state
|
318
|
+
server = FakeServerDb.new
|
319
|
+
|
320
|
+
# Write two messages to the socket to test failover
|
321
|
+
server.socket.data.write "bogus response\r\nbogus response\r\n"
|
322
|
+
server.socket.data.rewind
|
323
|
+
|
324
|
+
@cache.servers = []
|
325
|
+
@cache.servers << server
|
326
|
+
|
327
|
+
e = assert_raise IndexError do
|
328
|
+
@cache.cache_get_multi server, 'my_namespace:key'
|
329
|
+
end
|
330
|
+
|
331
|
+
assert_match /#{Regexp.quote 'No connection to server (NOT CONNECTED)'}/, e.message
|
332
|
+
|
333
|
+
assert !server.alive?
|
334
|
+
end
|
335
|
+
|
336
|
+
def test_initialize
|
337
|
+
cache = MemCacheDb.new :namespace => 'my_namespace', :readonly => true
|
338
|
+
|
339
|
+
assert_equal 'my_namespace', cache.namespace
|
340
|
+
assert_equal true, cache.readonly?
|
341
|
+
assert_equal true, cache.servers.empty?
|
342
|
+
end
|
343
|
+
|
344
|
+
def test_initialize_compatible
|
345
|
+
cache = MemCacheDb.new ['localhost:21201', 'localhost:11212'],
|
346
|
+
:namespace => 'my_namespace', :readonly => true
|
347
|
+
|
348
|
+
assert_equal 'my_namespace', cache.namespace
|
349
|
+
assert_equal true, cache.readonly?
|
350
|
+
assert_equal false, cache.servers.empty?
|
351
|
+
end
|
352
|
+
|
353
|
+
def test_initialize_compatible_no_hash
|
354
|
+
cache = MemCacheDb.new ['localhost:21201', 'localhost:11212']
|
355
|
+
|
356
|
+
assert_equal nil, cache.namespace
|
357
|
+
assert_equal false, cache.readonly?
|
358
|
+
assert_equal false, cache.servers.empty?
|
359
|
+
end
|
360
|
+
|
361
|
+
def test_initialize_compatible_one_server
|
362
|
+
cache = MemCacheDb.new 'localhost:21201'
|
363
|
+
|
364
|
+
assert_equal nil, cache.namespace
|
365
|
+
assert_equal false, cache.readonly?
|
366
|
+
assert_equal false, cache.servers.empty?
|
367
|
+
end
|
368
|
+
|
369
|
+
def test_initialize_compatible_bad_arg
|
370
|
+
e = assert_raise ArgumentError do
|
371
|
+
cache = MemCacheDb.new Object.new
|
372
|
+
end
|
373
|
+
|
374
|
+
assert_equal 'first argument must be Array, Hash or String', e.message
|
375
|
+
end
|
376
|
+
|
377
|
+
def test_initialize_multiple_servers
|
378
|
+
cache = MemCacheDb.new %w[localhost:21201 localhost:11212],
|
379
|
+
:namespace => 'my_namespace', :readonly => true
|
380
|
+
|
381
|
+
assert_equal 'my_namespace', cache.namespace
|
382
|
+
assert_equal true, cache.readonly?
|
383
|
+
assert_equal false, cache.servers.empty?
|
384
|
+
assert !cache.instance_variable_get(:@continuum).empty?
|
385
|
+
end
|
386
|
+
|
387
|
+
def test_initialize_too_many_args
|
388
|
+
assert_raises ArgumentError do
|
389
|
+
MemCacheDb.new 1, 2, 3
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
def test_decr
|
394
|
+
server = FakeServerDb.new
|
395
|
+
server.socket.data.write "5\r\n"
|
396
|
+
server.socket.data.rewind
|
397
|
+
|
398
|
+
@cache.servers = []
|
399
|
+
@cache.servers << server
|
400
|
+
|
401
|
+
value = @cache.decr 'key'
|
402
|
+
|
403
|
+
assert_equal "decr my_namespace:key 1\r\n",
|
404
|
+
@cache.servers.first.socket.written.string
|
405
|
+
|
406
|
+
assert_equal 5, value
|
407
|
+
end
|
408
|
+
|
409
|
+
def test_decr_not_found
|
410
|
+
server = FakeServerDb.new
|
411
|
+
server.socket.data.write "NOT_FOUND\r\n"
|
412
|
+
server.socket.data.rewind
|
413
|
+
|
414
|
+
@cache.servers = []
|
415
|
+
@cache.servers << server
|
416
|
+
|
417
|
+
value = @cache.decr 'key'
|
418
|
+
|
419
|
+
assert_equal "decr my_namespace:key 1\r\n",
|
420
|
+
@cache.servers.first.socket.written.string
|
421
|
+
|
422
|
+
assert_equal nil, value
|
423
|
+
end
|
424
|
+
|
425
|
+
def test_decr_space_padding
|
426
|
+
server = FakeServerDb.new
|
427
|
+
server.socket.data.write "5 \r\n"
|
428
|
+
server.socket.data.rewind
|
429
|
+
|
430
|
+
@cache.servers = []
|
431
|
+
@cache.servers << server
|
432
|
+
|
433
|
+
value = @cache.decr 'key'
|
434
|
+
|
435
|
+
assert_equal "decr my_namespace:key 1\r\n",
|
436
|
+
@cache.servers.first.socket.written.string
|
437
|
+
|
438
|
+
assert_equal 5, value
|
439
|
+
end
|
440
|
+
|
441
|
+
def test_get
|
442
|
+
util_setup_fake_server
|
443
|
+
|
444
|
+
value = @cache.get 'key'
|
445
|
+
|
446
|
+
assert_equal "get my_namespace:key\r\n",
|
447
|
+
@cache.servers.first.socket.written.string
|
448
|
+
|
449
|
+
assert_equal '0123456789', value
|
450
|
+
end
|
451
|
+
|
452
|
+
def test_get_bad_key
|
453
|
+
util_setup_fake_server
|
454
|
+
assert_raise ArgumentError do @cache.get 'k y' end
|
455
|
+
|
456
|
+
util_setup_fake_server
|
457
|
+
assert_raise ArgumentError do @cache.get 'k' * 250 end
|
458
|
+
end
|
459
|
+
|
460
|
+
def test_get_cache_get_IOError
|
461
|
+
socket = Object.new
|
462
|
+
def socket.write(arg) raise IOError, 'some io error'; end
|
463
|
+
server = FakeServerDb.new socket
|
464
|
+
|
465
|
+
@cache.servers = []
|
466
|
+
@cache.servers << server
|
467
|
+
|
468
|
+
e = assert_raise MemCacheDb::MemCacheDbError do
|
469
|
+
@cache.get 'my_namespace:key'
|
470
|
+
end
|
471
|
+
|
472
|
+
assert_equal 'some io error', e.message
|
473
|
+
end
|
474
|
+
|
475
|
+
def test_get_cache_get_SystemCallError
|
476
|
+
socket = Object.new
|
477
|
+
def socket.write(arg) raise SystemCallError, 'some syscall error'; end
|
478
|
+
server = FakeServerDb.new socket
|
479
|
+
|
480
|
+
@cache.servers = []
|
481
|
+
@cache.servers << server
|
482
|
+
|
483
|
+
e = assert_raise MemCacheDb::MemCacheDbError do
|
484
|
+
@cache.get 'my_namespace:key'
|
485
|
+
end
|
486
|
+
|
487
|
+
assert_equal 'unknown error - some syscall error', e.message
|
488
|
+
end
|
489
|
+
|
490
|
+
def test_get_no_connection
|
491
|
+
@cache.servers = 'localhost:1'
|
492
|
+
e = assert_raise MemCacheDb::MemCacheDbError do
|
493
|
+
@cache.get 'key'
|
494
|
+
end
|
495
|
+
|
496
|
+
assert_match /^No connection to server/, e.message
|
497
|
+
end
|
498
|
+
|
499
|
+
def test_get_no_servers
|
500
|
+
@cache.servers = []
|
501
|
+
e = assert_raise MemCacheDb::MemCacheDbError do
|
502
|
+
@cache.get 'key'
|
503
|
+
end
|
504
|
+
|
505
|
+
assert_equal 'No active servers', e.message
|
506
|
+
end
|
507
|
+
|
508
|
+
def test_get_multi
|
509
|
+
server = FakeServerDb.new
|
510
|
+
server.socket.data.write "VALUE my_namespace:key 0 14\r\n"
|
511
|
+
server.socket.data.write "\004\b\"\0170123456789\r\n"
|
512
|
+
server.socket.data.write "VALUE my_namespace:keyb 0 14\r\n"
|
513
|
+
server.socket.data.write "\004\b\"\0179876543210\r\n"
|
514
|
+
server.socket.data.write "END\r\n"
|
515
|
+
server.socket.data.rewind
|
516
|
+
|
517
|
+
@cache.servers = []
|
518
|
+
@cache.servers << server
|
519
|
+
|
520
|
+
values = @cache.get_multi 'key', 'keyb'
|
521
|
+
|
522
|
+
assert_equal "get my_namespace:key my_namespace:keyb\r\n",
|
523
|
+
server.socket.written.string
|
524
|
+
|
525
|
+
expected = { 'key' => '0123456789', 'keyb' => '9876543210' }
|
526
|
+
|
527
|
+
assert_equal expected.sort, values.sort
|
528
|
+
end
|
529
|
+
|
530
|
+
def test_get_raw
|
531
|
+
server = FakeServerDb.new
|
532
|
+
server.socket.data.write "VALUE my_namespace:key 0 10\r\n"
|
533
|
+
server.socket.data.write "0123456789\r\n"
|
534
|
+
server.socket.data.write "END\r\n"
|
535
|
+
server.socket.data.rewind
|
536
|
+
|
537
|
+
@cache.servers = []
|
538
|
+
@cache.servers << server
|
539
|
+
|
540
|
+
|
541
|
+
value = @cache.get 'key', true
|
542
|
+
|
543
|
+
assert_equal "get my_namespace:key\r\n",
|
544
|
+
@cache.servers.first.socket.written.string
|
545
|
+
|
546
|
+
assert_equal '0123456789', value
|
547
|
+
end
|
548
|
+
|
549
|
+
def test_get_server_for_key
|
550
|
+
server = @cache.get_server_for_key 'key'
|
551
|
+
assert_equal 'localhost', server.host
|
552
|
+
assert_equal 1, server.port
|
553
|
+
end
|
554
|
+
|
555
|
+
def test_get_server_for_key_multiple
|
556
|
+
s1 = util_setup_server @cache, 'one.example.com', ''
|
557
|
+
s2 = util_setup_server @cache, 'two.example.com', ''
|
558
|
+
@cache.servers = [s1, s2]
|
559
|
+
|
560
|
+
server = @cache.get_server_for_key 'keya'
|
561
|
+
assert_equal 'two.example.com', server.host
|
562
|
+
server = @cache.get_server_for_key 'keyb'
|
563
|
+
assert_equal 'two.example.com', server.host
|
564
|
+
server = @cache.get_server_for_key 'keyc'
|
565
|
+
assert_equal 'two.example.com', server.host
|
566
|
+
server = @cache.get_server_for_key 'keyd'
|
567
|
+
assert_equal 'one.example.com', server.host
|
568
|
+
end
|
569
|
+
|
570
|
+
def test_get_server_for_key_no_servers
|
571
|
+
@cache.servers = []
|
572
|
+
|
573
|
+
e = assert_raise MemCacheDb::MemCacheDbError do
|
574
|
+
@cache.get_server_for_key 'key'
|
575
|
+
end
|
576
|
+
|
577
|
+
assert_equal 'No servers available', e.message
|
578
|
+
end
|
579
|
+
|
580
|
+
def test_get_server_for_key_spaces
|
581
|
+
e = assert_raise ArgumentError do
|
582
|
+
@cache.get_server_for_key 'space key'
|
583
|
+
end
|
584
|
+
assert_equal 'illegal character in key "space key"', e.message
|
585
|
+
end
|
586
|
+
|
587
|
+
def test_get_server_for_key_length
|
588
|
+
@cache.get_server_for_key 'x' * 250
|
589
|
+
long_key = 'x' * 251
|
590
|
+
e = assert_raise ArgumentError do
|
591
|
+
@cache.get_server_for_key long_key
|
592
|
+
end
|
593
|
+
assert_equal "key too long #{long_key.inspect}", e.message
|
594
|
+
end
|
595
|
+
|
596
|
+
def test_incr
|
597
|
+
server = FakeServerDb.new
|
598
|
+
server.socket.data.write "5\r\n"
|
599
|
+
server.socket.data.rewind
|
600
|
+
|
601
|
+
@cache.servers = []
|
602
|
+
@cache.servers << server
|
603
|
+
|
604
|
+
value = @cache.incr 'key'
|
605
|
+
|
606
|
+
assert_equal "incr my_namespace:key 1\r\n",
|
607
|
+
@cache.servers.first.socket.written.string
|
608
|
+
|
609
|
+
assert_equal 5, value
|
610
|
+
end
|
611
|
+
|
612
|
+
def test_incr_not_found
|
613
|
+
server = FakeServerDb.new
|
614
|
+
server.socket.data.write "NOT_FOUND\r\n"
|
615
|
+
server.socket.data.rewind
|
616
|
+
|
617
|
+
@cache.servers = []
|
618
|
+
@cache.servers << server
|
619
|
+
|
620
|
+
value = @cache.incr 'key'
|
621
|
+
|
622
|
+
assert_equal "incr my_namespace:key 1\r\n",
|
623
|
+
@cache.servers.first.socket.written.string
|
624
|
+
|
625
|
+
assert_equal nil, value
|
626
|
+
end
|
627
|
+
|
628
|
+
def test_incr_space_padding
|
629
|
+
server = FakeServerDb.new
|
630
|
+
server.socket.data.write "5 \r\n"
|
631
|
+
server.socket.data.rewind
|
632
|
+
|
633
|
+
@cache.servers = []
|
634
|
+
@cache.servers << server
|
635
|
+
|
636
|
+
value = @cache.incr 'key'
|
637
|
+
|
638
|
+
assert_equal "incr my_namespace:key 1\r\n",
|
639
|
+
@cache.servers.first.socket.written.string
|
640
|
+
|
641
|
+
assert_equal 5, value
|
642
|
+
end
|
643
|
+
|
644
|
+
def test_make_cache_key
|
645
|
+
assert_equal 'my_namespace:key', @cache.make_cache_key('key')
|
646
|
+
@cache.namespace = nil
|
647
|
+
assert_equal 'key', @cache.make_cache_key('key')
|
648
|
+
end
|
649
|
+
|
650
|
+
def test_servers
|
651
|
+
server = FakeServerDb.new
|
652
|
+
@cache.servers = []
|
653
|
+
@cache.servers << server
|
654
|
+
assert_equal [server], @cache.servers
|
655
|
+
end
|
656
|
+
|
657
|
+
def test_set
|
658
|
+
server = FakeServerDb.new
|
659
|
+
server.socket.data.write "STORED\r\n"
|
660
|
+
server.socket.data.rewind
|
661
|
+
@cache.servers = []
|
662
|
+
@cache.servers << server
|
663
|
+
|
664
|
+
@cache.set 'key', 'value'
|
665
|
+
|
666
|
+
dumped = Marshal.dump('value')
|
667
|
+
expected = "set my_namespace:key 0 0 #{dumped.length}\r\n#{dumped}\r\n"
|
668
|
+
# expected = "set my_namespace:key 0 0 9\r\n\004\b\"\nvalue\r\n"
|
669
|
+
assert_equal expected, server.socket.written.string
|
670
|
+
end
|
671
|
+
|
672
|
+
def test_set_expiry
|
673
|
+
server = FakeServerDb.new
|
674
|
+
server.socket.data.write "STORED\r\n"
|
675
|
+
server.socket.data.rewind
|
676
|
+
@cache.servers = []
|
677
|
+
@cache.servers << server
|
678
|
+
|
679
|
+
@cache.set 'key', 'value', 5
|
680
|
+
|
681
|
+
dumped = Marshal.dump('value')
|
682
|
+
expected = "set my_namespace:key 0 5 #{dumped.length}\r\n#{dumped}\r\n"
|
683
|
+
assert_equal expected, server.socket.written.string
|
684
|
+
end
|
685
|
+
|
686
|
+
def test_set_raw
|
687
|
+
server = FakeServerDb.new
|
688
|
+
server.socket.data.write "STORED\r\n"
|
689
|
+
server.socket.data.rewind
|
690
|
+
@cache.servers = []
|
691
|
+
@cache.servers << server
|
692
|
+
|
693
|
+
@cache.set 'key', 'value', 0, true
|
694
|
+
|
695
|
+
expected = "set my_namespace:key 0 0 5\r\nvalue\r\n"
|
696
|
+
assert_equal expected, server.socket.written.string
|
697
|
+
end
|
698
|
+
|
699
|
+
def test_set_readonly
|
700
|
+
cache = MemCacheDb.new :readonly => true
|
701
|
+
|
702
|
+
e = assert_raise MemCacheDb::MemCacheDbError do
|
703
|
+
cache.set 'key', 'value'
|
704
|
+
end
|
705
|
+
|
706
|
+
assert_equal 'Update of readonly cache', e.message
|
707
|
+
end
|
708
|
+
|
709
|
+
def test_set_too_big
|
710
|
+
server = FakeServerDb.new
|
711
|
+
|
712
|
+
# Write two messages to the socket to test failover
|
713
|
+
server.socket.data.write "SERVER_ERROR\r\nSERVER_ERROR object too large for cache\r\n"
|
714
|
+
server.socket.data.rewind
|
715
|
+
|
716
|
+
@cache.servers = []
|
717
|
+
@cache.servers << server
|
718
|
+
|
719
|
+
e = assert_raise MemCacheDb::MemCacheDbError do
|
720
|
+
@cache.set 'key', 'v'
|
721
|
+
end
|
722
|
+
|
723
|
+
assert_match /object too large for cache/, e.message
|
724
|
+
end
|
725
|
+
|
726
|
+
def test_add
|
727
|
+
server = FakeServerDb.new
|
728
|
+
server.socket.data.write "STORED\r\n"
|
729
|
+
server.socket.data.rewind
|
730
|
+
@cache.servers = []
|
731
|
+
@cache.servers << server
|
732
|
+
|
733
|
+
@cache.add 'key', 'value'
|
734
|
+
|
735
|
+
dumped = Marshal.dump('value')
|
736
|
+
|
737
|
+
expected = "add my_namespace:key 0 0 #{dumped.length}\r\n#{dumped}\r\n"
|
738
|
+
assert_equal expected, server.socket.written.string
|
739
|
+
end
|
740
|
+
|
741
|
+
def test_add_exists
|
742
|
+
server = FakeServerDb.new
|
743
|
+
server.socket.data.write "NOT_STORED\r\n"
|
744
|
+
server.socket.data.rewind
|
745
|
+
@cache.servers = []
|
746
|
+
@cache.servers << server
|
747
|
+
|
748
|
+
@cache.add 'key', 'value'
|
749
|
+
|
750
|
+
dumped = Marshal.dump('value')
|
751
|
+
expected = "add my_namespace:key 0 0 #{dumped.length}\r\n#{dumped}\r\n"
|
752
|
+
assert_equal expected, server.socket.written.string
|
753
|
+
end
|
754
|
+
|
755
|
+
def test_add_expiry
|
756
|
+
server = FakeServerDb.new
|
757
|
+
server.socket.data.write "STORED\r\n"
|
758
|
+
server.socket.data.rewind
|
759
|
+
@cache.servers = []
|
760
|
+
@cache.servers << server
|
761
|
+
|
762
|
+
@cache.add 'key', 'value', 5
|
763
|
+
|
764
|
+
dumped = Marshal.dump('value')
|
765
|
+
expected = "add my_namespace:key 0 5 #{dumped.length}\r\n#{dumped}\r\n"
|
766
|
+
assert_equal expected, server.socket.written.string
|
767
|
+
end
|
768
|
+
|
769
|
+
def test_add_raw
|
770
|
+
server = FakeServerDb.new
|
771
|
+
server.socket.data.write "STORED\r\n"
|
772
|
+
server.socket.data.rewind
|
773
|
+
@cache.servers = []
|
774
|
+
@cache.servers << server
|
775
|
+
|
776
|
+
@cache.add 'key', 'value', 0, true
|
777
|
+
|
778
|
+
expected = "add my_namespace:key 0 0 5\r\nvalue\r\n"
|
779
|
+
assert_equal expected, server.socket.written.string
|
780
|
+
end
|
781
|
+
|
782
|
+
def test_add_raw_int
|
783
|
+
server = FakeServerDb.new
|
784
|
+
server.socket.data.write "STORED\r\n"
|
785
|
+
server.socket.data.rewind
|
786
|
+
@cache.servers = []
|
787
|
+
@cache.servers << server
|
788
|
+
|
789
|
+
@cache.add 'key', 12, 0, true
|
790
|
+
|
791
|
+
expected = "add my_namespace:key 0 0 2\r\n12\r\n"
|
792
|
+
assert_equal expected, server.socket.written.string
|
793
|
+
end
|
794
|
+
|
795
|
+
def test_add_readonly
|
796
|
+
cache = MemCacheDb.new :readonly => true
|
797
|
+
|
798
|
+
e = assert_raise MemCacheDb::MemCacheDbError do
|
799
|
+
cache.add 'key', 'value'
|
800
|
+
end
|
801
|
+
|
802
|
+
assert_equal 'Update of readonly cache', e.message
|
803
|
+
end
|
804
|
+
|
805
|
+
def test_delete
|
806
|
+
server = FakeServerDb.new
|
807
|
+
@cache.servers = []
|
808
|
+
@cache.servers << server
|
809
|
+
|
810
|
+
@cache.delete 'key'
|
811
|
+
|
812
|
+
expected = "delete my_namespace:key 0\r\n"
|
813
|
+
assert_equal expected, server.socket.written.string
|
814
|
+
end
|
815
|
+
|
816
|
+
def test_delete_with_expiry
|
817
|
+
server = FakeServerDb.new
|
818
|
+
@cache.servers = []
|
819
|
+
@cache.servers << server
|
820
|
+
|
821
|
+
@cache.delete 'key', 300
|
822
|
+
|
823
|
+
expected = "delete my_namespace:key 300\r\n"
|
824
|
+
assert_equal expected, server.socket.written.string
|
825
|
+
end
|
826
|
+
|
827
|
+
def test_flush_all
|
828
|
+
@cache.servers = []
|
829
|
+
3.times { @cache.servers << FakeServerDb.new }
|
830
|
+
|
831
|
+
@cache.flush_all
|
832
|
+
|
833
|
+
expected = "flush_all\r\n"
|
834
|
+
@cache.servers.each do |server|
|
835
|
+
assert_equal expected, server.socket.written.string
|
836
|
+
end
|
837
|
+
end
|
838
|
+
|
839
|
+
def test_flush_all_failure
|
840
|
+
socket = FakeSocketDb.new
|
841
|
+
|
842
|
+
# Write two messages to the socket to test failover
|
843
|
+
socket.data.write "ERROR\r\nERROR\r\n"
|
844
|
+
socket.data.rewind
|
845
|
+
|
846
|
+
server = FakeServerDb.new socket
|
847
|
+
|
848
|
+
@cache.servers = []
|
849
|
+
@cache.servers << server
|
850
|
+
|
851
|
+
assert_raise MemCacheDb::MemCacheDbError do
|
852
|
+
@cache.flush_all
|
853
|
+
end
|
854
|
+
|
855
|
+
assert_match /flush_all\r\n/, socket.written.string
|
856
|
+
end
|
857
|
+
|
858
|
+
def test_stats
|
859
|
+
socket = FakeSocketDb.new
|
860
|
+
socket.data.write "STAT pid 20188\r\nSTAT total_items 32\r\nSTAT version 1.2.3\r\nSTAT rusage_user 1:300\r\nSTAT dummy ok\r\nEND\r\n"
|
861
|
+
socket.data.rewind
|
862
|
+
server = FakeServerDb.new socket
|
863
|
+
def server.host() 'localhost'; end
|
864
|
+
def server.port() 11211; end
|
865
|
+
|
866
|
+
@cache.servers = []
|
867
|
+
@cache.servers << server
|
868
|
+
|
869
|
+
expected = {
|
870
|
+
'localhost:21201' => {
|
871
|
+
'pid' => 20188, 'total_items' => 32, 'version' => '1.2.3',
|
872
|
+
'rusage_user' => 1.0003, 'dummy' => 'ok'
|
873
|
+
}
|
874
|
+
}
|
875
|
+
assert_equal expected, @cache.stats
|
876
|
+
|
877
|
+
assert_equal "stats\r\n", socket.written.string
|
878
|
+
end
|
879
|
+
|
880
|
+
def test_basic_threaded_operations_should_work
|
881
|
+
cache = MemCacheDb.new :multithread => true,
|
882
|
+
:namespace => 'my_namespace',
|
883
|
+
:readonly => false
|
884
|
+
|
885
|
+
server = FakeServerDb.new
|
886
|
+
server.socket.data.write "STORED\r\n"
|
887
|
+
server.socket.data.rewind
|
888
|
+
|
889
|
+
cache.servers = []
|
890
|
+
cache.servers << server
|
891
|
+
|
892
|
+
assert cache.multithread
|
893
|
+
|
894
|
+
assert_nothing_raised do
|
895
|
+
cache.set "test", "test value"
|
896
|
+
end
|
897
|
+
|
898
|
+
output = server.socket.written.string
|
899
|
+
assert_match /set my_namespace:test/, output
|
900
|
+
assert_match /test value/, output
|
901
|
+
end
|
902
|
+
|
903
|
+
def test_basic_unthreaded_operations_should_work
|
904
|
+
cache = MemCacheDb.new :multithread => false,
|
905
|
+
:namespace => 'my_namespace',
|
906
|
+
:readonly => false
|
907
|
+
|
908
|
+
server = FakeServerDb.new
|
909
|
+
server.socket.data.write "STORED\r\n"
|
910
|
+
server.socket.data.rewind
|
911
|
+
|
912
|
+
cache.servers = []
|
913
|
+
cache.servers << server
|
914
|
+
|
915
|
+
assert !cache.multithread
|
916
|
+
|
917
|
+
assert_nothing_raised do
|
918
|
+
cache.set "test", "test value"
|
919
|
+
end
|
920
|
+
|
921
|
+
output = server.socket.written.string
|
922
|
+
assert_match /set my_namespace:test/, output
|
923
|
+
assert_match /test value/, output
|
924
|
+
end
|
925
|
+
|
926
|
+
def util_setup_fake_server
|
927
|
+
server = FakeServerDb.new
|
928
|
+
server.socket.data.write "VALUE my_namespace:key 0 14\r\n"
|
929
|
+
server.socket.data.write "\004\b\"\0170123456789\r\n"
|
930
|
+
server.socket.data.write "END\r\n"
|
931
|
+
server.socket.data.rewind
|
932
|
+
|
933
|
+
@cache.servers = []
|
934
|
+
@cache.servers << server
|
935
|
+
|
936
|
+
return server
|
937
|
+
end
|
938
|
+
|
939
|
+
def util_setup_server(memcache, host, responses)
|
940
|
+
server = MemCacheDb::Server.new memcache, host
|
941
|
+
server.instance_variable_set :@sock, StringIO.new(responses)
|
942
|
+
|
943
|
+
@cache.servers = []
|
944
|
+
@cache.servers << server
|
945
|
+
|
946
|
+
return server
|
947
|
+
end
|
948
|
+
|
949
|
+
end
|
950
|
+
|