cache 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -4,3 +4,4 @@ Gemfile.lock
4
4
  pkg/*
5
5
  secret.sh
6
6
  rdoc/*
7
+ .yardoc
data/BENCHMARKS CHANGED
@@ -1,3 +1,101 @@
1
+ # sabshere 3/4/11 v0.2.0
2
+
3
+ vidalia:~/github/cache (master) $ ruby test/profile/benchmark.rb
4
+ Darwin vidalia 9.8.0 Darwin Kernel Version 9.8.0: Wed Jul 15 16:55:01 PDT 2009; root:xnu-1228.15.4~1/RELEASE_I386 i386
5
+ ruby 1.8.7 (2010-05-25 patchlevel 266) [i686-darwin9.8.0]
6
+ RUBY_VERSION=ruby-1.8.7-head
7
+ Ruby 1.8.7p266
8
+ Loaded memcached 1.2
9
+ Loaded remix-stash 1.1.3
10
+ Loaded memcache-client 1.8.5
11
+ Loaded cache 0.1.2 # i just hadn't released the gem yet
12
+ Loaded kgio 2.3.2
13
+ Loaded dalli 1.0.2
14
+ Loops is 20000
15
+ Stack depth is 0
16
+ Small value size is: 13 bytes
17
+ Large value size is: 4158 bytes
18
+ No matching processes belonging to you were found
19
+ user system total real
20
+ set: cache:dalli:bin 5.720000 1.860000 7.580000 ( 10.301808)
21
+ set: cache:libm:bin 1.250000 1.270000 2.520000 ( 5.892224)
22
+ set: dalli:bin 5.430000 1.860000 7.290000 ( 9.966409)
23
+ set: libm:ascii 0.760000 1.330000 2.090000 ( 5.348975)
24
+ set: libm:ascii:pipeline 0.280000 0.010000 0.290000 ( 0.297230)
25
+ set: libm:ascii:udp 0.650000 0.710000 1.360000 ( 3.593454)
26
+ set: libm:bin 0.640000 1.360000 2.000000 ( 5.285160)
27
+ set: libm:bin:buffer 0.270000 0.160000 0.430000 ( 1.234561)
28
+ set: mclient:ascii 11.330000 3.780000 15.110000 ( 16.174666)
29
+ set: stash:bin 3.420000 1.330000 4.750000 ( 8.016146)
30
+
31
+ get: cache:dalli:bin 5.870000 1.950000 7.820000 ( 10.644558)
32
+ get: cache:libm:bin 1.370000 1.200000 2.570000 ( 5.946502)
33
+ get: dalli:bin 5.580000 2.070000 7.650000 ( 10.361689)
34
+ get: libm:ascii 0.980000 1.300000 2.280000 ( 5.455222)
35
+ get: libm:ascii:pipeline 1.030000 1.590000 2.620000 ( 5.875592)
36
+ get: libm:ascii:udp 0.820000 0.730000 1.550000 ( 3.515632)
37
+ get: libm:bin 0.830000 1.330000 2.160000 ( 5.381290)
38
+ get: libm:bin:buffer 0.900000 1.630000 2.530000 ( 5.761412)
39
+ get: mclient:ascii 13.630000 3.870000 17.500000 ( 17.912800)
40
+ get: stash:bin 3.100000 1.340000 4.440000 ( 7.667182)
41
+
42
+ delete: cache:dalli:bin 6.270000 2.390000 8.660000 ( 11.313843)
43
+ delete: cache:libm:bin 2.190000 1.560000 3.750000 ( 7.459292)
44
+ delete: dalli:bin 5.660000 2.340000 8.000000 ( 10.374507)
45
+ delete: libm:ascii 1.850000 1.570000 3.420000 ( 6.922356)
46
+ delete: libm:ascii:pipeline 0.230000 0.010000 0.240000 ( 0.242642)
47
+ delete: libm:ascii:udp 1.690000 0.930000 2.620000 ( 4.749489)
48
+ delete: libm:bin 1.780000 1.530000 3.310000 ( 6.768449)
49
+ delete: libm:bin:buffer 1.890000 1.880000 3.770000 ( 7.149319)
50
+ delete: mclient:ascii 11.940000 4.040000 15.980000 ( 16.423645)
51
+ delete:stash:bin => #<NoMethodError: undefined method `delete' for #<Remix::Stash:0x1249824>>
52
+
53
+ get-missing: cache:dalli:bin 5.530000 2.140000 7.670000 ( 10.187561)
54
+ get-missing: cache:libm:bin 2.200000 1.510000 3.710000 ( 6.953625)
55
+ get-missing: dalli:bin 5.230000 2.160000 7.390000 ( 9.891539)
56
+ get-missing: libm:ascii 1.960000 1.420000 3.380000 ( 6.459433)
57
+ get-missing: libm:ascii:pipeline 2.080000 1.860000 3.940000 ( 6.984890)
58
+ get-missing: libm:ascii:udp 1.730000 0.910000 2.640000 ( 4.582510)
59
+ get-missing: libm:bin 1.950000 1.470000 3.420000 ( 6.854775)
60
+ get-missing: libm:bin:buffer 2.060000 1.900000 3.960000 ( 7.180759)
61
+ get-missing: mclient:ascii 12.350000 4.100000 16.450000 ( 16.986064)
62
+ get-missing: stash:bin 3.110000 1.410000 4.520000 ( 7.908972)
63
+
64
+ set-large: cache:dalli:bin 8.650000 2.110000 10.760000 ( 13.864690)
65
+ set-large: cache:libm:bin 2.600000 1.450000 4.050000 ( 7.811509)
66
+ set-large: dalli:bin 8.250000 2.100000 10.350000 ( 13.370233)
67
+ set-large: libm:ascii 0.890000 1.460000 2.350000 ( 6.069504)
68
+ set-large: libm:ascii:pipeline 0.570000 0.450000 1.020000 ( 1.232906)
69
+ set-large: libm:ascii:udp 0.730000 0.850000 1.580000 ( 4.174216)
70
+ set-large: libm:bin 0.760000 1.510000 2.270000 ( 5.966586)
71
+ set-large: libm:bin:buffer 0.530000 0.700000 1.230000 ( 2.419153)
72
+ set-large: mclient:ascii 13.540000 4.250000 17.790000 ( 18.447711)
73
+ set-large: stash:bin 6.100000 1.500000 7.600000 ( 11.216811)
74
+
75
+ get-large: cache:dalli:bin 8.000000 2.480000 10.480000 ( 13.450322)
76
+ get-large: cache:libm:bin 6.820000 1.490000 8.310000 ( 12.331486)
77
+ get-large: dalli:bin 7.670000 2.490000 10.160000 ( 13.148162)
78
+ get-large: libm:ascii 1.740000 1.540000 3.280000 ( 7.256110)
79
+ get-large: libm:ascii:pipeline 1.870000 1.960000 3.830000 ( 7.723493)
80
+ get-large: libm:ascii:udp 1.560000 0.980000 2.540000 ( 4.823281)
81
+ get-large: libm:bin 1.590000 1.590000 3.180000 ( 7.018376)
82
+ get-large: libm:bin:buffer 1.730000 1.960000 3.690000 ( 7.365047)
83
+ get-large: mclient:ascii 17.160000 4.890000 22.050000 ( 22.647077)
84
+ get-large: stash:bin 3.290000 1.440000 4.730000 ( 7.950154)
85
+
86
+ hash:jenkins 0.550000 0.000000 0.550000 ( 0.561738)
87
+ hash:default 0.550000 0.000000 0.550000 ( 0.553209)
88
+ hash:crc 0.660000 0.000000 0.660000 ( 0.657155)
89
+ hash:fnv1_32 0.530000 0.010000 0.540000 ( 0.529303)
90
+ hash:hsieh 0.280000 0.000000 0.280000 ( 0.281685)
91
+ hash:fnv1_64 1.160000 0.000000 1.160000 ( 1.169524)
92
+ hash:none 0.310000 0.000000 0.310000 ( 0.313095)
93
+ hash:murmur 0.450000 0.000000 0.450000 ( 0.454460)
94
+ hash:md5 0.950000 0.010000 0.960000 ( 0.949226)
95
+ hash:fnv1a_64 0.590000 0.000000 0.590000 ( 0.590362)
96
+ hash:fnv1a_32 0.590000 0.000000 0.590000 ( 0.599005)
97
+
98
+
1
99
  # sabshere 2/22/11 v0.0.3
2
100
 
3
101
  vidalia:~/github/cache (master) $ ruby test/profile/benchmark.rb
data/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # cache
2
+
3
+ A unified cache handling interface, inspired by libraries like [ActiveSupport::Cache::Store](http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html), Perl's [Cache::Cache](http://cpan.uwinnipeg.ca/module/Cache::Cache), and [CHI](http://cpan.uwinnipeg.ca/module/CHI).
4
+
5
+ ## Supported methods
6
+
7
+ It will translate these methods to whatever Redis, Memcached, etc. client you're using:
8
+
9
+ @cache.get 'hello'
10
+ @cache.set 'hello', 'world', 5.minutes
11
+ @cache.delete 'hello'
12
+ @cache.flush
13
+ @cache.exist? 'hello'
14
+ @cache.reset
15
+ @cache.fetch 'hello' { 'world' }
16
+ @cache.cas 'hello' { |current| 'world' }
17
+ @cache.increment 'high-fives'
18
+ @cache.decrement 'high-fives'
19
+
20
+ Also provided for Rails compatibility:
21
+
22
+ @cache.write 'hello', 'world', :expires_in => 5.minutes
23
+ @cache.read 'hello'
24
+ @cache.clear
25
+ @cache.compare_and_swap
26
+ @cache.read_multi 'hello', 'privyet', 'hallo'
27
+
28
+ ## Rationale
29
+
30
+ I wanted a common interface to a bunch of great Ruby cache clients so I can develop gems (lock_method, cache_method) that accept any of them.
31
+
32
+ * I'm tired of rescuing from Memcached::NotFound
33
+ * I'm tired of forgetting whether it's :expires_in or :ttl
34
+ * I don't know why we ever started using read/write instead of get/set.
35
+ * I don't like how you have to manually handle after_fork for Redis, Memcached, etc.
36
+ * I don't know why Memcached::Rails isn't implemented as an ActiveRecord::Cache::Store (Dalli did it just fine!)
37
+ * Why are you asking me about :raw or whatever? Just marshal it
38
+
39
+ ## How you might use it
40
+
41
+ <table>
42
+ <tr>
43
+ <th>&nbsp;</th>
44
+ <th><a href="https://github.com/fauna/memcached">Super-fast memcached</a></th>
45
+ <th><a href="https://github.com/mperham/dalli">Pure Ruby memcached</a> (works on <a href="http://devcenter.heroku.com/articles/memcache">Heroku</a>)</th>
46
+ <th><a href="https://redistogo.com/">Redis</a></th>
47
+ </tr>
48
+ <tr>
49
+ <td><a href="http://guides.rubyonrails.org/caching_with_rails.html#cache-stores">Rails</a></td>
50
+ <td><pre>config.cache_store = Cache.wrap(Memcached.new)</pre></td>
51
+ <td><pre>config.cache_store = Cache.wrap(Dalli::Client.new)</pre></td>
52
+ <td><pre>config.cache_store = Cache.wrap(Redis.new)</pre></td>
53
+ </tr>
54
+ <tr>
55
+ <td><a href="https://github.com/seamusabshere/cache_method">CacheMethod</a> (already uses Cache internally)</td>
56
+ <td><pre>CacheMethod.config.storage = Memcached.new</pre></td>
57
+ <td><pre>CacheMethod.config.storage = Dalli::Client.new</pre></td>
58
+ <td><pre>CacheMethod.config.storage = Redis.new</pre></td>
59
+ </tr>
60
+ <tr>
61
+ <td><a href="https://github.com/seamusabshere/lock_method">LockMethod</a> (already uses Cache internally)</td>
62
+ <td><pre>LockMethod.config.storage = Memcached.new</pre></td>
63
+ <td><pre>LockMethod.config.storage = Dalli::Client.new</pre></td>
64
+ <td><pre>LockMethod.config.storage = Redis.new</pre></td>
65
+ </tr>
66
+ <tr>
67
+ <td>Your own library</td>
68
+ <td>
69
+ <pre>
70
+ # Accept any client, let Cache take care of it
71
+ def cache=(raw_client)
72
+ @cache = Cache.wrap(raw_client)
73
+ end
74
+ </pre>
75
+ </td>
76
+ <td>
77
+ <pre>
78
+ # Accept any client, let Cache take care of it
79
+ def cache=(raw_client)
80
+ @cache = Cache.wrap(raw_client)
81
+ end
82
+ </pre>
83
+ </td>
84
+ <td>
85
+ <pre>
86
+ # Accept any client, let Cache take care of it
87
+ def cache=(raw_client)
88
+ @cache = Cache.wrap(raw_client)
89
+ end
90
+ </pre>
91
+ </td>
92
+ </tr>
93
+ </table>
94
+
95
+ ## Forking/threading
96
+
97
+ When you use a Cache object to wrap Memcached or Redis, you don't have to worry about forking or threading.
98
+
99
+ For example, you don't have to set up unicorn or PhusionPassenger's <tt>after_fork</tt>.
100
+
101
+ ## TTL
102
+
103
+ 0 means don't expire.
104
+
105
+ ## Other examples
106
+
107
+ It defaults to an in-process memory store:
108
+
109
+ @cache = Cache.new
110
+ @cache.set 'hello'
111
+ @cache.get 'hello', 'world'
112
+
113
+ You can specify a more useful cache client:
114
+
115
+ require 'memcached' # a really fast memcached client gem
116
+ require 'cache' # this gem, which provides a standard interface
117
+ raw_client = Memcached.new '127.0.0.1:11211'
118
+ @cache = Cache.wrap(raw_client)
119
+
120
+ or
121
+
122
+ require 'redis' # the redis key-value store
123
+ require 'cache' # this gem, which provides a standard interface
124
+ raw_client = Redis.new
125
+ @cache = Cache.wrap(raw_client)
126
+
127
+ or
128
+
129
+ require 'dalli' # the dalli memcached client used by heroku
130
+ require 'cache' # this gem, which provides a standard interface
131
+ raw_client = Dalli::Client.new
132
+ @cache = Cache.wrap(raw_client)
133
+
134
+ Don't know why you would ever want to do this:
135
+
136
+ # Piggyback off the default rails cache
137
+ @cache = Cache.wrap(Rails.cache)
138
+
139
+ ## Supported clients
140
+
141
+ Supported memcached clients:
142
+
143
+ * [memcached](https://github.com/fauna/memcached) (super fast!)
144
+ * [dalli](https://github.com/mperham/dalli) (pure ruby, recommended if you're on heroku)
145
+ * [memcache-client](https://github.com/mperham/memcache-client) (not recommended. the one that comes with Rails.)
146
+
147
+ Supported Redis clients:
148
+
149
+ * [redis](https://github.com/ezmobius/redis-rb)
150
+
151
+ ## Copyright
152
+
153
+ Copyright 2011 Seamus Abshere
data/Rakefile CHANGED
@@ -11,12 +11,31 @@ end
11
11
 
12
12
  task :default => :test
13
13
 
14
- require 'rake/rdoctask'
15
- Rake::RDocTask.new do |rdoc|
16
- version = File.exist?('VERSION') ? File.read('VERSION') : ""
14
+ # http://stackoverflow.com/questions/2138427/can-i-get-my-readme-textile-into-my-rdoc-with-proper-formatting
15
+ desc "Generate RDoc"
16
+ task :rdoc => ['doc:generate']
17
+ namespace :doc do
18
+ project_root = File.expand_path(File.dirname(__FILE__))
19
+ doc_destination = File.join(project_root, 'rdoc')
20
+ files = Dir.glob(File.join(project_root, 'lib', '**', '*.rb')) + [ File.join(project_root, 'README.md') ]
21
+ begin
22
+ require 'yard'
23
+ require 'yard/rake/yardoc_task'
24
+
25
+ YARD::Rake::YardocTask.new(:generate) do |yt|
26
+ yt.files = files
27
+ yt.options = ['--output-dir', doc_destination, '--readme', 'README.md']
28
+ end
29
+ rescue LoadError
30
+ desc "Generate YARD Documentation"
31
+ task :generate do
32
+ abort "Please install the YARD gem to generate rdoc."
33
+ end
34
+ end
35
+
36
+ desc "Remove generated documenation"
37
+ task :clean do
38
+ rm_r doc_dir if File.exists?(doc_destination)
39
+ end
17
40
 
18
- rdoc.rdoc_dir = 'rdoc'
19
- rdoc.title = "cache #{version}"
20
- rdoc.rdoc_files.include('README*')
21
- rdoc.rdoc_files.include('lib/**/*.rb')
22
41
  end
data/cache.gemspec CHANGED
@@ -21,6 +21,7 @@ Gem::Specification.new do |s|
21
21
 
22
22
  s.add_dependency 'activesupport', '>=2.3.11' # for default memory store
23
23
  s.add_dependency 'i18n' # activesupport
24
+ s.add_development_dependency 'yard'
24
25
  s.add_development_dependency 'test-unit'
25
26
  s.add_development_dependency 'redis'
26
27
  s.add_development_dependency 'dalli'
data/lib/cache.rb CHANGED
@@ -3,9 +3,9 @@ class Cache
3
3
  autoload :Config, 'cache/config'
4
4
  autoload :Storage, 'cache/storage'
5
5
 
6
- # Create a new Cache object by passing it a client of your choice.
6
+ # Create a new Cache instance by wrapping a client of your choice.
7
7
  #
8
- # Defaults to an in-process memory store, but you probably don't want that.
8
+ # Defaults to an in-process memory store.
9
9
  #
10
10
  # Supported memcached clients:
11
11
  # * memcached[https://github.com/fauna/memcached] (either a Memcached or a Memcached::Rails)
@@ -17,8 +17,12 @@ class Cache
17
17
  #
18
18
  # Example:
19
19
  # raw_client = Memcached.new('127.0.0.1:11211')
20
- # cache = Cache.new raw_client
21
- def initialize(client = nil)
20
+ # cache = Cache.wrap raw_client
21
+ def self.wrap(client = nil)
22
+ new client
23
+ end
24
+
25
+ def initialize(client = nil) #:nodoc:
22
26
  config.client = client
23
27
  end
24
28
 
@@ -34,7 +38,7 @@ class Cache
34
38
  #
35
39
  # Example:
36
40
  # cache.get 'hello'
37
- def get(k)
41
+ def get(k, ignored_options = nil)
38
42
  storage.get k
39
43
  end
40
44
 
@@ -43,7 +47,7 @@ class Cache
43
47
  # Example:
44
48
  # cache.set 'hello', 'world'
45
49
  # cache.set 'hello', 'world', 80 # seconds til it expires
46
- def set(k, v, ttl = nil)
50
+ def set(k, v, ttl = nil, ignored_options = nil)
47
51
  storage.set k, v, ttl
48
52
  end
49
53
 
@@ -51,7 +55,7 @@ class Cache
51
55
  #
52
56
  # Example:
53
57
  # cache.delete 'hello'
54
- def delete(k)
58
+ def delete(k, ignored_options = nil)
55
59
  storage.delete k
56
60
  end
57
61
 
@@ -62,4 +66,73 @@ class Cache
62
66
  def flush
63
67
  storage.flush
64
68
  end
69
+
70
+ alias :clear :flush
71
+
72
+ # Check if something exists.
73
+ #
74
+ # Example:
75
+ # cache.exist? 'hello'
76
+ def exist?(k, ignored_options = nil)
77
+ storage.exist? k
78
+ end
79
+
80
+ # Increment a value.
81
+ #
82
+ # Example:
83
+ # cache.increment 'high-fives'
84
+ def increment(k, amount = 1, ignored_options = nil)
85
+ storage.increment k, amount
86
+ end
87
+
88
+ # Decrement a value.
89
+ #
90
+ # Example:
91
+ # cache.decrement 'high-fives'
92
+ def decrement(k, amount = 1, ignored_options = nil)
93
+ storage.decrement k, amount
94
+ end
95
+
96
+ # Reset the cache connection. You shouldn't really use this, because it happens automatically on forking/threading.
97
+ def reset
98
+ storage.reset
99
+ end
100
+
101
+ # Try to get a value and if it doesn't exist, set it to the result of the block.
102
+ #
103
+ # Example:
104
+ # cache.fetch 'hello' { 'world' }
105
+ def fetch(k, options_ignored_except_expires_in = {}, &blk)
106
+ storage.fetch k, options_ignored_except_expires_in[:expires_in], &blk
107
+ end
108
+
109
+ # Get the current value (if any), pass it into a block, and set the result.
110
+ #
111
+ # Example:
112
+ # cache.cas 'hello' { |current| 'world' }
113
+ def cas(k, ttl = nil, &blk)
114
+ storage.cas k, ttl, &blk
115
+ end
116
+
117
+ alias :compare_and_swap :cas
118
+
119
+ # Get stats.
120
+ #
121
+ # Example:
122
+ # cache.stats
123
+ def stats
124
+ storage.stats
125
+ end
126
+
127
+ def write(k, v, options_ignored_except_expires_in = {}) #:nodoc:
128
+ storage.set k, v, options_ignored_except_expires_in[:expires_in]
129
+ end
130
+
131
+ def read(k, ignored_options = nil) #:nodoc:
132
+ storage.get k
133
+ end
134
+
135
+ def read_multi(*ks) #:nodoc:
136
+ ks.map { |k| storage.get k }
137
+ end
65
138
  end
data/lib/cache/storage.rb CHANGED
@@ -12,17 +12,17 @@ class Cache
12
12
  def get(k)
13
13
  reset_if_forked_or_threaded
14
14
  if memcached?
15
- begin; bare_client.get(k); rescue ::Memcached::NotFound; nil; end
15
+ begin; bare.get(k); rescue ::Memcached::NotFound; nil; end
16
16
  elsif dalli? or memcached_rails? or mem_cache?
17
- bare_client.get k
17
+ bare.get k
18
18
  elsif redis?
19
- if cached_v = bare_client.get(k) and cached_v.is_a?(::String)
19
+ if cached_v = bare.get(k) and cached_v.is_a?(::String)
20
20
  ::Marshal.load cached_v
21
21
  end
22
22
  elsif active_support_store?
23
- bare_client.read k
23
+ bare.read k
24
24
  else
25
- raise "Don't know how to GET with #{bare_client.inspect}"
25
+ raise "Don't know how to GET with #{bare.inspect}"
26
26
  end
27
27
  end
28
28
 
@@ -31,51 +31,130 @@ class Cache
31
31
  ttl = ttl.to_i
32
32
  reset_if_forked_or_threaded
33
33
  if memcached? or dalli? or memcached_rails? or mem_cache?
34
- bare_client.set k, v, ttl
34
+ bare.set k, v, ttl
35
35
  elsif redis?
36
36
  if ttl == 0
37
- bare_client.set k, ::Marshal.dump(v)
37
+ bare.set k, ::Marshal.dump(v)
38
38
  else
39
- bare_client.setex k, ttl, ::Marshal.dump(v)
39
+ bare.setex k, ttl, ::Marshal.dump(v)
40
40
  end
41
41
  elsif active_support_store?
42
42
  if ttl == 0
43
- bare_client.write k, v # never expire
43
+ bare.write k, v # never expire
44
44
  else
45
- bare_client.write k, v, :expires_in => ttl
45
+ bare.write k, v, :expires_in => ttl
46
46
  end
47
47
  else
48
- raise "Don't know how to SET with #{bare_client.inspect}"
48
+ raise "Don't know how to SET with #{bare.inspect}"
49
49
  end
50
50
  end
51
51
 
52
52
  def delete(k)
53
53
  reset_if_forked_or_threaded
54
54
  if memcached?
55
- begin; bare_client.delete(k); rescue ::Memcached::NotFound; nil; end
55
+ begin; bare.delete(k); rescue ::Memcached::NotFound; nil; end
56
56
  elsif redis?
57
- bare_client.del k
57
+ bare.del k
58
58
  elsif dalli? or memcached_rails? or mem_cache? or active_support_store?
59
- bare_client.delete k
59
+ bare.delete k
60
60
  else
61
- raise "Don't know how to DELETE with #{bare_client.inspect}"
61
+ raise "Don't know how to DELETE with #{bare.inspect}"
62
62
  end
63
63
  end
64
64
 
65
65
  def flush
66
66
  reset_if_forked_or_threaded
67
- bare_client.send %w{ flush flushdb flush_all clear }.detect { |flush_cmd| bare_client.respond_to? flush_cmd }
67
+ bare.send %w{ flush flushdb flush_all clear }.detect { |flush_cmd| bare.respond_to? flush_cmd }
68
+ end
69
+
70
+ # TODO detect nils
71
+ def exist?(k)
72
+ reset_if_forked_or_threaded
73
+ if memcached?
74
+ begin; bare.get(k); true; rescue ::Memcached::NotFound; false; end
75
+ elsif redis?
76
+ bare.exists k
77
+ elsif bare.respond_to?(:exist?)
78
+ # slow because we're looking it up
79
+ bare.exist? k
80
+ else
81
+ # weak because it doesn't detect keys that equal nil
82
+ !get(k).nil?
83
+ end
84
+ end
85
+
86
+ # TODO use native memcached increment if available
87
+ # TODO don't reset the timer!
88
+ def increment(k, amount)
89
+ # reset_if_forked_or_threaded - uses get
90
+ new_v = get(k).to_i + amount
91
+ set k, new_v, 0
92
+ new_v
93
+ end
94
+
95
+ def decrement(k, amount)
96
+ # reset_if_forked_or_threaded - uses increment, which uses get
97
+ increment k, -amount
98
+ end
99
+
100
+ # TODO don't resort to trickery like this
101
+ def reset
102
+ @pid = nil
103
+ end
104
+
105
+ def fetch(k, ttl, &blk)
106
+ ttl ||= parent.config.default_ttl
107
+ ttl = ttl.to_i
108
+ reset_if_forked_or_threaded
109
+ if dalli? or mem_cache?
110
+ bare.fetch k, ttl, &blk
111
+ elsif active_support_store?
112
+ bare.fetch k, { :expires_in => ttl }, &blk
113
+ else
114
+ if exist? k
115
+ get k
116
+ elsif blk
117
+ v = blk.call
118
+ set k, v, ttl
119
+ v
120
+ end
121
+ end
122
+ end
123
+
124
+ def cas(k, ttl, &blk)
125
+ ttl ||= parent.config.default_ttl
126
+ ttl = ttl.to_i
127
+ reset_if_forked_or_threaded
128
+ if memcached?
129
+ begin; bare.cas(k, ttl, &blk); rescue ::Memcached::NotFound; nil; end
130
+ elsif dalli? or memcached_rails?
131
+ bare.cas k, ttl, &blk
132
+ elsif blk and exist?(k)
133
+ old_v = get k
134
+ new_v = blk.call old_v
135
+ set k, new_v, ttl
136
+ new_v
137
+ end
138
+ end
139
+
140
+ def stats
141
+ reset_if_forked_or_threaded
142
+ if bare.respond_to?(:stats)
143
+ bare.stats
144
+ else
145
+ {}
146
+ end
68
147
  end
69
148
 
70
149
  private
71
150
 
72
- def bare_client
73
- @bare_client ||= parent.config.client
151
+ def bare
152
+ @bare ||= parent.config.client
74
153
  end
75
154
 
76
155
  def reset_if_forked_or_threaded
77
156
  if fork_detected?
78
- $stderr.puts "fork detected" if ENV['CACHE_DEBUG'] == 'true'
157
+ # $stderr.puts "fork detected" if ENV['CACHE_DEBUG'] == 'true'
79
158
  if dalli?
80
159
  parent.config.client.close
81
160
  elsif dalli_store?
@@ -83,18 +162,18 @@ class Cache
83
162
  elsif memcached? or memcached_rails?
84
163
  cloned_client = parent.config.client.clone
85
164
  parent.config.client = cloned_client
86
- @bare_client = parent.config.client
165
+ @bare = parent.config.client
87
166
  elsif redis?
88
167
  parent.config.client.client.connect
89
168
  elsif mem_cache?
90
169
  parent.config.client.reset
91
170
  end
92
171
  elsif new_thread_detected?
93
- $stderr.puts "new thread detected" if ENV['CACHE_DEBUG'] == 'true'
172
+ # $stderr.puts "new thread detected" if ENV['CACHE_DEBUG'] == 'true'
94
173
  if memcached? or memcached_rails?
95
174
  cloned_client = parent.config.client.clone
96
175
  parent.config.client = cloned_client
97
- @bare_client = parent.config.client
176
+ @bare = parent.config.client
98
177
  end
99
178
  end
100
179
  end
@@ -113,56 +192,56 @@ class Cache
113
192
 
114
193
  def dalli?
115
194
  return @dalli_query[0] if @dalli_query.is_a?(::Array)
116
- answer = (defined?(::Dalli) and bare_client.is_a?(::Dalli::Client))
195
+ answer = (defined?(::Dalli) and bare.is_a?(::Dalli::Client))
117
196
  @dalli_query = [answer]
118
197
  answer
119
198
  end
120
199
 
121
200
  def active_support_store?
122
201
  return @active_support_store_query[0] if @active_support_store_query.is_a?(::Array)
123
- answer = (defined?(::ActiveSupport::Cache) and bare_client.is_a?(::ActiveSupport::Cache::Store))
202
+ answer = (defined?(::ActiveSupport::Cache) and bare.is_a?(::ActiveSupport::Cache::Store))
124
203
  @active_support_store_query = [answer]
125
204
  answer
126
205
  end
127
206
 
128
207
  def dalli_store?
129
208
  return @dalli_store_query[0] if @dalli_store_query.is_a?(::Array)
130
- answer = (defined?(::ActiveSupport::Cache::DalliStore) and bare_client.is_a?(::ActiveSupport::Cache::DalliStore))
209
+ answer = (defined?(::ActiveSupport::Cache::DalliStore) and bare.is_a?(::ActiveSupport::Cache::DalliStore))
131
210
  @dalli_store_query = [answer]
132
211
  answer
133
212
  end
134
213
 
135
214
  def memory_store?
136
215
  return @memory_store_query[0] if @memory_store_query.is_a?(::Array)
137
- answer = (defined?(::ActiveSupport::Cache::MemoryStore) and bare_client.is_a?(::ActiveSupport::Cache::MemoryStore))
216
+ answer = (defined?(::ActiveSupport::Cache::MemoryStore) and bare.is_a?(::ActiveSupport::Cache::MemoryStore))
138
217
  @memory_store_query = [answer]
139
218
  answer
140
219
  end
141
220
 
142
221
  def mem_cache?
