activesupport-cascadestore 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in activesupport-cascadestore.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # ActiveSupport::Cache::CascadeStore
2
+
3
+ Hopefully this cache store is merged upstream into core
4
+ with [this pull request](https://github.com/rails/rails/pull/5263).
5
+ In the meantime, packaging this up as a gem for easy access.
6
+
7
+ A thread-safe cache store implementation that cascades
8
+ operations to a list of other cache stores. It is used to
9
+ provide fallback cache stores when primary stores become
10
+ unavailable, or to put lower latency stores in front of
11
+ other cache stores.
12
+
13
+ For example, to initialize a CascadeStore that
14
+ cascades through MemCacheStore, MemoryStore, and FileStore:
15
+
16
+ ActiveSupport::Cache.lookup_store(:cascade_store,
17
+ :stores => [
18
+ :mem_cache_store,
19
+ :memory_store,
20
+ :file_store
21
+ ]
22
+ })
23
+
24
+ Cache operation behavior:
25
+
26
+ Read: returns first cache hit from :stores, nil if none found
27
+
28
+ Write/Delete: write/delete through to each cache store in
29
+ :stores
30
+
31
+ Increment/Decrement: increment/decrement each store, returning
32
+ the new number if any stores was successfully
33
+ incremented/decremented, nil otherwise
34
+
35
+ ### Development
36
+
37
+ To run tests
38
+
39
+ ```
40
+ ruby -Itest test/caching_test.rb
41
+ ```
42
+
43
+ ### License
44
+
45
+ Same license as [Rails](http://github.com/rails/rails)
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "activesupport-cascadestore"
6
+ s.version = "0.0.1"
7
+ s.authors = ["Jerry Cheung"]
8
+ s.email = ["jch@whatcodecraves.com"]
9
+ s.homepage = "http://github.com/jch/activesupport-cascadestore"
10
+ s.summary = %q{write-through cache store that allows you to chain multiple cache stores together}
11
+ s.description = %q{write-through cache store that allows you to chain multiple cache stores together}
12
+
13
+ s.rubyforge_project = "activesupport-cascadestore"
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.require_paths = ["lib"]
19
+
20
+ s.add_runtime_dependency "activesupport"
21
+ end
@@ -0,0 +1,97 @@
1
+ require 'monitor'
2
+
3
+ module ActiveSupport
4
+ module Cache
5
+ # A thread-safe cache store implementation that cascades
6
+ # operations to a list of other cache stores. It is used to
7
+ # provide fallback cache stores when primary stores become
8
+ # unavailable. For example, to initialize a CascadeStore that
9
+ # cascades through MemCacheStore, MemoryStore, and FileStore:
10
+ #
11
+ # ActiveSupport::Cache.lookup_store(:cascade_store,
12
+ # :stores => [
13
+ # :mem_cache_store,
14
+ # :memory_store,
15
+ # :file_store
16
+ # ]
17
+ # })
18
+ #
19
+ # Cache operation behavior:
20
+ #
21
+ # Read: returns first cache hit from :stores, nil if none found
22
+ #
23
+ # Write/Delete: write/delete through to each cache store in
24
+ # :stores
25
+ #
26
+ # Increment/Decrement: increment/decrement each store, returning
27
+ # the new number if any stores was successfully
28
+ # incremented/decremented, nil otherwise
29
+ class CascadeStore < Store
30
+ attr_reader :stores
31
+
32
+ # Initialize a CascadeStore with +options[:stores]+, an array of
33
+ # options to initialize other ActiveSupport::Cache::Store
34
+ # implementations. If options is a symbol, top level
35
+ # CascadeStore options are used for cascaded stores. If options
36
+ # is an array, they are passed on unchanged.
37
+ def initialize(options = nil, &blk)
38
+ options ||= {}
39
+ super(options)
40
+ @monitor = Monitor.new
41
+ store_options = options.delete(:stores) || []
42
+ @stores = store_options.map do |o|
43
+ o = o.is_a?(Symbol) ? [o, options] : o
44
+ ActiveSupport::Cache.lookup_store(*o)
45
+ end
46
+ end
47
+
48
+ def increment(name, amount = 1, options = nil)
49
+ nums = cascade(:increment, name, amount, options)
50
+ nums.detect {|n| !n.nil?}
51
+ end
52
+
53
+ def decrement(name, amount = 1, options = nil)
54
+ nums = cascade(:decrement, name, amount, options)
55
+ nums.detect {|n| !n.nil?}
56
+ end
57
+
58
+ def delete_matched(matcher, options = nil)
59
+ cascade(:delete_matched, matcher, options)
60
+ nil
61
+ end
62
+
63
+ protected
64
+ def synchronize(&block) # :nodoc:
65
+ @monitor.synchronize(&block)
66
+ end
67
+
68
+ def cascade(method, *args) # :nodoc:
69
+ synchronize do
70
+ @stores.map do |store|
71
+ store.send(method, *args) rescue nil
72
+ end
73
+ end
74
+ end
75
+
76
+ def read_entry(key, options) # :nodoc:
77
+ entry = nil
78
+ synchronize do
79
+ @stores.detect do |store|
80
+ entry = store.send(:read_entry, key, options)
81
+ end
82
+ end
83
+ entry
84
+ end
85
+
86
+ def write_entry(key, entry, options) # :nodoc:
87
+ cascade(:write_entry, key, entry, options)
88
+ true
89
+ end
90
+
91
+ def delete_entry(key, options) # :nodoc:
92
+ cascade(:delete_entry, key, options)
93
+ true
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1 @@
1
+ require "active_support/cache"
@@ -0,0 +1,40 @@
1
+ ORIG_ARGV = ARGV.dup
2
+
3
+ # begin
4
+ # old, $VERBOSE = $VERBOSE, nil
5
+ # require File.expand_path('../../../load_paths', __FILE__)
6
+ # ensure
7
+ # $VERBOSE = old
8
+ # end
9
+
10
+ lib = File.expand_path("#{File.dirname(__FILE__)}/../lib")
11
+ $:.unshift(lib) unless $:.include?('lib') || $:.include?(lib)
12
+
13
+ require 'active_support/core_ext/kernel/reporting'
14
+ require 'active_support/core_ext/string/encoding'
15
+
16
+ silence_warnings do
17
+ Encoding.default_internal = "UTF-8"
18
+ Encoding.default_external = "UTF-8"
19
+ end
20
+
21
+ require 'minitest/autorun'
22
+ # require 'empty_bool'
23
+
24
+ silence_warnings { require 'mocha' }
25
+
26
+ ENV['NO_RELOAD'] = '1'
27
+ require 'active_support'
28
+
29
+ def uses_memcached(test_name)
30
+ require 'memcache'
31
+ begin
32
+ MemCache.new('localhost:11211').stats
33
+ yield
34
+ rescue MemCache::MemCacheError
35
+ $stderr.puts "Skipping #{test_name} tests. Start memcached and try again."
36
+ end
37
+ end
38
+
39
+ # Show backtraces for deprecated behavior for quicker cleanup.
40
+ ActiveSupport::Deprecation.debug = true
@@ -0,0 +1,374 @@
1
+ require 'logger'
2
+ require 'abstract_unit'
3
+ require 'active_support/cache'
4
+
5
+ # Tests the base functionality that should be identical across all cache stores.
6
+ module CacheStoreBehavior
7
+ def test_should_read_and_write_strings
8
+ assert @cache.write('foo', 'bar')
9
+ assert_equal 'bar', @cache.read('foo')
10
+ end
11
+
12
+ def test_should_overwrite
13
+ @cache.write('foo', 'bar')
14
+ @cache.write('foo', 'baz')
15
+ assert_equal 'baz', @cache.read('foo')
16
+ end
17
+
18
+ def test_fetch_without_cache_miss
19
+ @cache.write('foo', 'bar')
20
+ @cache.expects(:write).never
21
+ assert_equal 'bar', @cache.fetch('foo') { 'baz' }
22
+ end
23
+
24
+ def test_fetch_with_cache_miss
25
+ @cache.expects(:write).with('foo', 'baz', @cache.options)
26
+ assert_equal 'baz', @cache.fetch('foo') { 'baz' }
27
+ end
28
+
29
+ def test_fetch_with_forced_cache_miss
30
+ @cache.write('foo', 'bar')
31
+ @cache.expects(:read).never
32
+ @cache.expects(:write).with('foo', 'bar', @cache.options.merge(:force => true))
33
+ @cache.fetch('foo', :force => true) { 'bar' }
34
+ end
35
+
36
+ def test_fetch_with_cached_nil
37
+ @cache.write('foo', nil)
38
+ @cache.expects(:write).never
39
+ assert_nil @cache.fetch('foo') { 'baz' }
40
+ end
41
+
42
+ def test_should_read_and_write_hash
43
+ assert @cache.write('foo', {:a => "b"})
44
+ assert_equal({:a => "b"}, @cache.read('foo'))
45
+ end
46
+
47
+ def test_should_read_and_write_integer
48
+ assert @cache.write('foo', 1)
49
+ assert_equal 1, @cache.read('foo')
50
+ end
51
+
52
+ def test_should_read_and_write_nil
53
+ assert @cache.write('foo', nil)
54
+ assert_equal nil, @cache.read('foo')
55
+ end
56
+
57
+ def test_should_read_and_write_false
58
+ assert @cache.write('foo', false)
59
+ assert_equal false, @cache.read('foo')
60
+ end
61
+
62
+ def test_read_multi
63
+ @cache.write('foo', 'bar')
64
+ @cache.write('fu', 'baz')
65
+ @cache.write('fud', 'biz')
66
+ assert_equal({"foo" => "bar", "fu" => "baz"}, @cache.read_multi('foo', 'fu'))
67
+ end
68
+
69
+ def test_read_multi_with_expires
70
+ @cache.write('foo', 'bar', :expires_in => 0.001)
71
+ @cache.write('fu', 'baz')
72
+ @cache.write('fud', 'biz')
73
+ sleep(0.002)
74
+ assert_equal({"fu" => "baz"}, @cache.read_multi('foo', 'fu'))
75
+ end
76
+
77
+ def test_read_and_write_compressed_small_data
78
+ @cache.write('foo', 'bar', :compress => true)
79
+ raw_value = @cache.send(:read_entry, 'foo', {}).raw_value
80
+ assert_equal 'bar', @cache.read('foo')
81
+ assert_equal 'bar', Marshal.load(raw_value)
82
+ end
83
+
84
+ def test_read_and_write_compressed_large_data
85
+ @cache.write('foo', 'bar', :compress => true, :compress_threshold => 2)
86
+ raw_value = @cache.send(:read_entry, 'foo', {}).raw_value
87
+ assert_equal 'bar', @cache.read('foo')
88
+ assert_equal 'bar', Marshal.load(Zlib::Inflate.inflate(raw_value))
89
+ end
90
+
91
+ def test_read_and_write_compressed_nil
92
+ @cache.write('foo', nil, :compress => true)
93
+ assert_nil @cache.read('foo')
94
+ end
95
+
96
+ def test_cache_key
97
+ obj = Object.new
98
+ def obj.cache_key
99
+ :foo
100
+ end
101
+ @cache.write(obj, "bar")
102
+ assert_equal "bar", @cache.read("foo")
103
+ end
104
+
105
+ def test_param_as_cache_key
106
+ obj = Object.new
107
+ def obj.to_param
108
+ "foo"
109
+ end
110
+ @cache.write(obj, "bar")
111
+ assert_equal "bar", @cache.read("foo")
112
+ end
113
+
114
+ def test_array_as_cache_key
115
+ @cache.write([:fu, "foo"], "bar")
116
+ assert_equal "bar", @cache.read("fu/foo")
117
+ end
118
+
119
+ def test_hash_as_cache_key
120
+ @cache.write({:foo => 1, :fu => 2}, "bar")
121
+ assert_equal "bar", @cache.read("foo=1/fu=2")
122
+ end
123
+
124
+ def test_keys_are_case_sensitive
125
+ @cache.write("foo", "bar")
126
+ assert_nil @cache.read("FOO")
127
+ end
128
+
129
+ def test_exist
130
+ @cache.write('foo', 'bar')
131
+ assert @cache.exist?('foo')
132
+ assert !@cache.exist?('bar')
133
+ end
134
+
135
+ def test_nil_exist
136
+ @cache.write('foo', nil)
137
+ assert @cache.exist?('foo')
138
+ end
139
+
140
+ def test_delete
141
+ @cache.write('foo', 'bar')
142
+ assert @cache.exist?('foo')
143
+ assert @cache.delete('foo')
144
+ assert !@cache.exist?('foo')
145
+ end
146
+
147
+ def test_read_should_return_a_different_object_id_each_time_it_is_called
148
+ @cache.write('foo', 'bar')
149
+ assert_not_equal @cache.read('foo').object_id, @cache.read('foo').object_id
150
+ value = @cache.read('foo')
151
+ value << 'bingo'
152
+ assert_not_equal value, @cache.read('foo')
153
+ end
154
+
155
+ def test_original_store_objects_should_not_be_immutable
156
+ bar = 'bar'
157
+ @cache.write('foo', bar)
158
+ assert_nothing_raised { bar.gsub!(/.*/, 'baz') }
159
+ end
160
+
161
+ def test_expires_in
162
+ time = Time.local(2008, 4, 24)
163
+ Time.stubs(:now).returns(time)
164
+
165
+ @cache.write('foo', 'bar')
166
+ assert_equal 'bar', @cache.read('foo')
167
+
168
+ Time.stubs(:now).returns(time + 30)
169
+ assert_equal 'bar', @cache.read('foo')
170
+
171
+ Time.stubs(:now).returns(time + 61)
172
+ assert_nil @cache.read('foo')
173
+ end
174
+
175
+ def test_race_condition_protection
176
+ time = Time.now
177
+ @cache.write('foo', 'bar', :expires_in => 60)
178
+ Time.stubs(:now).returns(time + 61)
179
+ result = @cache.fetch('foo', :race_condition_ttl => 10) do
180
+ assert_equal 'bar', @cache.read('foo')
181
+ "baz"
182
+ end
183
+ assert_equal "baz", result
184
+ end
185
+
186
+ def test_race_condition_protection_is_limited
187
+ time = Time.now
188
+ @cache.write('foo', 'bar', :expires_in => 60)
189
+ Time.stubs(:now).returns(time + 71)
190
+ result = @cache.fetch('foo', :race_condition_ttl => 10) do
191
+ assert_equal nil, @cache.read('foo')
192
+ "baz"
193
+ end
194
+ assert_equal "baz", result
195
+ end
196
+
197
+ def test_race_condition_protection_is_safe
198
+ time = Time.now
199
+ @cache.write('foo', 'bar', :expires_in => 60)
200
+ Time.stubs(:now).returns(time + 61)
201
+ begin
202
+ @cache.fetch('foo', :race_condition_ttl => 10) do
203
+ assert_equal 'bar', @cache.read('foo')
204
+ raise ArgumentError.new
205
+ end
206
+ rescue ArgumentError
207
+ end
208
+ assert_equal "bar", @cache.read('foo')
209
+ Time.stubs(:now).returns(time + 71)
210
+ assert_nil @cache.read('foo')
211
+ end
212
+
213
+ def test_crazy_key_characters
214
+ crazy_key = "#/:*(<+=> )&$%@?;'\"\'`~-"
215
+ assert @cache.write(crazy_key, "1", :raw => true)
216
+ assert_equal "1", @cache.read(crazy_key)
217
+ assert_equal "1", @cache.fetch(crazy_key)
218
+ assert @cache.delete(crazy_key)
219
+ assert_equal "2", @cache.fetch(crazy_key, :raw => true) { "2" }
220
+ assert_equal 3, @cache.increment(crazy_key)
221
+ assert_equal 2, @cache.decrement(crazy_key)
222
+ end
223
+
224
+ def test_really_long_keys
225
+ key = ""
226
+ 900.times{key << "x"}
227
+ assert @cache.write(key, "bar")
228
+ assert_equal "bar", @cache.read(key)
229
+ assert_equal "bar", @cache.fetch(key)
230
+ assert_nil @cache.read("#{key}x")
231
+ assert_equal({key => "bar"}, @cache.read_multi(key))
232
+ assert @cache.delete(key)
233
+ end
234
+ end
235
+
236
+ # https://rails.lighthouseapp.com/projects/8994/tickets/6225-memcachestore-cant-deal-with-umlauts-and-special-characters
237
+ # The error is caused by charcter encodings that can't be compared with ASCII-8BIT regular expressions and by special
238
+ # characters like the umlaut in UTF-8.
239
+ module EncodedKeyCacheBehavior
240
+ Encoding.list.each do |encoding|
241
+ define_method "test_#{encoding.name.underscore}_encoded_values" do
242
+ key = "foo".force_encoding(encoding)
243
+ assert @cache.write(key, "1", :raw => true)
244
+ assert_equal "1", @cache.read(key)
245
+ assert_equal "1", @cache.fetch(key)
246
+ assert @cache.delete(key)
247
+ assert_equal "2", @cache.fetch(key, :raw => true) { "2" }
248
+ assert_equal 3, @cache.increment(key)
249
+ assert_equal 2, @cache.decrement(key)
250
+ end
251
+ end
252
+
253
+ def test_common_utf8_values
254
+ key = "\xC3\xBCmlaut".force_encoding(Encoding::UTF_8)
255
+ assert @cache.write(key, "1", :raw => true)
256
+ assert_equal "1", @cache.read(key)
257
+ assert_equal "1", @cache.fetch(key)
258
+ assert @cache.delete(key)
259
+ assert_equal "2", @cache.fetch(key, :raw => true) { "2" }
260
+ assert_equal 3, @cache.increment(key)
261
+ assert_equal 2, @cache.decrement(key)
262
+ end
263
+
264
+ def test_retains_encoding
265
+ key = "\xC3\xBCmlaut".force_encoding(Encoding::UTF_8)
266
+ assert @cache.write(key, "1", :raw => true)
267
+ assert_equal Encoding::UTF_8, key.encoding
268
+ end
269
+ end
270
+
271
+ module CacheDeleteMatchedBehavior
272
+ def test_delete_matched
273
+ @cache.write("foo", "bar")
274
+ @cache.write("fu", "baz")
275
+ @cache.write("foo/bar", "baz")
276
+ @cache.write("fu/baz", "bar")
277
+ @cache.delete_matched(/oo/)
278
+ assert !@cache.exist?("foo")
279
+ assert @cache.exist?("fu")
280
+ assert !@cache.exist?("foo/bar")
281
+ assert @cache.exist?("fu/baz")
282
+ end
283
+ end
284
+
285
+ module CacheIncrementDecrementBehavior
286
+ def test_increment
287
+ @cache.write('foo', 1, :raw => true)
288
+ assert_equal 1, @cache.read('foo').to_i
289
+ assert_equal 2, @cache.increment('foo')
290
+ assert_equal 2, @cache.read('foo').to_i
291
+ assert_equal 3, @cache.increment('foo')
292
+ assert_equal 3, @cache.read('foo').to_i
293
+ end
294
+
295
+ def test_decrement
296
+ @cache.write('foo', 3, :raw => true)
297
+ assert_equal 3, @cache.read('foo').to_i
298
+ assert_equal 2, @cache.decrement('foo')
299
+ assert_equal 2, @cache.read('foo').to_i
300
+ assert_equal 1, @cache.decrement('foo')
301
+ assert_equal 1, @cache.read('foo').to_i
302
+ end
303
+ end
304
+
305
+ class CascadeStoreTest < ActiveSupport::TestCase
306
+ def setup
307
+ @cache = ActiveSupport::Cache.lookup_store(:cascade_store, {
308
+ :expires_in => 60,
309
+ :stores => [
310
+ :memory_store,
311
+ [:memory_store, :expires_in => 60]
312
+ ]
313
+ })
314
+ @store1 = @cache.stores[0]
315
+ @store2 = @cache.stores[1]
316
+ end
317
+
318
+ include CacheStoreBehavior
319
+ include CacheIncrementDecrementBehavior
320
+ include CacheDeleteMatchedBehavior
321
+ include EncodedKeyCacheBehavior
322
+
323
+ def test_default_child_store_options
324
+ assert_equal @store1.options[:expires_in], 60
325
+ end
326
+
327
+ def test_empty_store_cache_miss
328
+ cache = ActiveSupport::Cache.lookup_store(:cascade_store)
329
+ assert cache.write('foo', 'bar')
330
+ assert cache.fetch('foo').nil?
331
+ end
332
+
333
+ def test_cascade_write
334
+ @cache.write('foo', 'bar')
335
+ assert_equal @store1.read('foo'), 'bar'
336
+ assert_equal @store2.read('foo'), 'bar'
337
+ end
338
+
339
+ def test_cascade_read_returns_first_hit
340
+ @store1.write('foo', 'bar')
341
+ @store2.expects(:read_entry).never
342
+ assert_equal @cache.read('foo'), 'bar'
343
+ end
344
+
345
+ def test_cascade_read_fallback
346
+ @store1.delete('foo')
347
+ @store2.write('foo', 'bar')
348
+ assert_equal @cache.read('foo'), 'bar'
349
+ end
350
+
351
+ def test_cascade_read_not_found
352
+ assert_equal @cache.read('foo'), nil
353
+ end
354
+
355
+ def test_cascade_delete
356
+ @store1.write('foo', 'bar')
357
+ @store2.write('foo', 'bar')
358
+ @cache.delete('foo')
359
+ assert_equal @store1.read('foo'), nil
360
+ assert_equal @store2.read('foo'), nil
361
+ end
362
+
363
+ def test_cascade_increment_partial_returns_num
364
+ @store2.write('foo', 0)
365
+ assert_equal @cache.increment('foo', 1), 1
366
+ assert_equal @cache.read('foo'), 1
367
+ end
368
+
369
+ def test_cascade_decrement_partial_returns_num
370
+ @store2.write('foo', 1)
371
+ assert_equal @cache.decrement('foo', 1), 0
372
+ assert_equal @cache.read('foo'), 0
373
+ end
374
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activesupport-cascadestore
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jerry Cheung
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-03-06 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: &70233264692760 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70233264692760
25
+ description: write-through cache store that allows you to chain multiple cache stores
26
+ together
27
+ email:
28
+ - jch@whatcodecraves.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - .gitignore
34
+ - Gemfile
35
+ - README.md
36
+ - Rakefile
37
+ - activesupport-cascadestore.gemspec
38
+ - lib/active_support/cache/cascade_store.rb
39
+ - lib/activesupport-cascadestore.rb
40
+ - test/abstract_unit.rb
41
+ - test/caching_test.rb
42
+ homepage: http://github.com/jch/activesupport-cascadestore
43
+ licenses: []
44
+ post_install_message:
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ! '>='
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ requirements: []
61
+ rubyforge_project: activesupport-cascadestore
62
+ rubygems_version: 1.8.10
63
+ signing_key:
64
+ specification_version: 3
65
+ summary: write-through cache store that allows you to chain multiple cache stores
66
+ together
67
+ test_files:
68
+ - test/abstract_unit.rb
69
+ - test/caching_test.rb
70
+ has_rdoc: