tokyo_cache_cow 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +93 -0
- data/Rakefile +33 -0
- data/VERSION.yml +4 -0
- data/bin/tokyo_cache_cow +6 -0
- data/lib/tokyo_cache_cow/cache/base.rb +15 -0
- data/lib/tokyo_cache_cow/cache/file_memcache.rb +113 -0
- data/lib/tokyo_cache_cow/cache/hash_memcache.rb +92 -0
- data/lib/tokyo_cache_cow/cache/tokyo_cabinet_memcache.rb +125 -0
- data/lib/tokyo_cache_cow/cache.rb +11 -0
- data/lib/tokyo_cache_cow/providers.rb +13 -0
- data/lib/tokyo_cache_cow/runner.rb +110 -0
- data/lib/tokyo_cache_cow/server.rb +216 -0
- data/lib/tokyo_cache_cow.rb +5 -0
- data/rails/init.rb +43 -0
- data/spec/cache_spec.rb +152 -0
- data/spec/server_spec.rb +70 -0
- data/spec/spec.opts +8 -0
- metadata +72 -0
data/README.rdoc
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
= Tokyo Cache Cow
|
2
|
+
|
3
|
+
Tokyo Cache Cow is MemCache protocol speaking cache server. It offers the ability to delete keys based on a substring.
|
4
|
+
|
5
|
+
== Motivation
|
6
|
+
|
7
|
+
Cache sweepers in rails do not currently operate with memcache because the memcache server itself does not support key matching removal operations. After reading http://www.igvita.com/2009/02/13/tokyo-cabinet-beyond-key-value-store/ and seeing the performance characteristics of that database, I thought I'd give it a go. Event Machine does the heavy lifting on the network end. Performance is currently comparable to memcached.
|
8
|
+
|
9
|
+
== Prerequisites
|
10
|
+
|
11
|
+
You'll need the eventmachine gem installed. As well, you'll have to install Tokyo Cabinet itself (available at http://tokyocabinet.sourceforge.net/index.html) and the Tokyo Cabinet Ruby bindings (available at http://tokyocabinet.sourceforge.net/rubypkg/)
|
12
|
+
|
13
|
+
== Example (using the rails client under script/console)
|
14
|
+
|
15
|
+
Lets write four keys: <i>other_key</i>, <i>test_key</i>, <i>test_key2</i> and <i>test_key3</i>.
|
16
|
+
|
17
|
+
>> Rails.cache.write('other_key', 'other_value')
|
18
|
+
=> true
|
19
|
+
>> Rails.cache.write('test_key', 'test_value')
|
20
|
+
=> true
|
21
|
+
>> Rails.cache.write('test_key2', 'test_value2')
|
22
|
+
=> true
|
23
|
+
>> Rails.cache.write('test_key3', 'test_value3')
|
24
|
+
=> true
|
25
|
+
|
26
|
+
Read back <i>test_key</i> and make sure life is still good.
|
27
|
+
|
28
|
+
>> Rails.cache.read('test_key2')
|
29
|
+
=> "test_value2"
|
30
|
+
|
31
|
+
But lets delete <i>test_key2</i> for fun.
|
32
|
+
|
33
|
+
>> Rails.cache.delete('test_key2')
|
34
|
+
=> true
|
35
|
+
|
36
|
+
Confirm that <i>test_key2</i> is really gone.
|
37
|
+
|
38
|
+
>> Rails.cache.read('test_key2')
|
39
|
+
=> nil
|
40
|
+
|
41
|
+
.. but our other keys (namely, <i>test_key</i>) are just fine, thank you.
|
42
|
+
|
43
|
+
>> Rails.cache.read('test_key')
|
44
|
+
=> "test_value"
|
45
|
+
|
46
|
+
.. lets nuke *EVERYTHING* with <i>test_key</i> in it though.
|
47
|
+
|
48
|
+
>> Rails.cache.delete_matched('test_key')
|
49
|
+
=> true
|
50
|
+
|
51
|
+
Now <i>test_key</i> and <i>test_key3</i> are both nuked.
|
52
|
+
|
53
|
+
>> Rails.cache.read('test_key')
|
54
|
+
=> nil
|
55
|
+
>> Rails.cache.read('test_key3')
|
56
|
+
=> nil
|
57
|
+
|
58
|
+
But <i>other_key</i> is still peachy.
|
59
|
+
|
60
|
+
>> Rails.cache.read('other_key')
|
61
|
+
=> "other_value"
|
62
|
+
|
63
|
+
|
64
|
+
== Usage
|
65
|
+
|
66
|
+
=== Server
|
67
|
+
|
68
|
+
|
69
|
+
>> tokyo_cache_cow --help
|
70
|
+
|
71
|
+
Usage: tokyo_cache_cow [options]
|
72
|
+
Options:
|
73
|
+
-p, --port[OPTIONAL] Port (default: 11211)
|
74
|
+
-a, --address[OPTIONAL] Address (default: 0.0.0.0)
|
75
|
+
-c, --class[OPTIONAL] Cache provider class (default: TokyoCacheCow::Cache::TokyoCabinetMemcache)
|
76
|
+
-r, --require[OPTIONAL] require
|
77
|
+
-f, --file[OPTIONAL] File (default: /tmp/tcc-cache)
|
78
|
+
-d, --daemonize[OPTIONAL] Daemonize (default: false)
|
79
|
+
-P, --pid[OPTIONAL] Pid file (default: /tmp/tcc.pid)
|
80
|
+
-m, --matcher[OPTIONAL] Special flag for doing matched deletes (not enabled by default)
|
81
|
+
-h, --help Show this help message.
|
82
|
+
|
83
|
+
=== Client
|
84
|
+
|
85
|
+
Any standard memcache client will do, however, there is a special initialize for Rails to enable delete_matched functionality within the built-in memcache client there. To install this into rails:
|
86
|
+
|
87
|
+
script/plugin install git://github.com/joshbuddy/tokyo-cache-cow.git
|
88
|
+
|
89
|
+
== Caveats
|
90
|
+
|
91
|
+
Right now there is no compression on disk. Things could get big, but Tokyo Cabinet does support various compression schemes, so exposing that to the runner should be trivial. Potentially performance could degrade after time, but I have yet to seriously investigate if thats the case.
|
92
|
+
|
93
|
+
Feel the moo.
|
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'lib/tokyo_cache_cow'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |s|
|
7
|
+
s.name = "tokyo_cache_cow"
|
8
|
+
s.description = s.summary = ""
|
9
|
+
s.email = "joshbuddy@gmail.com"
|
10
|
+
s.homepage = "http://github.com/joshbuddy/tokyo_cache_cow"
|
11
|
+
s.authors = ["Joshua Hull"]
|
12
|
+
s.files = FileList["[A-Z]*", "{lib,spec,rails,bin}/**/*"]
|
13
|
+
s.executables = ['tokyo_cache_cow']
|
14
|
+
end
|
15
|
+
Jeweler::GemcutterTasks.new
|
16
|
+
rescue LoadError
|
17
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
18
|
+
end
|
19
|
+
|
20
|
+
require 'spec'
|
21
|
+
require 'spec/rake/spectask'
|
22
|
+
|
23
|
+
task :spec => 'spec:all'
|
24
|
+
namespace(:spec) do
|
25
|
+
Spec::Rake::SpecTask.new(:all) do |t|
|
26
|
+
t.spec_opts ||= []
|
27
|
+
t.spec_opts << "-rubygems"
|
28
|
+
t.spec_opts << "--options" << "spec/spec.opts"
|
29
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
data/VERSION.yml
ADDED
data/bin/tokyo_cache_cow
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'cgi'
|
4
|
+
|
5
|
+
class TokyoCacheCow
|
6
|
+
class Cache
|
7
|
+
class FileMemcache < Base
|
8
|
+
|
9
|
+
def process_time(time)
|
10
|
+
time = case time
|
11
|
+
when 0, nil: 0
|
12
|
+
when 1..2592000: (Time.now.to_i + time.to_i)
|
13
|
+
else time
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(options = {})
|
18
|
+
@path = options[:file] or raise('must supply file')
|
19
|
+
flush_all
|
20
|
+
end
|
21
|
+
|
22
|
+
def time_expired?(time)
|
23
|
+
time.to_i == 0 ? false : time < Time.now.to_i
|
24
|
+
end
|
25
|
+
|
26
|
+
def generate_data_hash(value, options)
|
27
|
+
{
|
28
|
+
:value => value,
|
29
|
+
:expires => process_time(options[:expires] || 0),
|
30
|
+
:flags => options[:flags] || 0
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def add(key, value, options = {})
|
35
|
+
if (data = get_raw(key)) && !time_expired?(data[:expired])
|
36
|
+
nil
|
37
|
+
else
|
38
|
+
set(key, value, options) and true
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def delete_match(key)
|
43
|
+
FileUtils.rm Dir.glob(File.join(@path, "*#{CGI.escape(key)}*"))
|
44
|
+
end
|
45
|
+
|
46
|
+
def replace(key, value, options = {})
|
47
|
+
set(key, value, options) if File.exists?(path_for_key(key))
|
48
|
+
end
|
49
|
+
|
50
|
+
def append(key, val)
|
51
|
+
if data = get(key)
|
52
|
+
data[:value] << val
|
53
|
+
set_raw(key, data)
|
54
|
+
true
|
55
|
+
else
|
56
|
+
false
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def incr(key, value)
|
61
|
+
if data = get(key)
|
62
|
+
new_value = data[:value].to_i + value
|
63
|
+
set(key, new_value.to_s, :expires => data[:expires], :flags => data[:flags])
|
64
|
+
new_value
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def decr(key, value)
|
69
|
+
incr(key, -value)
|
70
|
+
end
|
71
|
+
|
72
|
+
def flush_all
|
73
|
+
FileUtils.rm_rf(@path)
|
74
|
+
FileUtils.mkdir_p(@path)
|
75
|
+
true
|
76
|
+
end
|
77
|
+
|
78
|
+
def delete(key, expires = nil)
|
79
|
+
FileUtils.rm(Dir.glob(path_for_key(key))) and true
|
80
|
+
end
|
81
|
+
|
82
|
+
def path_for_key(key)
|
83
|
+
File.join(@path, CGI.escape(key))
|
84
|
+
end
|
85
|
+
|
86
|
+
def get_raw(key)
|
87
|
+
File.exists?(path_for_key(key)) ? YAML::load( File.open( path_for_key(key) ) ) : nil
|
88
|
+
end
|
89
|
+
|
90
|
+
def set_raw(key, data)
|
91
|
+
File.open(path_for_key(key), 'w') do |out|
|
92
|
+
YAML.dump(data, out)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def get(key)
|
97
|
+
if data = get_raw(key)
|
98
|
+
if time_expired?(data[:expires])
|
99
|
+
delete(key)
|
100
|
+
nil
|
101
|
+
else
|
102
|
+
data
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def set(key, value, options = {})
|
108
|
+
set_raw(key, generate_data_hash(value, options))
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
class TokyoCacheCow
|
2
|
+
class Cache
|
3
|
+
class HashMemcache < Base
|
4
|
+
|
5
|
+
def process_time(time)
|
6
|
+
time = case time
|
7
|
+
when 0, nil: 0
|
8
|
+
when 1..2592000: (Time.now.to_i + time.to_i)
|
9
|
+
else time
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(options = {})
|
14
|
+
@cache = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def time_expired?(time)
|
18
|
+
time.to_i == 0 ? false : time < Time.now.to_i
|
19
|
+
end
|
20
|
+
|
21
|
+
def generate_data_hash(value, options)
|
22
|
+
{
|
23
|
+
:value => value,
|
24
|
+
:expires => process_time(options[:expires] || 0),
|
25
|
+
:flags => options[:flags] || 0
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def add(key, value, options = {})
|
30
|
+
if (data = @cache[key]) && !time_expired?(data[:expired])
|
31
|
+
nil
|
32
|
+
else
|
33
|
+
set(key, value, options) and true
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def delete_match(key)
|
38
|
+
@cache.delete_if{ |key, value| key.index(key) }
|
39
|
+
end
|
40
|
+
|
41
|
+
def replace(key, value, options = {})
|
42
|
+
set(key, value, options) if @cache.key?(key)
|
43
|
+
end
|
44
|
+
|
45
|
+
def append(key, val)
|
46
|
+
if data = get(key)
|
47
|
+
data[:value] << val
|
48
|
+
@cache[key] = data
|
49
|
+
true
|
50
|
+
else
|
51
|
+
false
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def incr(key, value)
|
56
|
+
if data = get(key)
|
57
|
+
new_value = data[:value].to_i + value
|
58
|
+
set(key, new_value.to_s, :expires => data[:expires], :flags => data[:flags])
|
59
|
+
new_value
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def decr(key, value)
|
64
|
+
incr(key, -value)
|
65
|
+
end
|
66
|
+
|
67
|
+
def flush_all
|
68
|
+
@cache.clear and true
|
69
|
+
end
|
70
|
+
|
71
|
+
def delete(key, expires = nil)
|
72
|
+
@cache.delete(key)
|
73
|
+
end
|
74
|
+
|
75
|
+
def get(key)
|
76
|
+
if data = @cache[key]
|
77
|
+
if time_expired?(data[:expires])
|
78
|
+
@cache.delete(key)
|
79
|
+
nil
|
80
|
+
else
|
81
|
+
data
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def set(key, value, options = {})
|
87
|
+
@cache[key] = generate_data_hash(value, options)
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'tokyocabinet'
|
2
|
+
|
3
|
+
class TokyoCacheCow
|
4
|
+
class Cache
|
5
|
+
class TokyoCabinetMemcache < Base
|
6
|
+
|
7
|
+
include TokyoCabinet
|
8
|
+
|
9
|
+
def process_time(time)
|
10
|
+
time = case time_i = Integer(time)
|
11
|
+
when 0: '0'
|
12
|
+
when 1..2592000: (Time.now.to_i + time_i).to_s
|
13
|
+
else time
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def flush_all
|
18
|
+
@cache.vanish
|
19
|
+
end
|
20
|
+
|
21
|
+
def get(key, cas = nil)
|
22
|
+
if (data = @cache.get(key)) && data['expired']
|
23
|
+
nil
|
24
|
+
elsif data
|
25
|
+
expires = data['expires'] && data['expires'].to_i
|
26
|
+
flags = data['flags'] && data['flags'].to_i
|
27
|
+
if expires != 0 && expires < Time.now.to_i
|
28
|
+
delete(key)
|
29
|
+
nil
|
30
|
+
else
|
31
|
+
{ :value => data['value'], :expires => expires, :flags => flags }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def incr(key, value)
|
37
|
+
if data = get(key)
|
38
|
+
new_value = data[:value].to_i + value
|
39
|
+
set(key, new_value.to_s, :expires => data[:expires], :flags => data[:flags])
|
40
|
+
new_value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def decr(key, value)
|
45
|
+
incr(key, -value)
|
46
|
+
end
|
47
|
+
|
48
|
+
def append(key, val)
|
49
|
+
if data = get(key)
|
50
|
+
data[:value] << val
|
51
|
+
set(key, data[:value], :expires => data[:expires], :flags => data[:flags])
|
52
|
+
true
|
53
|
+
else
|
54
|
+
false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def prepend(key, val)
|
59
|
+
if data = @cache.get(key)
|
60
|
+
data[:data][0,0] = val
|
61
|
+
put(key, data[:value], :expires => data[:expires], :flags => data[:flags])
|
62
|
+
true
|
63
|
+
else
|
64
|
+
false
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def generate_data_hash(value, options)
|
69
|
+
expires = options[:expires] && options[:expires].to_s || '0'
|
70
|
+
flags = options[:flags] && options[:flags].to_s || '0'
|
71
|
+
{ 'value' => value, 'expires' => process_time(expires), 'flags' => flags }
|
72
|
+
end
|
73
|
+
|
74
|
+
def time_expired?(time)
|
75
|
+
time == '0' ? false : time.to_i < Time.now.to_i
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
def set(key, value, options = {})
|
80
|
+
@cache.put(key, generate_data_hash(value, options))
|
81
|
+
end
|
82
|
+
|
83
|
+
def add(key, value, options = {})
|
84
|
+
if data = @cache.get(key)
|
85
|
+
time_expired?(data[:expired]) ?
|
86
|
+
nil : @cache.putkeep(key, generate_data_hash(value, options))
|
87
|
+
else
|
88
|
+
@cache.putkeep(key, generate_data_hash(value, options))
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def replace(key, value, options = {})
|
93
|
+
get(key) ? @cache.put(key, generate_data_hash(value, options)) : nil
|
94
|
+
end
|
95
|
+
|
96
|
+
def delete(key, opts = {})
|
97
|
+
if opts[:expires] && opts[:expires] != 0
|
98
|
+
@cache.put(key, {'expired' => process_time(opts[:expires])})
|
99
|
+
else
|
100
|
+
@cache.out(key)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def delete_match(match)
|
105
|
+
q = TDBQRY.new(@cache)
|
106
|
+
q.addcond('', TDBQRY::QCSTRINC, match)
|
107
|
+
q.search
|
108
|
+
q.searchout
|
109
|
+
end
|
110
|
+
|
111
|
+
def initialize(options = {})
|
112
|
+
@cache = TDB::new # hash database
|
113
|
+
raise('must supply file') unless options[:file]
|
114
|
+
if @cache.open(options[:file], TDB::OWRITER | TDB::OCREAT | TDB::OTRUNC)
|
115
|
+
@cache.setxmsiz(500_000_000)
|
116
|
+
else
|
117
|
+
puts @cache.ecode
|
118
|
+
puts @cache.errmsg(@cache.ecode)
|
119
|
+
raise
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class TokyoCacheCow
|
4
|
+
class Cache
|
5
|
+
self.autoload :Base, 'cache/base'
|
6
|
+
self.autoload :FileMemcache, 'cache/file_memcache'
|
7
|
+
self.autoload :TokyoCabinetMemcache, 'cache/tokyo_cabinet_memcache'
|
8
|
+
self.autoload :HashMemcache, 'cache/hash_memcache'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class TokyoCacheCow
|
2
|
+
|
3
|
+
autoload :TokyoCabinetMemcache, 'lib/tokyo_cache_cow/tokyo_cabinet_memcache'
|
4
|
+
|
5
|
+
class Providers
|
6
|
+
|
7
|
+
def self.provide_cache
|
8
|
+
#require 'lib/tokyo_cache_cow/tokyo_cabinet_memcache'
|
9
|
+
@@cache ||= TokyoCacheCow::TokyoCabinetMemcache.new('/tmp/tcc')
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'optparse'
|
3
|
+
|
4
|
+
class TokyoCacheCow
|
5
|
+
class Runner
|
6
|
+
|
7
|
+
attr_reader :options
|
8
|
+
|
9
|
+
def initialize(argv)
|
10
|
+
@argv = argv
|
11
|
+
# Default options values
|
12
|
+
@options = {
|
13
|
+
:chdir => Dir.pwd,
|
14
|
+
:address => '0.0.0.0',
|
15
|
+
:port => Server::DefaultPort,
|
16
|
+
:class => 'TokyoCacheCow::Cache::TokyoCabinetMemcache',
|
17
|
+
:require => [],
|
18
|
+
:file => '/tmp/tcc-cache',
|
19
|
+
:pid => '/tmp/tcc.pid',
|
20
|
+
:special_delete_prefix => nil,
|
21
|
+
:daemonize => false
|
22
|
+
}
|
23
|
+
|
24
|
+
parse!
|
25
|
+
end
|
26
|
+
|
27
|
+
def parser
|
28
|
+
OptionParser.new do |opts|
|
29
|
+
opts.banner = "Usage: tokyo_cache_cow [options]"
|
30
|
+
|
31
|
+
opts.separator ""
|
32
|
+
opts.separator "Options:"
|
33
|
+
|
34
|
+
opts.on("-p[OPTIONAL]", "--port", "Port (default: #{options[:port]})") do |v|
|
35
|
+
options[:port] = v
|
36
|
+
end
|
37
|
+
|
38
|
+
opts.on("-a[OPTIONAL]", "--address", "Address (default: #{options[:address]})") do |v|
|
39
|
+
options[:address] = v
|
40
|
+
end
|
41
|
+
|
42
|
+
opts.on("-c[OPTIONAL]", "--class", "Cache provider class (default: #{options[:class]})") do |v|
|
43
|
+
options[:provider] = v
|
44
|
+
end
|
45
|
+
|
46
|
+
opts.on("-r[OPTIONAL]", "--require", "require") do |v|
|
47
|
+
options[:require] << v
|
48
|
+
end
|
49
|
+
|
50
|
+
opts.on("-f[OPTIONAL]", "--file", "File (default: #{options[:file]})") do |v|
|
51
|
+
options[:file] = v
|
52
|
+
end
|
53
|
+
|
54
|
+
opts.on("-d[OPTIONAL]", "--daemonize", "Daemonize (default: #{options[:daemonize]})") do |v|
|
55
|
+
options[:daemonize] = true
|
56
|
+
end
|
57
|
+
|
58
|
+
opts.on("-P[OPTIONAL]", "--pid", "Pid file (default: #{options[:pid]})") do |v|
|
59
|
+
options[:pid] = true
|
60
|
+
end
|
61
|
+
|
62
|
+
opts.on("-m[OPTIONAL]", "--matcher", "Special flag for doing matched deletes (not enabled by default)") do |v|
|
63
|
+
options[:special_delete_char] = v
|
64
|
+
end
|
65
|
+
|
66
|
+
opts.on_tail("-h", "--help", "Show this help message.") { puts opts; exit }
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def parse!
|
72
|
+
parser.parse!(@argv)
|
73
|
+
end
|
74
|
+
|
75
|
+
def start!
|
76
|
+
@options[:require].each {|r| require r}
|
77
|
+
|
78
|
+
clazz = @options[:class].to_s.split('::').inject(Kernel) do |parent, mod|
|
79
|
+
parent.const_get(mod)
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
address = @options[:address]
|
84
|
+
port = @options[:port]
|
85
|
+
special_delete_char = @options[:special_delete_char]
|
86
|
+
puts "Starting the tokyo cache cow #{address} #{port}"
|
87
|
+
pid = EM.fork_reactor do
|
88
|
+
cache = clazz.new(:file => @options[:file])
|
89
|
+
trap("INT") { EM.stop; puts "\nmoooooooo ya later"; exit(0)}
|
90
|
+
EM.run do
|
91
|
+
EM.start_server(address, port, TokyoCacheCow::Server) do |c|
|
92
|
+
c.cache = cache
|
93
|
+
c.special_delete_char = special_delete_char if special_delete_char
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
if @options[:daemonize]
|
99
|
+
File.open(options[:pid], 'w') {|f| f << pid}
|
100
|
+
Process.detach(pid)
|
101
|
+
else
|
102
|
+
trap("INT") { }
|
103
|
+
Process.wait(pid)
|
104
|
+
end
|
105
|
+
|
106
|
+
pid
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,216 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
require 'eventmachine'
|
3
|
+
|
4
|
+
class TokyoCacheCow
|
5
|
+
class Server < EventMachine::Protocols::LineAndTextProtocol
|
6
|
+
|
7
|
+
DefaultPort = 11211
|
8
|
+
|
9
|
+
Terminator = "\r\n"
|
10
|
+
|
11
|
+
#set
|
12
|
+
SetCommand = /(\S+) +(\d+) +(\d+) +(\d+)( +noreply)?/
|
13
|
+
CasCommand = /(\S+) +(\d+) +(\d+) +(\d+) +(\d+)( +noreply)?/
|
14
|
+
|
15
|
+
StoredReply = "STORED\r\n"
|
16
|
+
NotStoredReply = "NOT_STORED\r\n"
|
17
|
+
ExistsReply = "EXISTS\r\n"
|
18
|
+
NotFoundReply = "NOT_FOUND\r\n"
|
19
|
+
|
20
|
+
GetValueReply = "VALUE %s %d %d\r\n"
|
21
|
+
CasValueReply = "VALUE %d %d %d %d\r\n"
|
22
|
+
EndReply = "END\r\n"
|
23
|
+
|
24
|
+
#delete
|
25
|
+
DeleteCommand = /(\S+) *(noreply)?/
|
26
|
+
DeleteWithTimeoutCommand = /(\S+) +(\d+) *(noreply)?/
|
27
|
+
|
28
|
+
DeletedReply = "DELETED\r\n"
|
29
|
+
NotDeletedReply = "NOT_DELETED\r\n"
|
30
|
+
|
31
|
+
#delete_match
|
32
|
+
DeleteMatchCommand = /(\S+)( +noreply)?/
|
33
|
+
|
34
|
+
#Increment/Decrement
|
35
|
+
IncrementDecrementCommand = /(\S+) +(\d+)( +noreply)?/
|
36
|
+
|
37
|
+
ValueReply = "%d\r\n"
|
38
|
+
|
39
|
+
#errors
|
40
|
+
OK = "OK\r\n"
|
41
|
+
Error = "ERROR\r\n"
|
42
|
+
ClientError = "CLIENT_ERROR %s\r\n"
|
43
|
+
ServerError = "SERVER_ERROR %s\r\n"
|
44
|
+
|
45
|
+
TerminatorRegex = /\r\n/
|
46
|
+
|
47
|
+
attr_accessor :cache, :special_delete_char
|
48
|
+
|
49
|
+
def send_client_error(message = "invalid arguments")
|
50
|
+
send_data(ClientError % message.to_s)
|
51
|
+
end
|
52
|
+
|
53
|
+
def send_server_error(message = "there was a problem")
|
54
|
+
send_data(ServerError % message.to_s)
|
55
|
+
end
|
56
|
+
|
57
|
+
def validate_key(key)
|
58
|
+
if key.nil?
|
59
|
+
send_data(ClientError % "key cannot be blank")
|
60
|
+
elsif key && key.index(' ')
|
61
|
+
send_data(ClientError % "key cannot contain spaces")
|
62
|
+
nil
|
63
|
+
elsif key.size > 250
|
64
|
+
send_data(ClientError % "key must be less than 250 characters")
|
65
|
+
nil
|
66
|
+
else
|
67
|
+
key
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def set_incomplete(ss, length, part)
|
72
|
+
if ss.rest.size < length
|
73
|
+
@expected_size = length + ss.pre_match.size + 2
|
74
|
+
if @body
|
75
|
+
@body << ss.string
|
76
|
+
else
|
77
|
+
@body = part
|
78
|
+
@body << ss.rest
|
79
|
+
end
|
80
|
+
true
|
81
|
+
else
|
82
|
+
false
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def receive_data(data)
|
87
|
+
if @body
|
88
|
+
@body << data
|
89
|
+
return if @body.size < @expected_size
|
90
|
+
end
|
91
|
+
|
92
|
+
ss = StringScanner.new(@body || data)
|
93
|
+
|
94
|
+
while part = ss.scan_until(TerminatorRegex)
|
95
|
+
begin
|
96
|
+
command_argument_separator_index = part.index(/\s/)
|
97
|
+
command = part[0, command_argument_separator_index]
|
98
|
+
args = part[command_argument_separator_index + 1, part.size - command_argument_separator_index - 3]
|
99
|
+
case command
|
100
|
+
when 'get', 'gets'
|
101
|
+
keys = args.split(/\s+/)
|
102
|
+
keys.each do |k|
|
103
|
+
next unless validate_key(k)
|
104
|
+
if data = @cache.get(k)
|
105
|
+
if command == 'get'
|
106
|
+
send_data(GetValueReply % [k, data[:flags], data[:value].size])
|
107
|
+
else
|
108
|
+
send_data(CasValueReply % [k, data[:flags], data[:value].size, data[:value].hash])
|
109
|
+
end
|
110
|
+
send_data(data[:value])
|
111
|
+
send_data(Terminator)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
send_data(EndReply)
|
115
|
+
when 'set'
|
116
|
+
SetCommand.match(args) or (send_client_error and next)
|
117
|
+
(key, flags, expires, bytes, noreply) = [$1, Integer($2), Integer($3), Integer($4), !$5.nil?]
|
118
|
+
next unless validate_key(key)
|
119
|
+
return if set_incomplete(ss, bytes, part)
|
120
|
+
send_data(@cache.set(key, ss.rest[0, bytes.to_i], :flags => flags, :expires => expires) ?
|
121
|
+
StoredReply : NotStoredReply)
|
122
|
+
@body = nil
|
123
|
+
ss.pos += bytes + 2
|
124
|
+
when 'add'
|
125
|
+
SetCommand.match(args)
|
126
|
+
(key, flags, expires, bytes, noreply) = [$1, $2.to_i, $3.to_i, $4, !$5.nil?]
|
127
|
+
return if set_incomplete(ss, bytes, part)
|
128
|
+
send_data(@cache.add(key, ss.rest[0, bytes.to_i], :flags => flags, :expires => expires) ?
|
129
|
+
StoredReply : NotStoredReply)
|
130
|
+
@body = nil
|
131
|
+
ss.pos += bytes + 2
|
132
|
+
when 'replace'
|
133
|
+
SetCommand.match(args)
|
134
|
+
(key, flags, expires, bytes, noreply) = [$1, $2.to_i, $3.to_i, $4, !$5.nil?]
|
135
|
+
return if set_incomplete(ss, bytes, part)
|
136
|
+
send_data(@cache.replace(key, ss.rest[0, bytes.to_i], :flags => flags, :expires => expires) ?
|
137
|
+
StoredReply : NotStoredReply)
|
138
|
+
@body = nil
|
139
|
+
ss.pos += bytes + 2
|
140
|
+
when 'append'
|
141
|
+
SetCommand.match(args)
|
142
|
+
(key, flags, expires, bytes, noreply) = [$1, $2.to_i, $3.to_i, $4, !$5.nil?]
|
143
|
+
return if set_incomplete(ss, bytes, part)
|
144
|
+
send_data(@cache.append(key, ss.rest[0, bytes.to_i], :flags => flags, :expires => expires) ?
|
145
|
+
StoredReply : NotStoredReply)
|
146
|
+
@body = nil
|
147
|
+
ss.pos += bytes + 2
|
148
|
+
when 'prepend'
|
149
|
+
SetCommand.match(args)
|
150
|
+
(key, flags, expires, bytes, noreply) = [$1, $2.to_i, $3.to_i, $4, !$5.nil?]
|
151
|
+
return if set_incomplete(ss, bytes, part)
|
152
|
+
send_data(@cache.prepend(key, ss.rest[0, bytes.to_i], :flags => flags, :expires => expires) ?
|
153
|
+
StoredReply : NotStoredReply)
|
154
|
+
@body = nil
|
155
|
+
ss.pos += bytes + 2
|
156
|
+
when 'cas'
|
157
|
+
# do something
|
158
|
+
when 'delete'
|
159
|
+
case args
|
160
|
+
when DeleteWithTimeoutCommand
|
161
|
+
(key, timeout, noreply) = [$1.chomp, $2, !$3.nil?]
|
162
|
+
next unless validate_key(key)
|
163
|
+
send_data(@cache.delete_expire(key, timeout) ?
|
164
|
+
DeletedReply : NotDeletedReply)
|
165
|
+
when DeleteCommand
|
166
|
+
(key, noreply) = [$1.chomp, !$2.nil?]
|
167
|
+
next unless validate_key(key)
|
168
|
+
if special_delete_char && key.index(special_delete_char) == 0
|
169
|
+
key.slice!(0,special_delete_char.size)
|
170
|
+
@cache.delete_match(key)
|
171
|
+
send_data(DeletedReply)
|
172
|
+
else
|
173
|
+
send_data @cache.delete(key) ?
|
174
|
+
DeletedReply : NotDeletedReply
|
175
|
+
end
|
176
|
+
end
|
177
|
+
when 'delete_match'
|
178
|
+
DeleteMatchCommand.match(args)
|
179
|
+
(key, noreply) = [$1.chomp, !$2.nil?]
|
180
|
+
next unless validate_key(key)
|
181
|
+
@cache.delete_match(key)
|
182
|
+
send_data(DeletedReply)
|
183
|
+
when 'incr', 'decr'
|
184
|
+
IncrementDecrementCommand.match(args)
|
185
|
+
(key, value, noreply) = [$1, $2.to_i, !$3.nil?]
|
186
|
+
next unless validate_key(key)
|
187
|
+
send_data(if d = @cache.get(key)
|
188
|
+
value = -value if command == 'decr'
|
189
|
+
d['data'] = (val = (d['data'].to_i + value)).to_s
|
190
|
+
@cache.put(key, d)
|
191
|
+
ValueReply % val
|
192
|
+
else
|
193
|
+
NotFoundReply
|
194
|
+
end)
|
195
|
+
when 'stats'
|
196
|
+
send_data(Error)
|
197
|
+
when 'flush_all'
|
198
|
+
send_data(@cache.flush_all ? OK : Error)
|
199
|
+
when 'version'
|
200
|
+
send_data(Error)
|
201
|
+
when 'quit'
|
202
|
+
close_connection_after_writing
|
203
|
+
else
|
204
|
+
send_data(Error)
|
205
|
+
end
|
206
|
+
rescue
|
207
|
+
puts $!
|
208
|
+
puts $!.backtrace
|
209
|
+
send_server_error($!)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
214
|
+
|
215
|
+
end
|
216
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
module ::ActiveSupport
|
2
|
+
module Cache
|
3
|
+
class MemCacheStore < Store
|
4
|
+
|
5
|
+
def delete_matched(matcher, options = nil) # :nodoc:
|
6
|
+
super
|
7
|
+
response = @data.delete_match(matcher)
|
8
|
+
response == Response::DELETED
|
9
|
+
rescue MemCache::MemCacheError => e
|
10
|
+
logger.error("MemCacheError (#{e}): #{e.message}")
|
11
|
+
false
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class ::MemCache
|
19
|
+
|
20
|
+
def delete_match(key)
|
21
|
+
@mutex.lock if @multithread
|
22
|
+
|
23
|
+
raise MemCacheError, "No active servers" unless active?
|
24
|
+
cache_key = make_cache_key key
|
25
|
+
server = get_server_for_key cache_key
|
26
|
+
|
27
|
+
sock = server.socket
|
28
|
+
raise MemCacheError, "No connection to server" if sock.nil?
|
29
|
+
|
30
|
+
begin
|
31
|
+
sock.write "delete_match #{cache_key}\r\n"
|
32
|
+
result = sock.gets
|
33
|
+
raise_on_error_response! result
|
34
|
+
result
|
35
|
+
rescue SocketError, SystemCallError, IOError => err
|
36
|
+
server.close
|
37
|
+
raise MemCacheError, err.message
|
38
|
+
end
|
39
|
+
ensure
|
40
|
+
@mutex.unlock if @multithread
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
data/spec/cache_spec.rb
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
require 'lib/tokyo_cache_cow/cache'
|
3
|
+
|
4
|
+
{
|
5
|
+
'tokyo cabinet memcache' => TokyoCacheCow::Cache::TokyoCabinetMemcache.new(:file => '/tmp/tcc1'),
|
6
|
+
'hash memcache' => TokyoCacheCow::Cache::HashMemcache.new,
|
7
|
+
'file memcache' => TokyoCacheCow::Cache::FileMemcache.new(:file => '/tmp/filecache')
|
8
|
+
}.each do |name, cache|
|
9
|
+
describe name do
|
10
|
+
|
11
|
+
before(:each) do
|
12
|
+
cache.flush_all.should == true
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should add" do
|
16
|
+
cache.get('added_key').should == nil
|
17
|
+
cache.add('added_key','zig').should == true
|
18
|
+
cache.get('added_key')[:value].should == 'zig'
|
19
|
+
cache.add('added_key','ziglar')
|
20
|
+
cache.get('added_key')[:value].should == 'zig'
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should put & get" do
|
24
|
+
100.times do |i|
|
25
|
+
cache.set("/blog/show/#{i}","this is a big ol' blog post!!! #{i}")
|
26
|
+
end
|
27
|
+
|
28
|
+
100.times do |i|
|
29
|
+
cache.get("/blog/show/#{i}")[:value].should == "this is a big ol' blog post!!! #{i}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should delete" do
|
34
|
+
cache.set("key-set-123","you should never see me")
|
35
|
+
cache.get("key-set-123")[:value].should == "you should never see me"
|
36
|
+
cache.delete("key-set-123")
|
37
|
+
cache.get("key-set-123").should == nil
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should delete (with expiry)" do
|
41
|
+
cache.set('delete-with-expiry', 'hillbillies')
|
42
|
+
cache.get('delete-with-expiry')[:value].should == 'hillbillies'
|
43
|
+
cache.delete('delete-with-expiry', :expires => 3)
|
44
|
+
cache.get('delete-with-expiry').should == nil
|
45
|
+
cache.replace('delete-with-expiry', 'more hillbillies')
|
46
|
+
cache.get('delete-with-expiry').should == nil
|
47
|
+
sleep(5)
|
48
|
+
cache.get('delete-with-expiry').should == nil
|
49
|
+
cache.set('delete-with-expiry', 'more hillbillies')
|
50
|
+
cache.get('delete-with-expiry')[:value].should == 'more hillbillies'
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should delete (with expiry) and set again" do
|
54
|
+
cache.set('delete-with-expiry', 'hillbillies')
|
55
|
+
cache.get('delete-with-expiry')[:value].should == 'hillbillies'
|
56
|
+
cache.delete('delete-with-expiry', 3)
|
57
|
+
cache.get('delete-with-expiry').should == nil
|
58
|
+
cache.set('delete-with-expiry', 'more hillbillies')
|
59
|
+
cache.get('delete-with-expiry')[:value].should == 'more hillbillies'
|
60
|
+
sleep(5)
|
61
|
+
cache.get('delete-with-expiry')[:value].should == 'more hillbillies'
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should delete_match" do
|
65
|
+
100.times do
|
66
|
+
cache.set("asd/qwe/zxc/10","you should never see me")
|
67
|
+
cache.set("asd/qwe/zxc/20","you should never see me")
|
68
|
+
cache.set("asd/qwe/zxc/30","you should never see me")
|
69
|
+
cache.set("asd/qwe/zxc/40","you should never see me")
|
70
|
+
cache.set("asd/qwe/zxc/11","you should never see me")
|
71
|
+
cache.set("asd/qwe/zxc/21","you should never see me")
|
72
|
+
cache.set("asd/qwe/zxc/31","you should never see me")
|
73
|
+
cache.set("asd/qwe/zxc/41","you should never see me")
|
74
|
+
cache.set("asd/qwe/zxc/12","you should never see me")
|
75
|
+
cache.set("asd/qwe/zxc/22","you should never see me")
|
76
|
+
cache.set("asd/qwe/zxc/32","you should never see me")
|
77
|
+
cache.set("asd/qwe/zxc/42","you should never see me")
|
78
|
+
cache.set("asd/qwe/zxc/101","you should never see me")
|
79
|
+
cache.set("asd/qwe/zxc/201","you should never see me")
|
80
|
+
cache.set("asd/qwe/zxc/301","you should never see me")
|
81
|
+
cache.set("asd/qwe/zxc/401","you should never see me")
|
82
|
+
cache.set("asd/qwe/zxc/111","you should never see me")
|
83
|
+
cache.set("asd/qwe/zxc/211","you should never see me")
|
84
|
+
cache.set("asd/qwe/zxc/311","you should never see me")
|
85
|
+
cache.set("asd/qwe/zxc/411","you should never see me")
|
86
|
+
cache.set("asd/qwe/zxc/121","you should never see me")
|
87
|
+
cache.set("asd/qwe/zxc/221","you should never see me")
|
88
|
+
cache.set("asd/qwe/zxc/321","you should never see me")
|
89
|
+
cache.set("asd/qwe/zxc/421","you should never see me")
|
90
|
+
cache.delete_match("asd/qwe/zxc")
|
91
|
+
cache.get("asd/qwe/zxc/40").should == nil
|
92
|
+
cache.get("asd/qwe/zxc/30").should == nil
|
93
|
+
cache.get("asd/qwe/zxc/20").should == nil
|
94
|
+
cache.get("asd/qwe/zxc/10").should == nil
|
95
|
+
cache.get("asd/qwe/zxc/41").should == nil
|
96
|
+
cache.get("asd/qwe/zxc/31").should == nil
|
97
|
+
cache.get("asd/qwe/zxc/21").should == nil
|
98
|
+
cache.get("asd/qwe/zxc/11").should == nil
|
99
|
+
cache.get("asd/qwe/zxc/42").should == nil
|
100
|
+
cache.get("asd/qwe/zxc/32").should == nil
|
101
|
+
cache.get("asd/qwe/zxc/22").should == nil
|
102
|
+
cache.get("asd/qwe/zxc/12").should == nil
|
103
|
+
cache.get("asd/qwe/zxc/401").should == nil
|
104
|
+
cache.get("asd/qwe/zxc/301").should == nil
|
105
|
+
cache.get("asd/qwe/zxc/201").should == nil
|
106
|
+
cache.get("asd/qwe/zxc/101").should == nil
|
107
|
+
cache.get("asd/qwe/zxc/411").should == nil
|
108
|
+
cache.get("asd/qwe/zxc/311").should == nil
|
109
|
+
cache.get("asd/qwe/zxc/211").should == nil
|
110
|
+
cache.get("asd/qwe/zxc/111").should == nil
|
111
|
+
cache.get("asd/qwe/zxc/421").should == nil
|
112
|
+
cache.get("asd/qwe/zxc/321").should == nil
|
113
|
+
cache.get("asd/qwe/zxc/221").should == nil
|
114
|
+
cache.get("asd/qwe/zxc/121").should == nil
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should expire" do
|
119
|
+
cache.set("expiring key","you should never see me", :expires => 1)
|
120
|
+
sleep(3)
|
121
|
+
cache.get("expiring key").should == nil
|
122
|
+
end
|
123
|
+
|
124
|
+
it "should replace" do
|
125
|
+
cache.replace("replacing-key", "newkey")
|
126
|
+
cache.get("replacing-key").should == nil
|
127
|
+
cache.set("replacing-key", "oldkey")
|
128
|
+
cache.replace("replacing-key", "newkey")
|
129
|
+
cache.get("replacing-key")[:value].should == 'newkey'
|
130
|
+
end
|
131
|
+
|
132
|
+
it "should append" do
|
133
|
+
cache.set("appending-key", "test1")
|
134
|
+
cache.get("appending-key")[:value].should == "test1"
|
135
|
+
cache.append("appending-key", "test2")
|
136
|
+
cache.get("appending-key")[:value].should == "test1test2"
|
137
|
+
end
|
138
|
+
|
139
|
+
it "should incr" do
|
140
|
+
cache.set("incr-key", 123)
|
141
|
+
cache.incr("incr-key", 20).should == 143
|
142
|
+
cache.get("incr-key")[:value].should == '143'
|
143
|
+
end
|
144
|
+
|
145
|
+
it "should decr" do
|
146
|
+
cache.set("decr-key", 123)
|
147
|
+
cache.decr("decr-key", 20).should == 103
|
148
|
+
cache.get("decr-key")[:value].should == '103'
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
end
|
data/spec/server_spec.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'memcached'
|
3
|
+
require 'benchmark'
|
4
|
+
require 'lib/tokyo_cache_cow'
|
5
|
+
|
6
|
+
cache = Memcached.new('127.0.0.1:11211')
|
7
|
+
|
8
|
+
describe 'memcache server' do
|
9
|
+
|
10
|
+
before(:all) do
|
11
|
+
runner = TokyoCacheCow::Runner.new(['--daemonize'])
|
12
|
+
@pid = runner.start!
|
13
|
+
sleep(1)
|
14
|
+
end
|
15
|
+
|
16
|
+
before(:each) do
|
17
|
+
cache.flush
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should get & set" do
|
21
|
+
cache.set('asd', "qweqweasd" * 20000)
|
22
|
+
cache.get('asd').should == "qweqweasd" * 20000
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should delete" do
|
26
|
+
cache.set('asd', 'qwe')
|
27
|
+
cache.delete('asd')
|
28
|
+
proc {cache.get('asd')}.should raise_error Memcached::NotFound
|
29
|
+
end
|
30
|
+
|
31
|
+
after(:all) do
|
32
|
+
Process.kill('INT', @pid)
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
describe 'memcache server with special delete support' do
|
39
|
+
|
40
|
+
before(:all) do
|
41
|
+
runner = TokyoCacheCow::Runner.new(['--daemonize', '-m...'])
|
42
|
+
@pid = runner.start!
|
43
|
+
sleep(1)
|
44
|
+
end
|
45
|
+
|
46
|
+
before(:each) do
|
47
|
+
cache.flush
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should delete match through special char " do
|
51
|
+
cache.set('asd123', "qweqweasd")
|
52
|
+
cache.set('asd456', "qweqweasd")
|
53
|
+
cache.set('asd678', "qweqweasd")
|
54
|
+
cache.set('qwe678', "qweqweasd")
|
55
|
+
cache.set('qwe679', "qweqweasd")
|
56
|
+
cache.delete('...asd')
|
57
|
+
proc {cache.get('asd123')}.should raise_error Memcached::NotFound
|
58
|
+
proc {cache.get('asd456')}.should raise_error Memcached::NotFound
|
59
|
+
proc {cache.get('asd678')}.should raise_error Memcached::NotFound
|
60
|
+
cache.delete('...qwe678')
|
61
|
+
proc {cache.get('qwe678')}.should raise_error Memcached::NotFound
|
62
|
+
cache.get('qwe679').should=='qweqweasd'
|
63
|
+
end
|
64
|
+
|
65
|
+
after(:all) do
|
66
|
+
Process.kill('INT', @pid)
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
end
|
data/spec/spec.opts
ADDED
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tokyo_cache_cow
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.4
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Joshua Hull
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-11-24 00:00:00 -05:00
|
13
|
+
default_executable: tokyo_cache_cow
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: ""
|
17
|
+
email: joshbuddy@gmail.com
|
18
|
+
executables:
|
19
|
+
- tokyo_cache_cow
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.rdoc
|
24
|
+
files:
|
25
|
+
- README.rdoc
|
26
|
+
- Rakefile
|
27
|
+
- VERSION.yml
|
28
|
+
- bin/tokyo_cache_cow
|
29
|
+
- lib/tokyo_cache_cow.rb
|
30
|
+
- lib/tokyo_cache_cow/cache.rb
|
31
|
+
- lib/tokyo_cache_cow/cache/base.rb
|
32
|
+
- lib/tokyo_cache_cow/cache/file_memcache.rb
|
33
|
+
- lib/tokyo_cache_cow/cache/hash_memcache.rb
|
34
|
+
- lib/tokyo_cache_cow/cache/tokyo_cabinet_memcache.rb
|
35
|
+
- lib/tokyo_cache_cow/providers.rb
|
36
|
+
- lib/tokyo_cache_cow/runner.rb
|
37
|
+
- lib/tokyo_cache_cow/server.rb
|
38
|
+
- rails/init.rb
|
39
|
+
- spec/cache_spec.rb
|
40
|
+
- spec/server_spec.rb
|
41
|
+
- spec/spec.opts
|
42
|
+
has_rdoc: true
|
43
|
+
homepage: http://github.com/joshbuddy/tokyo_cache_cow
|
44
|
+
licenses: []
|
45
|
+
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options:
|
48
|
+
- --charset=UTF-8
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: "0"
|
56
|
+
version:
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: "0"
|
62
|
+
version:
|
63
|
+
requirements: []
|
64
|
+
|
65
|
+
rubyforge_project:
|
66
|
+
rubygems_version: 1.3.5
|
67
|
+
signing_key:
|
68
|
+
specification_version: 3
|
69
|
+
summary: ""
|
70
|
+
test_files:
|
71
|
+
- spec/cache_spec.rb
|
72
|
+
- spec/server_spec.rb
|