mperham-memcache-client 1.6.0 → 1.6.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +22 -6
- data/README.rdoc +43 -0
- data/Rakefile +4 -0
- data/ext/memcache/binary_search.c +54 -0
- data/ext/memcache/extconf.rb +5 -0
- data/lib/continuum.rb +46 -0
- data/lib/memcache.rb +57 -75
- data/test/test_mem_cache.rb +157 -37
- metadata +6 -4
data/History.txt
CHANGED
@@ -1,21 +1,37 @@
|
|
1
|
-
= 1.6.
|
1
|
+
= 1.6.2 (upcoming)
|
2
2
|
|
3
|
-
* First official release since 1.5.0. Thanks to Eric Hodel for turning the project to me!
|
3
|
+
* First official release since 1.5.0. Thanks to Eric Hodel for turning over the project to me!
|
4
|
+
New project home page: http://github.com/mperham/memcache-client
|
4
5
|
|
5
|
-
|
6
|
-
|
6
|
+
= 1.6.1 (2009-01-28)
|
7
|
+
|
8
|
+
* Add option to disable socket timeout support. Socket timeout has a significant performance
|
9
|
+
penalty (approx 3x slower than without in Ruby 1.8.6). You can turn off the timeouts if you
|
10
|
+
need absolute performance, but by default timeouts are enabled. The performance
|
11
|
+
penalty is much lower in Ruby 1.8.7, 1.9 and JRuby. (mperham)
|
12
|
+
|
13
|
+
* Add option to disable server failover. Failover can lead to "split-brain" caches that
|
14
|
+
return stale data. (mperham)
|
7
15
|
|
8
|
-
*
|
16
|
+
* Implement continuum binary search in native code for performance reasons. Pure ruby
|
17
|
+
is available for platforms like JRuby or Rubinius which can't use C extensions. (mperham)
|
18
|
+
|
19
|
+
* Fix #add with raw=true (iamaleksey)
|
20
|
+
|
21
|
+
= 1.6.0
|
9
22
|
|
10
23
|
* Implement a consistent hashing algorithm, as described in libketama.
|
11
24
|
This dramatically reduces the cost of adding or removing servers dynamically
|
12
25
|
as keys are much more likely to map to the same server.
|
13
26
|
|
14
|
-
Take a scenario where we add a fourth server. With a
|
27
|
+
Take a scenario where we add a fourth server. With a naive modulo algorithm, about
|
15
28
|
25% of the keys will map to the same server. In other words, 75% of your memcached
|
16
29
|
content suddenly becomes invalid. With a consistent algorithm, 75% of the keys
|
17
30
|
will map to the same server as before - only 25% will be invalidated. (mperham)
|
18
31
|
|
32
|
+
* Implement socket timeouts, should fix rare cases of very bad things happening
|
33
|
+
in production at 37signals and FiveRuns. (jseirles)
|
34
|
+
|
19
35
|
= 1.5.0.5
|
20
36
|
|
21
37
|
* Remove native C CRC32_ITU_T extension in favor of Zlib's crc32 method.
|
data/README.rdoc
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
= memcache-client
|
2
|
+
|
3
|
+
A pure ruby library for accessing memcached.
|
4
|
+
|
5
|
+
Source:
|
6
|
+
|
7
|
+
http://github.com/mperham/memcache-client
|
8
|
+
|
9
|
+
== Installing memcache-client
|
10
|
+
|
11
|
+
Just install the gem:
|
12
|
+
|
13
|
+
$ sudo gem install memcache-client
|
14
|
+
|
15
|
+
== Using memcache-client
|
16
|
+
|
17
|
+
With one server:
|
18
|
+
|
19
|
+
CACHE = MemCache.new 'localhost:11211', :namespace => 'my_namespace'
|
20
|
+
|
21
|
+
Or with multiple servers:
|
22
|
+
|
23
|
+
CACHE = MemCache.new %w[one.example.com:11211 two.example.com:11211],
|
24
|
+
:namespace => 'my_namespace'
|
25
|
+
|
26
|
+
See MemCache.new for details. Please note memcache-client is not thread-safe
|
27
|
+
by default. You should create a separate instance for each thread in your
|
28
|
+
process.
|
29
|
+
|
30
|
+
== Using memcache-client with Rails
|
31
|
+
|
32
|
+
There's no need to use memcache-client in a Rails application. Rails 2.1+ includes
|
33
|
+
a basic caching library which can be used with memcached. See ActiveSupport::Cache::Store
|
34
|
+
for more details.
|
35
|
+
|
36
|
+
== Questions?
|
37
|
+
|
38
|
+
memcache-client is maintained by Mike Perham and was originally written by Bob Cottrell,
|
39
|
+
Eric Hodel and the seattle.rb crew.
|
40
|
+
|
41
|
+
Email:: mailto:mperham@gmail.com
|
42
|
+
Twitter:: mperham[http://twitter.com/mperham]
|
43
|
+
WWW:: http://mikeperham.com
|
data/Rakefile
CHANGED
@@ -0,0 +1,54 @@
|
|
1
|
+
#include "ruby.h"
|
2
|
+
#include "stdio.h"
|
3
|
+
|
4
|
+
/*
|
5
|
+
def binary_search(ary, value)
|
6
|
+
upper = ary.size - 1
|
7
|
+
lower = 0
|
8
|
+
idx = 0
|
9
|
+
|
10
|
+
while(lower <= upper) do
|
11
|
+
idx = (lower + upper) / 2
|
12
|
+
comp = ary[idx].value <=> value
|
13
|
+
|
14
|
+
if comp == 0
|
15
|
+
return idx
|
16
|
+
elsif comp > 0
|
17
|
+
upper = idx - 1
|
18
|
+
else
|
19
|
+
lower = idx + 1
|
20
|
+
end
|
21
|
+
end
|
22
|
+
return upper
|
23
|
+
end
|
24
|
+
*/
|
25
|
+
static VALUE binary_search(VALUE self, VALUE ary, VALUE number) {
|
26
|
+
int upper = RARRAY_LEN(ary) - 1;
|
27
|
+
int lower = 0;
|
28
|
+
int idx = 0;
|
29
|
+
unsigned int r = NUM2UINT(number);
|
30
|
+
ID value = rb_intern("value");
|
31
|
+
|
32
|
+
while (lower <= upper) {
|
33
|
+
idx = (lower + upper) / 2;
|
34
|
+
|
35
|
+
VALUE continuumValue = rb_funcall(RARRAY_PTR(ary)[idx], value, 0);
|
36
|
+
unsigned int l = NUM2UINT(continuumValue);
|
37
|
+
if (l == r) {
|
38
|
+
return INT2FIX(idx);
|
39
|
+
}
|
40
|
+
else if (l > r) {
|
41
|
+
upper = idx - 1;
|
42
|
+
}
|
43
|
+
else {
|
44
|
+
lower = idx + 1;
|
45
|
+
}
|
46
|
+
}
|
47
|
+
return INT2FIX(upper);
|
48
|
+
}
|
49
|
+
|
50
|
+
VALUE cContinuum;
|
51
|
+
void Init_binary_search() {
|
52
|
+
cContinuum = rb_define_module("Continuum");
|
53
|
+
rb_define_module_function(cContinuum, "binary_search", binary_search, 2);
|
54
|
+
}
|
data/lib/continuum.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
module Continuum
|
2
|
+
POINTS_PER_SERVER = 160 # this is the default in libmemcached
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'binary_search' # try to load native extension
|
6
|
+
rescue LoadError => e
|
7
|
+
puts "Unable to load fast binary search, falling back to pure Ruby: #{e.message}"
|
8
|
+
|
9
|
+
# slow but pure ruby version
|
10
|
+
# Find the closest index in Continuum with value <= the given value
|
11
|
+
def self.binary_search(ary, value, &block)
|
12
|
+
upper = ary.size - 1
|
13
|
+
lower = 0
|
14
|
+
idx = 0
|
15
|
+
|
16
|
+
while(lower <= upper) do
|
17
|
+
idx = (lower + upper) / 2
|
18
|
+
comp = ary[idx].value <=> value
|
19
|
+
|
20
|
+
if comp == 0
|
21
|
+
return idx
|
22
|
+
elsif comp > 0
|
23
|
+
upper = idx - 1
|
24
|
+
else
|
25
|
+
lower = idx + 1
|
26
|
+
end
|
27
|
+
end
|
28
|
+
return upper
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
class Entry
|
34
|
+
attr_reader :value
|
35
|
+
attr_reader :server
|
36
|
+
|
37
|
+
def initialize(val, srv)
|
38
|
+
@value = val
|
39
|
+
@server = srv
|
40
|
+
end
|
41
|
+
|
42
|
+
def inspect
|
43
|
+
"<#{value}, #{server.host}:#{server.port}>"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/memcache.rb
CHANGED
@@ -3,10 +3,11 @@ $TESTING = defined?($TESTING) && $TESTING
|
|
3
3
|
require 'socket'
|
4
4
|
require 'thread'
|
5
5
|
require 'timeout'
|
6
|
-
require 'rubygems'
|
7
6
|
require 'zlib'
|
8
7
|
require 'digest/sha1'
|
9
8
|
|
9
|
+
require 'continuum'
|
10
|
+
|
10
11
|
##
|
11
12
|
# A Ruby client library for memcached.
|
12
13
|
#
|
@@ -19,7 +20,7 @@ class MemCache
|
|
19
20
|
##
|
20
21
|
# The version of MemCache you are using.
|
21
22
|
|
22
|
-
VERSION = '1.6.
|
23
|
+
VERSION = '1.6.1'
|
23
24
|
|
24
25
|
##
|
25
26
|
# Default options for the cache object.
|
@@ -28,6 +29,8 @@ class MemCache
|
|
28
29
|
:namespace => nil,
|
29
30
|
:readonly => false,
|
30
31
|
:multithread => false,
|
32
|
+
:failover => true,
|
33
|
+
:timeout => 0.25,
|
31
34
|
}
|
32
35
|
|
33
36
|
##
|
@@ -40,13 +43,6 @@ class MemCache
|
|
40
43
|
|
41
44
|
DEFAULT_WEIGHT = 1
|
42
45
|
|
43
|
-
##
|
44
|
-
# The amount of time to wait for a response from a memcached server. If a
|
45
|
-
# response is not completed within this time, the connection to the server
|
46
|
-
# will be closed and an error will be raised.
|
47
|
-
|
48
|
-
attr_accessor :request_timeout
|
49
|
-
|
50
46
|
##
|
51
47
|
# The namespace for this instance
|
52
48
|
|
@@ -62,6 +58,24 @@ class MemCache
|
|
62
58
|
|
63
59
|
attr_reader :servers
|
64
60
|
|
61
|
+
##
|
62
|
+
# Socket timeout limit with this client, defaults to 0.25 sec.
|
63
|
+
# Set to nil to disable timeouts.
|
64
|
+
|
65
|
+
attr_reader :timeout
|
66
|
+
|
67
|
+
##
|
68
|
+
# Should the client try to failover to another server if the
|
69
|
+
# first server is down? Defaults to true.
|
70
|
+
|
71
|
+
attr_reader :failover
|
72
|
+
|
73
|
+
##
|
74
|
+
# Log errors to the following IO stream, defaults to STDOUT,
|
75
|
+
# set to nil to disable logging.
|
76
|
+
|
77
|
+
attr_accessor :logger
|
78
|
+
|
65
79
|
##
|
66
80
|
# Accepts a list of +servers+ and a list of +opts+. +servers+ may be
|
67
81
|
# omitted. See +servers=+ for acceptable server list arguments.
|
@@ -71,6 +85,10 @@ class MemCache
|
|
71
85
|
# [:namespace] Prepends this value to all keys added or retrieved.
|
72
86
|
# [:readonly] Raises an exception on cache writes when true.
|
73
87
|
# [:multithread] Wraps cache access in a Mutex for thread safety.
|
88
|
+
# [:failover] Should the client try to failover to another server if the
|
89
|
+
# first server is down? Defaults to true.
|
90
|
+
# [:timeout] Time to use as the socket read timeout. Defaults to 0.25 sec,
|
91
|
+
# set to nil to disable timeouts (this is a major performance penalty in Ruby 1.8).
|
74
92
|
#
|
75
93
|
# Other options are ignored.
|
76
94
|
|
@@ -98,7 +116,10 @@ class MemCache
|
|
98
116
|
@namespace = opts[:namespace]
|
99
117
|
@readonly = opts[:readonly]
|
100
118
|
@multithread = opts[:multithread]
|
119
|
+
@timeout = opts[:timeout]
|
120
|
+
@failover = opts[:failover]
|
101
121
|
@mutex = Mutex.new if @multithread
|
122
|
+
@logger = $stdout
|
102
123
|
self.servers = servers
|
103
124
|
end
|
104
125
|
|
@@ -138,13 +159,11 @@ class MemCache
|
|
138
159
|
port ||= DEFAULT_PORT
|
139
160
|
weight ||= DEFAULT_WEIGHT
|
140
161
|
Server.new self, host, port, weight
|
141
|
-
|
162
|
+
else
|
142
163
|
if server.multithread != @multithread then
|
143
164
|
raise ArgumentError, "can't mix threaded and non-threaded servers"
|
144
165
|
end
|
145
166
|
server
|
146
|
-
else
|
147
|
-
raise TypeError, "cannot convert #{server.class} into MemCache::Server"
|
148
167
|
end
|
149
168
|
end
|
150
169
|
|
@@ -284,7 +303,7 @@ class MemCache
|
|
284
303
|
raise MemCacheError, "Update of readonly cache" if @readonly
|
285
304
|
with_server(key) do |server, cache_key|
|
286
305
|
value = Marshal.dump value unless raw
|
287
|
-
command = "add #{cache_key} 0 #{expiry} #{value.size}\r\n#{value}\r\n"
|
306
|
+
command = "add #{cache_key} 0 #{expiry} #{value.to_s.size}\r\n#{value}\r\n"
|
288
307
|
|
289
308
|
with_socket_management(server) do |socket|
|
290
309
|
socket.write command
|
@@ -389,7 +408,7 @@ class MemCache
|
|
389
408
|
while line = socket.gets do
|
390
409
|
raise_on_error_response! line
|
391
410
|
break if line == "END\r\n"
|
392
|
-
if line =~ /\ASTAT ([\
|
411
|
+
if line =~ /\ASTAT ([\S]+) ([\w\.\:]+)/ then
|
393
412
|
name, value = $1, $2
|
394
413
|
stats[name] = case name
|
395
414
|
when 'version'
|
@@ -463,8 +482,10 @@ class MemCache
|
|
463
482
|
hkey = hash_for(key)
|
464
483
|
|
465
484
|
20.times do |try|
|
466
|
-
|
485
|
+
entryidx = Continuum.binary_search(@continuum, hkey)
|
486
|
+
server = @continuum[entryidx].server
|
467
487
|
return server if server.alive?
|
488
|
+
break unless failover
|
468
489
|
hkey = hash_for "#{try}#{key}"
|
469
490
|
end
|
470
491
|
|
@@ -571,22 +592,22 @@ class MemCache
|
|
571
592
|
def with_socket_management(server, &block)
|
572
593
|
@mutex.lock if @multithread
|
573
594
|
retried = false
|
574
|
-
|
595
|
+
|
575
596
|
begin
|
576
597
|
socket = server.socket
|
577
598
|
|
578
599
|
# Raise an IndexError to show this server is out of whack. If were inside
|
579
600
|
# a with_server block, we'll catch it and attempt to restart the operation.
|
580
|
-
|
601
|
+
|
581
602
|
raise IndexError, "No connection to server (#{server.status})" if socket.nil?
|
582
|
-
|
603
|
+
|
583
604
|
block.call(socket)
|
584
|
-
|
605
|
+
|
585
606
|
rescue SocketError => err
|
586
607
|
server.mark_dead(err.message)
|
587
608
|
handle_error(server, err)
|
588
609
|
|
589
|
-
rescue MemCacheError,
|
610
|
+
rescue MemCacheError, SystemCallError, IOError => err
|
590
611
|
handle_error(server, err) if retried || socket.nil?
|
591
612
|
retried = true
|
592
613
|
retry
|
@@ -602,7 +623,7 @@ class MemCache
|
|
602
623
|
yield server, cache_key
|
603
624
|
rescue IndexError => e
|
604
625
|
if !retried && @servers.size > 1
|
605
|
-
puts "Connection to server #{server.inspect} DIED! Retrying operation..."
|
626
|
+
logger.puts "Connection to server #{server.inspect} DIED! Retrying operation..." if logger
|
606
627
|
retried = true
|
607
628
|
retry
|
608
629
|
end
|
@@ -646,7 +667,7 @@ class MemCache
|
|
646
667
|
entry_count_for(server, servers.size, total_weight).times do |idx|
|
647
668
|
hash = Digest::SHA1.hexdigest("#{server.host}:#{server.port}:#{idx}")
|
648
669
|
value = Integer("0x#{hash[0..7]}")
|
649
|
-
continuum <<
|
670
|
+
continuum << Continuum::Entry.new(value, server)
|
650
671
|
end
|
651
672
|
end
|
652
673
|
|
@@ -654,23 +675,7 @@ class MemCache
|
|
654
675
|
end
|
655
676
|
|
656
677
|
def entry_count_for(server, total_servers, total_weight)
|
657
|
-
((total_servers *
|
658
|
-
end
|
659
|
-
|
660
|
-
class ContinuumEntry
|
661
|
-
POINTS_PER_SERVER = 160 # this is the default in libmemcached
|
662
|
-
|
663
|
-
attr_reader :value
|
664
|
-
attr_reader :server
|
665
|
-
|
666
|
-
def initialize(val, srv)
|
667
|
-
@value = val
|
668
|
-
@server = srv
|
669
|
-
end
|
670
|
-
|
671
|
-
def inspect
|
672
|
-
"<#{value}, #{server.host}:#{server.port}>"
|
673
|
-
end
|
678
|
+
((total_servers * Continuum::POINTS_PER_SERVER * server.weight) / Float(total_weight)).floor
|
674
679
|
end
|
675
680
|
|
676
681
|
##
|
@@ -685,13 +690,6 @@ class MemCache
|
|
685
690
|
|
686
691
|
CONNECT_TIMEOUT = 0.25
|
687
692
|
|
688
|
-
##
|
689
|
-
# The amount of time to wait for a response from a memcached server.
|
690
|
-
# If a response isn't received within this time limit,
|
691
|
-
# the server will be marked as down.
|
692
|
-
|
693
|
-
SOCKET_TIMEOUT = 0.25
|
694
|
-
|
695
693
|
##
|
696
694
|
# The amount of time to wait before attempting to re-establish a
|
697
695
|
# connection with a server that is marked dead.
|
@@ -743,6 +741,7 @@ class MemCache
|
|
743
741
|
@sock = nil
|
744
742
|
@retry = nil
|
745
743
|
@status = 'NOT CONNECTED'
|
744
|
+
@timeout = memcache.timeout
|
746
745
|
end
|
747
746
|
|
748
747
|
##
|
@@ -777,8 +776,7 @@ class MemCache
|
|
777
776
|
|
778
777
|
# Attempt to connect if not already connected.
|
779
778
|
begin
|
780
|
-
|
781
|
-
@sock = TCPTimeoutSocket.new @host, @port
|
779
|
+
@sock = @timeout ? TCPTimeoutSocket.new(@host, @port, @timeout) : TCPSocket.new(@host, @port)
|
782
780
|
|
783
781
|
if Socket.constants.include? 'TCP_NODELAY' then
|
784
782
|
@sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
|
@@ -826,35 +824,15 @@ class MemCache
|
|
826
824
|
|
827
825
|
class MemCacheError < RuntimeError; end
|
828
826
|
|
829
|
-
|
830
|
-
# Find the closest element in Array less than or equal to value.
|
831
|
-
def binary_search(ary, value, &block)
|
832
|
-
upper = ary.size - 1
|
833
|
-
lower = 0
|
834
|
-
idx = 0
|
835
|
-
|
836
|
-
result = while(lower <= upper) do
|
837
|
-
idx = (lower + upper) / 2
|
838
|
-
comp = block.call(ary[idx]) <=> value
|
839
|
-
|
840
|
-
if comp == 0
|
841
|
-
break idx
|
842
|
-
elsif comp > 0
|
843
|
-
upper = idx - 1
|
844
|
-
else
|
845
|
-
lower = idx + 1
|
846
|
-
end
|
847
|
-
end
|
848
|
-
result ? ary[result] : ary[upper]
|
849
|
-
end
|
850
827
|
end
|
851
828
|
|
852
829
|
# TCPSocket facade class which implements timeouts.
|
853
830
|
class TCPTimeoutSocket
|
854
|
-
|
831
|
+
|
832
|
+
def initialize(host, port, timeout)
|
855
833
|
Timeout::timeout(MemCache::Server::CONNECT_TIMEOUT, SocketError) do
|
856
|
-
@sock = TCPSocket.new(
|
857
|
-
@len =
|
834
|
+
@sock = TCPSocket.new(host, port)
|
835
|
+
@len = timeout
|
858
836
|
end
|
859
837
|
end
|
860
838
|
|
@@ -880,7 +858,11 @@ class TCPTimeoutSocket
|
|
880
858
|
@sock
|
881
859
|
end
|
882
860
|
|
883
|
-
def
|
884
|
-
@sock.
|
861
|
+
def setsockopt(*args)
|
862
|
+
@sock.setsockopt(*args)
|
885
863
|
end
|
886
|
-
|
864
|
+
|
865
|
+
def closed?
|
866
|
+
@sock.closed?
|
867
|
+
end
|
868
|
+
end
|
data/test/test_mem_cache.rb
CHANGED
@@ -5,7 +5,7 @@ require 'rubygems'
|
|
5
5
|
begin
|
6
6
|
gem 'flexmock'
|
7
7
|
require 'flexmock/test_unit'
|
8
|
-
rescue => e
|
8
|
+
rescue LoadError => e
|
9
9
|
puts "Some tests require flexmock, please run `gem install flexmock`"
|
10
10
|
end
|
11
11
|
|
@@ -42,22 +42,67 @@ class FakeSocket
|
|
42
42
|
|
43
43
|
end
|
44
44
|
|
45
|
+
class Test::Unit::TestCase
|
46
|
+
def requirement(bool, msg)
|
47
|
+
if bool
|
48
|
+
yield
|
49
|
+
else
|
50
|
+
puts msg
|
51
|
+
assert true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def memcached_running?
|
56
|
+
TCPSocket.new('localhost', 11211) rescue false
|
57
|
+
end
|
58
|
+
|
59
|
+
def xprofile(name, &block)
|
60
|
+
a = Time.now
|
61
|
+
block.call
|
62
|
+
Time.now - a
|
63
|
+
end
|
64
|
+
|
65
|
+
def profile(name, &block)
|
66
|
+
require 'ruby-prof'
|
67
|
+
a = Time.now
|
68
|
+
result = RubyProf.profile(&block)
|
69
|
+
time = Time.now - a
|
70
|
+
printer = RubyProf::GraphHtmlPrinter.new(result)
|
71
|
+
File.open("#{name}.html", 'w') do |f|
|
72
|
+
printer.print(f, :min_percent=>1)
|
73
|
+
end
|
74
|
+
time
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
45
79
|
class FakeServer
|
46
80
|
|
47
|
-
attr_reader :host, :port, :socket
|
81
|
+
attr_reader :host, :port, :socket, :weight, :multithread, :status
|
48
82
|
|
49
83
|
def initialize(socket = nil)
|
50
84
|
@closed = false
|
51
85
|
@host = 'example.com'
|
52
86
|
@port = 11211
|
53
87
|
@socket = socket || FakeSocket.new
|
88
|
+
@weight = 1
|
89
|
+
@multithread = false
|
90
|
+
@status = "CONNECTED"
|
54
91
|
end
|
55
92
|
|
56
93
|
def close
|
94
|
+
# begin
|
95
|
+
# raise "Already closed"
|
96
|
+
# rescue => e
|
97
|
+
# puts e.backtrace.join("\n")
|
98
|
+
# end
|
57
99
|
@closed = true
|
100
|
+
@socket = nil
|
101
|
+
@status = "NOT CONNECTED"
|
58
102
|
end
|
59
103
|
|
60
104
|
def alive?
|
105
|
+
# puts "I'm #{@closed ? 'dead' : 'alive'}"
|
61
106
|
!@closed
|
62
107
|
end
|
63
108
|
|
@@ -69,28 +114,102 @@ class TestMemCache < Test::Unit::TestCase
|
|
69
114
|
@cache = MemCache.new 'localhost:1', :namespace => 'my_namespace'
|
70
115
|
end
|
71
116
|
|
117
|
+
def test_performance
|
118
|
+
requirement(memcached_running?, 'A real memcached server must be running for performance testing') do
|
119
|
+
host = Socket.gethostname
|
120
|
+
|
121
|
+
cache = MemCache.new(['localhost:11211',"#{host}:11211"])
|
122
|
+
cache.add('a', 1, 120)
|
123
|
+
with = xprofile 'get' do
|
124
|
+
1000.times do
|
125
|
+
cache.get('a')
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
cache = MemCache.new(['localhost:11211',"#{host}:11211"], :timeout => false)
|
130
|
+
cache.add('a', 1, 120)
|
131
|
+
without = xprofile 'get' do
|
132
|
+
1000.times do
|
133
|
+
cache.get('a')
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
assert without < with
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
72
141
|
def test_consistent_hashing
|
73
|
-
|
142
|
+
requirement(self.respond_to?(:flexmock), 'Flexmock is required to run this test') do
|
143
|
+
|
144
|
+
flexmock(MemCache::Server).new_instances.should_receive(:alive?).and_return(true)
|
74
145
|
|
75
|
-
|
76
|
-
|
146
|
+
# Setup a continuum of two servers
|
147
|
+
@cache.servers = ['mike1', 'mike2', 'mike3']
|
77
148
|
|
78
|
-
|
79
|
-
|
80
|
-
|
149
|
+
keys = []
|
150
|
+
1000.times do |idx|
|
151
|
+
keys << idx.to_s
|
152
|
+
end
|
153
|
+
|
154
|
+
before_continuum = keys.map {|key| @cache.get_server_for_key(key) }
|
155
|
+
|
156
|
+
@cache.servers = ['mike1', 'mike2', 'mike3', 'mike4']
|
157
|
+
|
158
|
+
after_continuum = keys.map {|key| @cache.get_server_for_key(key) }
|
159
|
+
|
160
|
+
same_count = before_continuum.zip(after_continuum).find_all {|a| a[0].host == a[1].host }.size
|
161
|
+
|
162
|
+
# With continuum, we should see about 75% of the keys map to the same server
|
163
|
+
# With modulo, we would see about 25%.
|
164
|
+
assert same_count > 700
|
81
165
|
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def test_cache_get_with_failover
|
169
|
+
s1 = FakeServer.new
|
170
|
+
s2 = FakeServer.new
|
82
171
|
|
83
|
-
|
172
|
+
@cache.logger = nil
|
84
173
|
|
85
|
-
|
174
|
+
# Write two messages to the socket to test failover
|
175
|
+
s1.socket.data.write "VALUE foo 0 14\r\n\004\b\"\0170123456789\r\n"
|
176
|
+
s1.socket.data.rewind
|
177
|
+
s2.socket.data.write "bogus response\r\nbogus response\r\n"
|
178
|
+
s2.socket.data.rewind
|
179
|
+
|
180
|
+
@cache.instance_variable_set(:@failover, true)
|
181
|
+
@cache.servers = [s1, s2]
|
86
182
|
|
87
|
-
|
183
|
+
assert s1.alive?
|
184
|
+
assert s2.alive?
|
185
|
+
@cache.get('foo')
|
186
|
+
assert s1.alive?
|
187
|
+
assert !s2.alive?
|
188
|
+
end
|
189
|
+
|
190
|
+
def test_cache_get_without_failover
|
191
|
+
s1 = FakeServer.new
|
192
|
+
s2 = FakeServer.new
|
193
|
+
|
194
|
+
@cache.logger = nil
|
88
195
|
|
89
|
-
|
196
|
+
s1.socket.data.write "VALUE foo 0 14\r\n\004\b\"\0170123456789\r\n"
|
197
|
+
s1.socket.data.rewind
|
198
|
+
s2.socket.data.write "bogus response\r\nbogus response\r\n"
|
199
|
+
s2.socket.data.rewind
|
90
200
|
|
91
|
-
|
92
|
-
|
93
|
-
|
201
|
+
@cache.instance_variable_set(:@failover, false)
|
202
|
+
@cache.servers = [s1, s2]
|
203
|
+
|
204
|
+
assert s1.alive?
|
205
|
+
assert s2.alive?
|
206
|
+
e = assert_raise MemCache::MemCacheError do
|
207
|
+
@cache.get('foo')
|
208
|
+
end
|
209
|
+
assert s1.alive?
|
210
|
+
assert !s2.alive?
|
211
|
+
|
212
|
+
assert_equal "No servers available", e.message
|
94
213
|
end
|
95
214
|
|
96
215
|
def test_cache_get
|
@@ -107,11 +226,11 @@ class TestMemCache < Test::Unit::TestCase
|
|
107
226
|
server = util_setup_fake_server
|
108
227
|
server.socket.data.string = ''
|
109
228
|
|
110
|
-
e = assert_raise
|
229
|
+
e = assert_raise IndexError do
|
111
230
|
@cache.cache_get server, 'my_namespace:key'
|
112
231
|
end
|
113
232
|
|
114
|
-
assert_equal "
|
233
|
+
assert_equal "No connection to server (NOT CONNECTED)", e.message
|
115
234
|
end
|
116
235
|
|
117
236
|
def test_cache_get_bad_state
|
@@ -124,15 +243,13 @@ class TestMemCache < Test::Unit::TestCase
|
|
124
243
|
@cache.servers = []
|
125
244
|
@cache.servers << server
|
126
245
|
|
127
|
-
e = assert_raise
|
246
|
+
e = assert_raise IndexError do
|
128
247
|
@cache.cache_get(server, 'my_namespace:key')
|
129
248
|
end
|
130
249
|
|
131
|
-
assert_match /#{Regexp.quote '
|
250
|
+
assert_match /#{Regexp.quote 'No connection to server (NOT CONNECTED)'}/, e.message
|
132
251
|
|
133
252
|
assert !server.alive?
|
134
|
-
|
135
|
-
assert_match /get my_namespace:key\r\n/, server.socket.written.string
|
136
253
|
end
|
137
254
|
|
138
255
|
def test_cache_get_miss
|
@@ -167,11 +284,11 @@ class TestMemCache < Test::Unit::TestCase
|
|
167
284
|
server = util_setup_fake_server
|
168
285
|
server.socket.data.string = ''
|
169
286
|
|
170
|
-
e = assert_raise
|
287
|
+
e = assert_raise IndexError do
|
171
288
|
@cache.cache_get_multi server, 'my_namespace:key'
|
172
289
|
end
|
173
290
|
|
174
|
-
assert_equal "
|
291
|
+
assert_equal "No connection to server (NOT CONNECTED)", e.message
|
175
292
|
end
|
176
293
|
|
177
294
|
def test_cache_get_multi_bad_state
|
@@ -184,15 +301,13 @@ class TestMemCache < Test::Unit::TestCase
|
|
184
301
|
@cache.servers = []
|
185
302
|
@cache.servers << server
|
186
303
|
|
187
|
-
e = assert_raise
|
304
|
+
e = assert_raise IndexError do
|
188
305
|
@cache.cache_get_multi server, 'my_namespace:key'
|
189
306
|
end
|
190
307
|
|
191
|
-
assert_match /#{Regexp.quote '
|
308
|
+
assert_match /#{Regexp.quote 'No connection to server (NOT CONNECTED)'}/, e.message
|
192
309
|
|
193
310
|
assert !server.alive?
|
194
|
-
|
195
|
-
assert_match /get my_namespace:key\r\n/, server.socket.written.string
|
196
311
|
end
|
197
312
|
|
198
313
|
def test_initialize
|
@@ -516,14 +631,6 @@ class TestMemCache < Test::Unit::TestCase
|
|
516
631
|
assert_equal [server], @cache.servers
|
517
632
|
end
|
518
633
|
|
519
|
-
def test_servers_equals_type_error
|
520
|
-
e = assert_raise TypeError do
|
521
|
-
@cache.servers = [Object.new]
|
522
|
-
end
|
523
|
-
|
524
|
-
assert_equal 'cannot convert Object into MemCache::Server', e.message
|
525
|
-
end
|
526
|
-
|
527
634
|
def test_set
|
528
635
|
server = FakeServer.new
|
529
636
|
server.socket.data.write "STORED\r\n"
|
@@ -649,6 +756,19 @@ class TestMemCache < Test::Unit::TestCase
|
|
649
756
|
assert_equal expected, server.socket.written.string
|
650
757
|
end
|
651
758
|
|
759
|
+
def test_add_raw_int
|
760
|
+
server = FakeServer.new
|
761
|
+
server.socket.data.write "STORED\r\n"
|
762
|
+
server.socket.data.rewind
|
763
|
+
@cache.servers = []
|
764
|
+
@cache.servers << server
|
765
|
+
|
766
|
+
@cache.add 'key', 12, 0, true
|
767
|
+
|
768
|
+
expected = "add my_namespace:key 0 0 2\r\n12\r\n"
|
769
|
+
assert_equal expected, server.socket.written.string
|
770
|
+
end
|
771
|
+
|
652
772
|
def test_add_readonly
|
653
773
|
cache = MemCache.new :readonly => true
|
654
774
|
|
@@ -753,7 +873,7 @@ class TestMemCache < Test::Unit::TestCase
|
|
753
873
|
end
|
754
874
|
|
755
875
|
# TODO Fails in 1.9
|
756
|
-
assert_match /set my_namespace:test
|
876
|
+
assert_match /set my_namespace:test.*?\r\n.*?test value.*?\r\n/, server.socket.written.string
|
757
877
|
end
|
758
878
|
|
759
879
|
def test_basic_unthreaded_operations_should_work
|
@@ -775,7 +895,7 @@ class TestMemCache < Test::Unit::TestCase
|
|
775
895
|
end
|
776
896
|
|
777
897
|
# TODO Fails in 1.9
|
778
|
-
assert_match /set my_namespace:test
|
898
|
+
assert_match /set my_namespace:test.*?\r\n.*?test value.*?\r\n/, server.socket.written.string
|
779
899
|
end
|
780
900
|
|
781
901
|
def util_setup_fake_server
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mperham-memcache-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.6.
|
4
|
+
version: 1.6.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eric Hodel
|
@@ -19,17 +19,19 @@ description: A pure Ruby library for accessing memcached.
|
|
19
19
|
email: mperham@gmail.com
|
20
20
|
executables: []
|
21
21
|
|
22
|
-
extensions:
|
23
|
-
|
22
|
+
extensions:
|
23
|
+
- ext/memcache/extconf.rb
|
24
24
|
extra_rdoc_files: []
|
25
25
|
|
26
26
|
files:
|
27
|
-
- README.
|
27
|
+
- README.rdoc
|
28
28
|
- LICENSE.txt
|
29
29
|
- History.txt
|
30
30
|
- Rakefile
|
31
|
+
- lib/continuum.rb
|
31
32
|
- lib/memcache.rb
|
32
33
|
- lib/memcache_util.rb
|
34
|
+
- ext/memcache/binary_search.c
|
33
35
|
has_rdoc: false
|
34
36
|
homepage: http://github.com/mperham/memcache-client
|
35
37
|
post_install_message:
|