143
222
  return @mem_cache_query[0] if @mem_cache_query.is_a?(::Array)
144
- answer = (defined?(::MemCache) and bare_client.is_a?(::MemCache))
223
+ answer = (defined?(::MemCache) and bare.is_a?(::MemCache))
145
224
  @mem_cache_query = [answer]
146
225
  answer
147
226
  end
148
227
 
149
228
  def memcached?
150
229
  return @memcached_query[0] if @memcached_query.is_a?(::Array)
151
- answer = (defined?(::Memcached) and bare_client.is_a?(::Memcached))
230
+ answer = (defined?(::Memcached) and bare.is_a?(::Memcached) and not bare.is_a?(::Memcached::Rails))
152
231
  @memcached_query = [answer]
153
232
  answer
154
233
  end
155
234
 
156
235
  def memcached_rails?
157
236
  return @memcached_rails_query[0] if @memcached_rails_query.is_a?(::Array)
158
- answer = (defined?(::Memcached) and bare_client.is_a?(::Memcached::Rails))
237
+ answer = (defined?(::Memcached) and bare.is_a?(::Memcached::Rails))
159
238
  @memcached_rails_query = [answer]
160
239
  answer
161
240
  end
162
241
 
163
242
  def redis?
164
243
  return @redis_query[0] if @redis_query.is_a?(::Array)
165
- answer = (defined?(::Redis) and bare_client.is_a?(::Redis))
244
+ answer = (defined?(::Redis) and bare.is_a?(::Redis))
166
245
  @redis_query = [answer]
167
246
  answer
168
247
  end
data/lib/cache/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Cache
2
- VERSION = "0.1.3"
2
+ VERSION = "0.2.0"
3
3
  end
data/test/helper.rb CHANGED
@@ -10,7 +10,7 @@ require 'shared_tests'
10
10
 
11
11
  class Test::Unit::TestCase
12
12
  def setup
13
- @cache = Cache.new raw_client
13
+ @cache = Cache.wrap raw_client
14
14
  @cache.flush
15
15
  end
16
16
  end
@@ -53,14 +53,14 @@ class Dalli::ClientCompat < Dalli::Client
53
53
  end
54
54
  end
55
55
 
56
- class Cache::Compat < Cache
57
- def set(*args)
58
- super(*args[0..2])
59
- end
60
- def get(*args)
61
- super(args.first)
62
- end
63
- end
56
+ # class Cache::Compat < Cache
57
+ # def set(*args)
58
+ # super(*args[0..2])
59
+ # end
60
+ # def get(*args)
61
+ # super(args.first)
62
+ # end
63
+ # end
64
64
 
65
65
  class Bench
66
66
 
@@ -108,8 +108,8 @@ class Bench
108
108
 
109
109
  def reset_clients
