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 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
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 4
3
+ :major: 0
4
+ :minor: 0
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby -rubygems
2
+ # Tokyo cache cow command line interface script.
3
+ # Run <tt>tokyo_cache_cow -h</tt> to get more usage.
4
+ require File.dirname(__FILE__) + '/../lib/tokyo_cache_cow'
5
+
6
+ TokyoCacheCow::Runner.new(ARGV).start!
@@ -0,0 +1,15 @@
1
+ class TokyoCacheCow
2
+ class Cache
3
+ class 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
+ end
14
+ end
15
+ end
@@ -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
@@ -0,0 +1,5 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'tokyo_cache_cow/runner'
4
+ require 'tokyo_cache_cow/server'
5
+ require 'tokyo_cache_cow/cache'
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
@@ -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
@@ -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
@@ -0,0 +1,8 @@
1
+ -rubygems
2
+ --colour
3
+ --format
4
+ specdoc
5
+ --loadby
6
+ mtime
7
+ --reverse
8
+ --backtrace
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