cache 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.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