activesupport-cascadestore 0.0.1

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 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: