ninjudd-memcache 0.9.0
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/LICENSE +20 -0
- data/README.rdoc +161 -0
- data/Rakefile +55 -0
- data/VERSION +1 -0
- data/lib/memcache.rb +1118 -0
- data/lib/memcache/local_server.rb +91 -0
- data/lib/memcache/migration.rb +23 -0
- data/lib/memcache/null_server.rb +30 -0
- data/lib/memcache/pg_server.rb +159 -0
- data/lib/memcache/segmented_server.rb +89 -0
- data/lib/memcache/server.rb +249 -0
- data/lib/memcache_extended.rb +120 -0
- data/lib/memcache_mock.rb +137 -0
- data/lib/memcache_util.rb +90 -0
- data/test/memcache_local_server_test.rb +11 -0
- data/test/memcache_null_server_test.rb +65 -0
- data/test/memcache_pg_server_test.rb +28 -0
- data/test/memcache_segmented_server_test.rb +21 -0
- data/test/memcache_server_test.rb +17 -0
- data/test/memcache_server_test_helper.rb +159 -0
- data/test/memcache_test.rb +223 -0
- data/test/test_helper.rb +24 -0
- data/test/test_mem_cache.rb +739 -0
- data/test/test_memcache_extended.rb +44 -0
- data/test/test_memcache_mock.rb +94 -0
- metadata +89 -0
@@ -0,0 +1,120 @@
|
|
1
|
+
# Need to override entire class with Justin's changes
|
2
|
+
require File.dirname(__FILE__) + '/memcache'
|
3
|
+
|
4
|
+
module MemCacheExtensions
|
5
|
+
LOCK_TIMEOUT = 5 if not defined? LOCK_TIMEOUT
|
6
|
+
WRITE_LOCK_WAIT = 0.001 if not defined? WRITE_LOCK_WAIT
|
7
|
+
|
8
|
+
def get_some(keys, opts = {})
|
9
|
+
opts[:expiry] ||= default_expiry
|
10
|
+
keys = keys.collect {|key| key.to_s}
|
11
|
+
|
12
|
+
records = {}
|
13
|
+
records = self.get_multi(keys) unless opts[:disable]
|
14
|
+
if opts[:validation]
|
15
|
+
records.delete_if do |key, value|
|
16
|
+
not opts[:validation].call(key, value)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
keys_to_fetch = keys - records.keys
|
20
|
+
|
21
|
+
if keys_to_fetch.any?
|
22
|
+
yield(keys_to_fetch).each do |key, data_item|
|
23
|
+
self.set(key, data_item, opts[:expiry]) unless opts[:disable] or opts[:disable_write]
|
24
|
+
records[key] = data_item
|
25
|
+
end
|
26
|
+
end
|
27
|
+
records
|
28
|
+
end
|
29
|
+
|
30
|
+
def in_namespace(namespace)
|
31
|
+
begin
|
32
|
+
# Temporarily change the namespace for convenience.
|
33
|
+
old_namespace = self.namespace
|
34
|
+
self.namespace = "#{old_namespace}#{namespace}"
|
35
|
+
yield
|
36
|
+
ensure
|
37
|
+
self.namespace = old_namespace
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def get_or_set(key)
|
42
|
+
get(key) || begin
|
43
|
+
value = yield
|
44
|
+
set(key, value)
|
45
|
+
value
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_reset_expiry(key, expiry)
|
50
|
+
result = get(key)
|
51
|
+
set(key, result, expiry) if result
|
52
|
+
result
|
53
|
+
end
|
54
|
+
|
55
|
+
def lock(key)
|
56
|
+
# Returns true if the lock already exists.
|
57
|
+
response = add(lock_key(key), true, LOCK_TIMEOUT)
|
58
|
+
response.index('STORED') != 0
|
59
|
+
end
|
60
|
+
|
61
|
+
def unlock(key)
|
62
|
+
response = delete(lock_key(key))
|
63
|
+
response.index('DELETED') == 0
|
64
|
+
end
|
65
|
+
|
66
|
+
def with_lock(key, flag = nil)
|
67
|
+
while lock(key) do
|
68
|
+
return if flag == :ignore
|
69
|
+
sleep(WRITE_LOCK_WAIT) # just wait
|
70
|
+
end
|
71
|
+
yield
|
72
|
+
unlock(key) unless flag == :keep
|
73
|
+
end
|
74
|
+
|
75
|
+
def lock_key(key)
|
76
|
+
"lock:#{key}"
|
77
|
+
end
|
78
|
+
|
79
|
+
def locked?(key)
|
80
|
+
not get(lock_key(key)).nil?
|
81
|
+
end
|
82
|
+
|
83
|
+
def set_with_lock(*args)
|
84
|
+
with_lock(args.first, :ignore) do
|
85
|
+
set(*args)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def add_with_lock(*args)
|
90
|
+
with_lock(args.first, :ignore) do
|
91
|
+
add(*args)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def delete_with_lock(*args)
|
96
|
+
# leave a :delete lock around to prevent someone from
|
97
|
+
# adding stale data for a little while
|
98
|
+
with_lock(args.first, :keep) do
|
99
|
+
delete(*args)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def clear
|
104
|
+
flush_all
|
105
|
+
end
|
106
|
+
|
107
|
+
### To support using memcache in testing.
|
108
|
+
def empty?; false; end
|
109
|
+
###
|
110
|
+
end
|
111
|
+
|
112
|
+
class MemCache
|
113
|
+
include MemCacheExtensions
|
114
|
+
end
|
115
|
+
|
116
|
+
if defined?(MemCacheMock)
|
117
|
+
class MemCacheMock
|
118
|
+
include MemCacheExtensions
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
class MemCacheMock
|
2
|
+
attr_writer :namespace
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@data = {}
|
6
|
+
@expiry = {}
|
7
|
+
@auto_clear = false
|
8
|
+
end
|
9
|
+
|
10
|
+
def namespace
|
11
|
+
@namespace.to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
def cache_key(key)
|
15
|
+
"#{namespace}:#{key}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def default_expiry
|
19
|
+
0
|
20
|
+
end
|
21
|
+
|
22
|
+
# Note: This doesn't work exactly like memcache's incr
|
23
|
+
# because MemCacheMock doesn't support raw storage.
|
24
|
+
# This version will work on marshalled data.
|
25
|
+
# This is also not atomic.
|
26
|
+
def incr(key, amount=1)
|
27
|
+
oldval = get(key).to_i or return nil
|
28
|
+
newval = oldval + amount
|
29
|
+
set(key, newval) # Note: Loses the expiry.
|
30
|
+
return newval
|
31
|
+
end
|
32
|
+
|
33
|
+
def decr(key, amount=1)
|
34
|
+
incr(key, amount * -1)
|
35
|
+
end
|
36
|
+
|
37
|
+
def set(*args)
|
38
|
+
do_set(*args)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Note: Raw not implemented.
|
42
|
+
def do_set(key, value, expiry = default_expiry, raw=false)
|
43
|
+
return '' if @auto_clear
|
44
|
+
key = cache_key(key)
|
45
|
+
|
46
|
+
@data[key] = Marshal.dump(value)
|
47
|
+
@expiry[key] = Time.now + expiry if expiry and expiry != 0
|
48
|
+
'STORED'
|
49
|
+
end
|
50
|
+
|
51
|
+
def add(key, value, expiry = 0)
|
52
|
+
return '' if get(key)
|
53
|
+
do_set(key, value, expiry)
|
54
|
+
end
|
55
|
+
|
56
|
+
def kind_of?(type)
|
57
|
+
(type == MemCache) || super
|
58
|
+
end
|
59
|
+
|
60
|
+
def delete(key)
|
61
|
+
key = cache_key(key)
|
62
|
+
@data.delete(key)
|
63
|
+
end
|
64
|
+
|
65
|
+
def clear
|
66
|
+
@data.clear
|
67
|
+
@expiry.clear
|
68
|
+
end
|
69
|
+
|
70
|
+
def reset
|
71
|
+
# do nothing
|
72
|
+
end
|
73
|
+
|
74
|
+
def get_multi(*keys)
|
75
|
+
opts = keys.last.kind_of?(Hash) ? keys.pop : {}
|
76
|
+
keys.flatten!
|
77
|
+
|
78
|
+
hash = {}
|
79
|
+
keys.each do |key|
|
80
|
+
val = get(key)
|
81
|
+
hash[key.to_s] = val if val
|
82
|
+
end
|
83
|
+
hash
|
84
|
+
end
|
85
|
+
|
86
|
+
# Note: Raw not implemented.
|
87
|
+
def get(key, raw=false)
|
88
|
+
key = cache_key(key)
|
89
|
+
clear if @auto_clear
|
90
|
+
if @expiry[key] and Time.now > @expiry[key]
|
91
|
+
@data[key] = nil
|
92
|
+
@expiry[key] = nil
|
93
|
+
end
|
94
|
+
return if not @data[key]
|
95
|
+
Marshal.load(@data[key])
|
96
|
+
end
|
97
|
+
|
98
|
+
def [](key)
|
99
|
+
get(key)
|
100
|
+
end
|
101
|
+
|
102
|
+
def []=(key, value)
|
103
|
+
set(key, value)
|
104
|
+
end
|
105
|
+
|
106
|
+
def empty?
|
107
|
+
@data.empty?
|
108
|
+
end
|
109
|
+
|
110
|
+
def keys
|
111
|
+
@data.keys
|
112
|
+
end
|
113
|
+
|
114
|
+
def auto_clear_on(&block)
|
115
|
+
if block_given?
|
116
|
+
auto_clear_block(true, &block)
|
117
|
+
else
|
118
|
+
@auto_clear = true
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def auto_clear_off(&block)
|
123
|
+
if block_given?
|
124
|
+
auto_clear_block(false, &block)
|
125
|
+
else
|
126
|
+
@auto_clear = false
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def auto_clear_block(value, &block)
|
131
|
+
old_auto_clear = @auto_clear
|
132
|
+
@auto_clear = value
|
133
|
+
block.call
|
134
|
+
@auto_clear = old_auto_clear
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
##
|
2
|
+
# A utility wrapper around the MemCache client to simplify cache access. All
|
3
|
+
# methods silently ignore MemCache errors.
|
4
|
+
|
5
|
+
module Cache
|
6
|
+
|
7
|
+
##
|
8
|
+
# Returns the object at +key+ from the cache if successful, or nil if either
|
9
|
+
# the object is not in the cache or if there was an error attermpting to
|
10
|
+
# access the cache.
|
11
|
+
#
|
12
|
+
# If there is a cache miss and a block is given the result of the block will
|
13
|
+
# be stored in the cache with optional +expiry+, using the +add+ method rather
|
14
|
+
# than +set+.
|
15
|
+
|
16
|
+
def self.get(key, expiry = 0)
|
17
|
+
start_time = Time.now
|
18
|
+
value = CACHE.get key
|
19
|
+
elapsed = Time.now - start_time
|
20
|
+
ActiveRecord::Base.logger.debug('MemCache Get (%0.6f) %s' % [elapsed, key])
|
21
|
+
if value.nil? and block_given? then
|
22
|
+
value = yield
|
23
|
+
add key, value, expiry
|
24
|
+
end
|
25
|
+
value
|
26
|
+
rescue MemCache::MemCacheError => err
|
27
|
+
ActiveRecord::Base.logger.debug "MemCache Error: #{err.message}"
|
28
|
+
if block_given? then
|
29
|
+
value = yield
|
30
|
+
put key, value, expiry
|
31
|
+
end
|
32
|
+
value
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# Sets +value+ in the cache at +key+, with an optional +expiry+ time in
|
37
|
+
# seconds.
|
38
|
+
|
39
|
+
def self.put(key, value, expiry = 0)
|
40
|
+
start_time = Time.now
|
41
|
+
CACHE.set key, value, expiry
|
42
|
+
elapsed = Time.now - start_time
|
43
|
+
ActiveRecord::Base.logger.debug('MemCache Set (%0.6f) %s' % [elapsed, key])
|
44
|
+
value
|
45
|
+
rescue MemCache::MemCacheError => err
|
46
|
+
ActiveRecord::Base.logger.debug "MemCache Error: #{err.message}"
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# Sets +value+ in the cache at +key+, with an optional +expiry+ time in
|
52
|
+
# seconds. If +key+ already exists in cache, returns nil.
|
53
|
+
|
54
|
+
def self.add(key, value, expiry = 0)
|
55
|
+
start_time = Time.now
|
56
|
+
response = CACHE.add key, value, expiry
|
57
|
+
elapsed = Time.now - start_time
|
58
|
+
ActiveRecord::Base.logger.debug('MemCache Add (%0.6f) %s' % [elapsed, key])
|
59
|
+
(response == "STORED\r\n") ? value : nil
|
60
|
+
rescue MemCache::MemCacheError => err
|
61
|
+
ActiveRecord::Base.logger.debug "MemCache Error: #{err.message}"
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# Deletes +key+ from the cache in +delay+ seconds.
|
67
|
+
|
68
|
+
def self.delete(key, delay = nil)
|
69
|
+
start_time = Time.now
|
70
|
+
CACHE.delete key, delay
|
71
|
+
elapsed = Time.now - start_time
|
72
|
+
ActiveRecord::Base.logger.debug('MemCache Delete (%0.6f) %s' %
|
73
|
+
[elapsed, key])
|
74
|
+
nil
|
75
|
+
rescue MemCache::MemCacheError => err
|
76
|
+
ActiveRecord::Base.logger.debug "MemCache Error: #{err.message}"
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
|
80
|
+
##
|
81
|
+
# Resets all connections to MemCache servers.
|
82
|
+
|
83
|
+
def self.reset
|
84
|
+
CACHE.reset
|
85
|
+
ActiveRecord::Base.logger.debug 'MemCache Connections Reset'
|
86
|
+
nil
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require File.dirname(__FILE__) + '/../lib/memcache/local_server'
|
3
|
+
require File.dirname(__FILE__) + '/memcache_server_test_helper'
|
4
|
+
|
5
|
+
class MemcacheLocalServerTest < Test::Unit::TestCase
|
6
|
+
include MemcacheServerTestHelper
|
7
|
+
|
8
|
+
def setup
|
9
|
+
@memcache = Memcache::LocalServer.new
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require File.dirname(__FILE__) + '/../lib/memcache/null_server'
|
3
|
+
|
4
|
+
class MemcacheNullServerTest < Test::Unit::TestCase
|
5
|
+
def setup
|
6
|
+
@memcache = Memcache::NullServer.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def m
|
10
|
+
@memcache
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_set_and_get
|
14
|
+
m.set(2, 'foo', 0)
|
15
|
+
|
16
|
+
assert_equal nil, m.get('2')
|
17
|
+
assert_equal nil, m.get('2')
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_incr
|
21
|
+
m.incr('foo')
|
22
|
+
assert_equal nil, m.get('foo')
|
23
|
+
|
24
|
+
m.incr('foo', -1)
|
25
|
+
assert_equal nil, m.get('foo')
|
26
|
+
|
27
|
+
m.incr('foo', 52)
|
28
|
+
assert_equal nil, m.get('foo')
|
29
|
+
|
30
|
+
m.incr('foo', -43)
|
31
|
+
assert_equal nil, m.get('foo')
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_multi_get
|
35
|
+
m.set(2, '1,2,3')
|
36
|
+
m.set(3, '4,5')
|
37
|
+
|
38
|
+
assert_equal Hash.new, m.get([2,3])
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_delete
|
42
|
+
m.set(2, '1,2,3')
|
43
|
+
|
44
|
+
assert_equal nil, m.get(2)
|
45
|
+
|
46
|
+
m.delete(2)
|
47
|
+
|
48
|
+
assert_equal nil, m.get(2)
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_flush_all
|
52
|
+
m.set(2, 'bar')
|
53
|
+
|
54
|
+
assert_equal nil, m.get(2)
|
55
|
+
|
56
|
+
m.flush_all
|
57
|
+
|
58
|
+
assert_equal nil, m.get(2)
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_expiry
|
62
|
+
m.add('test', '1', 1)
|
63
|
+
assert_equal nil, m.get('test')
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'rubygems'
|
3
|
+
require File.dirname(__FILE__) + '/memcache_server_test_helper'
|
4
|
+
require File.dirname(__FILE__) + '/../lib/memcache/pg_server'
|
5
|
+
|
6
|
+
class MemcacheDBServerTest < Test::Unit::TestCase
|
7
|
+
ActiveRecord::Base.establish_connection(
|
8
|
+
:adapter => "postgresql",
|
9
|
+
:host => "localhost",
|
10
|
+
:username => "postgres",
|
11
|
+
:password => "",
|
12
|
+
:database => "memcache_test"
|
13
|
+
)
|
14
|
+
ActiveRecord::Migration.verbose = false
|
15
|
+
ActiveRecord::Base.connection.client_min_messages = 'panic'
|
16
|
+
|
17
|
+
include MemcacheServerTestHelper
|
18
|
+
|
19
|
+
def setup
|
20
|
+
Memcache::Migration.table = 'memcache_test'
|
21
|
+
Memcache::Migration.up
|
22
|
+
@memcache = Memcache::PGServer.new(:table => 'memcache_test')
|
23
|
+
end
|
24
|
+
|
25
|
+
def teardown
|
26
|
+
Memcache::Migration.down
|
27
|
+
end
|
28
|
+
end
|