110
110
  @clients = {
111
- "cache:libm:bin" => Cache::Compat.new(Memcached.new(['127.0.0.1:43042', '127.0.0.1:43043'], :buffer_requests => false, :no_block => false, :namespace => "namespace", :binary_protocol => true)),
112
- "cache:dalli:bin" => Cache::Compat.new(Dalli::Client.new(['127.0.0.1:43042', '127.0.0.1:43043'], :marshal => false, :threadsafe => false)),
111
+ "cache:libm:bin" => Cache.wrap(Memcached.new(['127.0.0.1:43042', '127.0.0.1:43043'], :buffer_requests => false, :no_block => false, :namespace => "namespace", :binary_protocol => true)),
112
+ "cache:dalli:bin" => Cache.wrap(Dalli::Client.new(['127.0.0.1:43042', '127.0.0.1:43043'], :marshal => false, :threadsafe => false)),
113
113
  "libm:ascii" => Memcached::Rails.new(
114
114
  ['127.0.0.1:43042', '127.0.0.1:43043'],
115
115
  :buffer_requests => false, :no_block => false, :namespace => "namespace"),
data/test/shared_tests.rb CHANGED
@@ -34,8 +34,97 @@ module SharedTests
34
34
 
35
35
  def test_flush
36
36
  @cache.set 'hello', 'world'
37
- assert_equal 'world', @cache.get('hello')
37
+ assert @cache.exist?('hello')
38
38
  @cache.flush
39
+ assert !@cache.exist?('hello')
40
+ end
41
+
42
+ def test_exist
43
+ assert !@cache.exist?('hello')
44
+ @cache.set 'hello', 'world'
45
+ assert @cache.exist?('hello')
46
+ end
47
+
48
+ def test_exist_key_with_nil_value
49
+ assert !@cache.exist?('hello')
50
+ @cache.set 'hello', nil
51
+ assert @cache.exist?('hello')
52
+ end
53
+
54
+ def test_stats
55
+ assert_nothing_raised do
56
+ @cache.stats
57
+ end
58
+ end
59
+
60
+ def test_reset
61
+ @cache.set 'hello', 'world'
62
+ assert @cache.exist?('hello')
63
+ @cache.reset
64
+ # still there!
65
+ assert @cache.exist?('hello')
66
+ end
67
+
68
+ def test_fetch
69
+ assert_equal nil, @cache.fetch('hello')
70
+ assert_equal 'world', @cache.fetch('hello') { 'world' }
71
+ end
72
+
73
+ def test_cas
74
+ toggle = lambda do |current|
75
+ current == 'on' ? 'off' : 'on'
76
+ end
77
+
78
+ @cache.set 'lights', 'on'
79
+ assert_equal 'on', @cache.get('lights')
80
+ @cache.cas 'lights', &toggle
81
+ assert_equal 'off', @cache.get('lights')
82
+ @cache.cas 'lights', &toggle
83
+ assert_equal 'on', @cache.get('lights')
84
+ @cache.cas 'lights', &toggle
85
+ assert_equal 'off', @cache.get('lights')
86
+ end
87
+
88
+ def test_write
89
+ @cache.write 'hello', 'world'
90
+ assert_equal 'world', @cache.get('hello')
91
+ end
92
+
93
+ def test_write_with_expires_in
94
+ @cache.write 'hello', 'world', :expires_in => 1
95
+ assert_equal 'world', @cache.get('hello')
96
+ sleep 2
39
97
  assert_equal nil, @cache.get('hello')
40
98
  end
99
+
100
+ def test_read
101
+ @cache.set 'hello', 'world'
102
+ assert_equal 'world', @cache.read('hello')
103
+ end
104
+
105
+ def test_increment
106
+ assert !@cache.exist?('high-fives')
107
+ @cache.increment 'high-fives'
108
+ assert_equal 1, @cache.get('high-fives')
109
+ @cache.increment 'high-fives'
110
+ assert_equal 2, @cache.get('high-fives')
111
+ end
112
+
113
+ def test_decrement
114
+ assert !@cache.exist?('high-fives')
115
+ @cache.decrement 'high-fives'
116
+ assert_equal -1, @cache.get('high-fives')
117
+ @cache.decrement 'high-fives'
118
+ assert_equal -2, @cache.get('high-fives')
119
+ end
120
+
121
+ def test_read_multi
122
+ @cache.set 'hello', 'world'
123
+ @cache.set 'privyet', 'mir'
124
+ assert_equal %w{world mir}, @cache.read_multi('hello', 'privyet')
125
+ end
126
+
127
+ alias :test_clear :test_flush
128
+
129
+ alias :test_compare_and_swap :test_cas
41
130
  end
@@ -4,13 +4,13 @@ require 'memcached'
4
4
 
5
5
  class TestMemcachedRailsStorage < Test::Unit::TestCase
6
6
  def raw_client
7
- Memcached::Rails.new 'localhost:11211'
7
+ Memcached::Rails.new 'localhost:11211', :support_cas => true
8
8
  end
9
9
 
10
10
  include SharedTests
11
11
 
12
- def get_bare_client_id
13
- @cache.storage.send(:bare_client).object_id
12
+ def get_bare_id
13
+ @cache.storage.send(:bare).object_id
14
14
  end
15
15
 
16
16
  def test_treats_as_not_thread_safe
@@ -18,17 +18,17 @@ class TestMemcachedRailsStorage < Test::Unit::TestCase
18
18
  @cache.get 'hi'
19
19
 
20
20
  # get the main thread's bare client
21
- main_thread_bare_client_id = get_bare_client_id
21
+ main_thread_bare_id = get_bare_id
22
22
 
23
23
  # sanity check that it's not changing every time
24
24
  @cache.get 'hi'
25
- assert_equal main_thread_bare_client_id, get_bare_client_id
25
+ assert_equal main_thread_bare_id, get_bare_id
26
26
 
27
27
  # create a new thread and get its bare client
28
- new_thread_bare_client_id = Thread.new { @cache.get 'hi'; get_bare_client_id }.value
28
+ new_thread_bare_id = Thread.new { @cache.get 'hi'; get_bare_id }.value
29
29
 
30
30
  # make sure the bare client was reinitialized
31
- assert(main_thread_bare_client_id != new_thread_bare_client_id)
31
+ assert(main_thread_bare_id != new_thread_bare_id)
32
32
  end
33
33
 
34
34
  def test_treats_as_not_fork_safe
@@ -36,16 +36,16 @@ class TestMemcachedRailsStorage < Test::Unit::TestCase
36
36
  @cache.get 'hi'
37
37
 
38
38
  # get the main process's bare client
39
- parent_process_bare_client_id = get_bare_client_id
39
+ parent_process_bare_id = get_bare_id
40
40
 
41
41
  # sanity check that it's not changing every time
42
42
  @cache.get 'hi'
43
- assert_equal parent_process_bare_client_id, get_bare_client_id
43
+ assert_equal parent_process_bare_id, get_bare_id
44
44
 
45
45
  # fork a new process
46
46
  pid = Kernel.fork do
47
47
  @cache.get 'hi'
48
- raise "Didn't split!" if parent_process_bare_client_id == get_bare_client_id
48
+ raise "Didn't split!" if parent_process_bare_id == get_bare_id
49
49
  end
50
50
  Process.wait pid
51
51
 
@@ -4,13 +4,13 @@ require 'memcached'
4
4
 
5
5
  class TestMemcachedStorage < Test::Unit::TestCase
6
6
  def raw_client
7
- Memcached.new 'localhost:11211'
7
+ Memcached.new 'localhost:11211', :support_cas => true
8
8
  end
9
9
 
10
10
  include SharedTests
11
11
 
12
- def get_bare_client_id
13
- @cache.storage.send(:bare_client).object_id
12
+ def get_bare_id
13
+ @cache.storage.send(:bare).object_id
14
14
  end
15
15
 
16
16
  def test_treats_as_not_thread_safe
@@ -18,17 +18,17 @@ class TestMemcachedStorage < Test::Unit::TestCase
18
18
  @cache.get 'hi'
19
19
 
20
20
  # get the main thread's bare client
21
- main_thread_bare_client_id = get_bare_client_id
21
+ main_thread_bare_id = get_bare_id
22
22
 
23
23
  # sanity check that it's not changing every time
24
24
  @cache.get 'hi'
25
- assert_equal main_thread_bare_client_id, get_bare_client_id
25
+ assert_equal main_thread_bare_id, get_bare_id
26
26
 
27
27
  # create a new thread and get its bare client
28
- new_thread_bare_client_id = Thread.new { @cache.get 'hi'; get_bare_client_id }.value
28
+ new_thread_bare_id = Thread.new { @cache.get 'hi'; get_bare_id }.value
29
29
 
30
30
  # make sure the bare client was reinitialized
31
- assert(main_thread_bare_client_id != new_thread_bare_client_id)
31
+ assert(main_thread_bare_id != new_thread_bare_id)
32
32
  end
33
33
 
34
34
  def test_treats_as_not_fork_safe
@@ -36,16 +36,16 @@ class TestMemcachedStorage < Test::Unit::TestCase
36
36
  @cache.get 'hi'
37
37
 
38
38
  # get the main process's bare client
39
- parent_process_bare_client_id = get_bare_client_id
39
+ parent_process_bare_id = get_bare_id
40
40
 
41
41
  # sanity check that it's not changing every time
42
42
  @cache.get 'hi'
43
- assert_equal parent_process_bare_client_id, get_bare_client_id
43
+ assert_equal parent_process_bare_id, get_bare_id
44
44
 
45
45
  # fork a new process
46
46
  pid = Kernel.fork do
47
47
  @cache.get 'hi'
48
- raise "Didn't split!" if parent_process_bare_client_id == get_bare_client_id
48
+ raise "Didn't split!" if parent_process_bare_id == get_bare_id
49
49
  end
50
50
  Process.wait pid
51
51
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cache
3
3
  version: !ruby/object:Gem::Version
4
- hash: 29
4
+ hash: 23
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 1
9
- - 3
10
- version: 0.1.3
8
+ - 2
9
+ - 0
10
+ version: 0.2.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Seamus Abshere
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-03-01 00:00:00 -06:00
18
+ date: 2011-03-04 00:00:00 -06:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -49,7 +49,7 @@ dependencies:
49
49
  type: :runtime
50
50
  version_requirements: *id002
51
51
  - !ruby/object:Gem::Dependency
52
- name: test-unit
52
+ name: yard
53
53
  prerelease: false
54
54
  requirement: &id003 !ruby/object:Gem::Requirement
55
55
  none: false
@@ -63,7 +63,7 @@ dependencies:
63
63
  type: :development
64
64
  version_requirements: *id003
65
65
  - !ruby/object:Gem::Dependency
66
- name: redis
66
+ name: test-unit
67
67
  prerelease: false
68
68
  requirement: &id004 !ruby/object:Gem::Requirement
69
69
  none: false
@@ -77,7 +77,7 @@ dependencies:
77
77
  type: :development
78
78
  version_requirements: *id004
79
79
  - !ruby/object:Gem::Dependency
80
- name: dalli
80
+ name: redis
81
81
  prerelease: false
82
82
  requirement: &id005 !ruby/object:Gem::Requirement
83
83
  none: false
@@ -91,7 +91,7 @@ dependencies:
91
91
  type: :development
92
92
  version_requirements: *id005
93
93
  - !ruby/object:Gem::Dependency
94
- name: memcached
94
+ name: dalli
95
95
  prerelease: false
96
96
  requirement: &id006 !ruby/object:Gem::Requirement
97
97
  none: false
@@ -105,7 +105,7 @@ dependencies:
105
105
  type: :development
106
106
  version_requirements: *id006
107
107
  - !ruby/object:Gem::Dependency
108
- name: memcache-client
108
+ name: memcached
109
109
  prerelease: false
110
110
  requirement: &id007 !ruby/object:Gem::Requirement
111
111
  none: false
@@ -118,6 +118,20 @@ dependencies:
118
118
  version: "0"
119
119
  type: :development
120
120
  version_requirements: *id007
121
+ - !ruby/object:Gem::Dependency
122
+ name: memcache-client
123
+ prerelease: false
124
+ requirement: &id008 !ruby/object:Gem::Requirement
125
+ none: false
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ hash: 3
130
+ segments:
131
+ - 0
132
+ version: "0"
133
+ type: :development
134
+ version_requirements: *id008
121
135
  description: A unified cache handling interface for Ruby, inspired by (but simpler than) Perl's Cache::Cache
122
136
  email:
123
137
  - seamus@abshere.net
@@ -131,7 +145,7 @@ files:
131
145
  - .gitignore
132
146
  - BENCHMARKS
133
147
  - Gemfile
134
- - README.rdoc
148
+ - README.md
135
149
  - Rakefile
136
150
  - cache.gemspec
137
151
  - lib/cache.rb
data/README.rdoc DELETED
@@ -1,87 +0,0 @@
1
- = cache
2
-
3
- A unified cache handling interface, inspired (but simpler than) Perl's Cache::Cache[http://cpan.uwinnipeg.ca/module/Cache::Cache] and CHI[http://cpan.uwinnipeg.ca/module/CHI].
4
-
5
- Takes care of exceptions like Memcached::NotFound and also forking/threading.
6
-
7
- == Example
8
-
9
- It uses a (useless?) memory store by default:
10
-
11
- cache = Cache.new
12
- cache.set 'hello'
13
- cache.get 'hello', 'world'
14
-
15
- You can specify a more useful cache client:
16
-
17
- require 'memcached' # a really fast memcached client gem
18
- require 'cache' # this gem, which provides a standard interface
19
- raw_client = Memcached.new '127.0.0.1:11211'
20
- cache = Cache.new raw_client
21
-
22
- or
23
-
24
- require 'redis' # the redis key-value store
25
- require 'cache' # this gem, which provides a standard interface
26
- raw_client = Redis.new
27
- cache = Cache.new raw_client
28
-
29
- or
30
-
31
- require 'dalli' # the dalli memcached client used by heroku
32
- require 'cache' # this gem, which provides a standard interface
33
- raw_client = Dalli::Client.new
34
- cache = Cache.new raw_client
35
-
36
- Maybe this will even work:
37
-
38
- # Piggyback off the default rails cache
39
- cache = Cache.new Rails.cache
40
-
41
- == Methods
42
-
43
- cache.get 'hello'
44
- cache.set 'hello', 'world'
45
- cache.delete 'hello'
46
- cache.flush
47
-
48
- == Supported clients
49
-
50
- Supported memcached clients:
51
-
52
- * memcached[https://github.com/fauna/memcached] (either a Memcached or a Memcached::Rails)
53
- * dalli[https://github.com/mperham/dalli] (either a Dalli::Client or an ActiveSupport::Cache::DalliStore)
54
- * memcache-client[https://github.com/mperham/memcache-client] (MemCache, the one commonly used by Rails)
55
-
56
- Supported Redis clients:
57
-
58
- * redis[https://github.com/ezmobius/redis-rb]
59
-
60
- == TTL
61
-
62
- 0 means don't expire.
63
-
64
- == Forking/threading
65
-
66
- When you use a Cache object to wrap Memcached or Redis, you don't have to worry about forking or threading.
67
-
68
- For example, you don't have to set up unicorn or PhusionPassenger's <tt>after_fork</tt>.
69
-
70
- == Rationale
71
-
72
- I wanted a common interface to a bunch of great Ruby cache clients so I can develop gems (lock_method, cache_method) that accept any of them.
73
-
74
- * I am so tired of rescuing from Memcached::NotFound
75
- * I am so tired of forgetting whether it's :expires_in or :ttl
76
- * I don't know why we ever started using read/write instead of get/set.
77
- * I don't like how you have to manually handle after_fork for Redis, Memcached, etc.
78
-
79
- == Currently unsupported
80
-
81
- * cas
82
- * fetch
83
- * incr
84
-
85
- == Copyright
86
-
87
- Copyright 2011 Seamus Abshere