Skip to content

Commit

Permalink
initial commit.
Browse files Browse the repository at this point in the history
ExpiringMemoryStore inherits from Store, not from MemoryStore
(hopefully that's alright)
  • Loading branch information
matthewrudy committed Dec 5, 2008
0 parents commit c1c2dea
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 0 deletions.
20 changes: 20 additions & 0 deletions MIT-LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Copyright (c) 2008 [Matthew Rudy Jacobs]

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28 changes: 28 additions & 0 deletions README
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
ExpiringMemoryStore
===================

Expiring MemoryStore is a modification to the default Rails MemoryStore which adds the :expires_in functionality that is so useful with Memcache.
Although this increases the memory overhead slightly,
it ensures that caches can be expired easily, without restarting your webserver.

Just add;
config.cache_store = :expiring_memory_store
to your environment.rb.

Example
=======

>> Rails.cache.write('are you there?', :im_still_here, :expires_in => 30)
=> :im_still_here

>> Rails.cache.read('are you there?')
=> :im_still_here

wait 30 seconds

>> Rails.cache.read('are you there?')
=> nil

WICKED!!!

Copyright (c) 2008 [Matthew Rudy Jacobs], released under the MIT license
22 changes: 22 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'

desc 'Default: run unit tests.'
task :default => :test

desc 'Test the expiring_memory_store plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end

desc 'Generate documentation for the expiring_memory_store plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'ExpiringMemoryStore'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end
1 change: 1 addition & 0 deletions init.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require 'active_support/cache/expiring_memory_store'
55 changes: 55 additions & 0 deletions lib/active_support/cache/expiring_memory_store.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
require 'activesupport'
module ActiveSupport
module Cache
# Like MemoryStore, but caches are expired after the period specified in the :expires_in option.
class ExpiringMemoryStore < Store
def initialize
@data = {}
end

def read(name, options = nil)
super
value, expiry = @data[name]
if expiry && expiry < Time.now
delete(name)
return nil
end
return value
end

def write(name, value, options = nil)
super
@data[name] = [value.freeze, expires_at(options)].freeze
return value
end

def delete(name, options = nil)
super
@data.delete(name)
end

def delete_matched(matcher, options = nil)
super
@data.delete_if { |k,v| k =~ matcher }
end

def exist?(name,options = nil)
super
@data.has_key?(name)
end

def clear
@data.clear
end

private

def expires_at(options)
if expires_in = options && options[:expires_in]
return expires_in.from_now if expires_in != 0
end
return nil
end
end
end
end
127 changes: 127 additions & 0 deletions test/expiring_memory_store_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
require 'rubygems'
require 'test/unit'
require 'mocha'
require File.dirname(__FILE__) + '/../init'

class CacheStoreTest < Test::Unit::TestCase
def setup
@cache = ActiveSupport::Cache.lookup_store(:expiring_memory_store)
end

def test_fetch_without_cache_miss
@cache.stubs(:read).with('foo', {}).returns('bar')
@cache.expects(:write).never
assert_equal 'bar', @cache.fetch('foo') { 'baz' }
end

def test_fetch_with_cache_miss
@cache.stubs(:read).with('foo', {}).returns(nil)
@cache.expects(:write).with('foo', 'baz', {})
assert_equal 'baz', @cache.fetch('foo') { 'baz' }
end

def test_fetch_with_forced_cache_miss
@cache.expects(:read).never
@cache.expects(:write).with('foo', 'bar', :force => true)
@cache.fetch('foo', :force => true) { 'bar' }
end
end

# Tests the base functionality that should be identical across all cache stores.
module CacheStoreBehavior
def test_should_read_and_write_strings
@cache.write('foo', 'bar')
assert_equal 'bar', @cache.read('foo')
end

def test_should_read_and_write_hash
@cache.write('foo', {:a => "b"})
assert_equal({:a => "b"}, @cache.read('foo'))
end

def test_should_read_and_write_nil
@cache.write('foo', nil)
assert_equal nil, @cache.read('foo')
end

def test_fetch_without_cache_miss
@cache.write('foo', 'bar')
assert_equal 'bar', @cache.fetch('foo') { 'baz' }
end

def test_fetch_with_cache_miss
assert_equal 'baz', @cache.fetch('foo') { 'baz' }
end

def test_fetch_with_forced_cache_miss
@cache.fetch('foo', :force => true) { 'bar' }
end

def test_increment
@cache.write('foo', 1, :raw => true)
assert_equal 1, @cache.read('foo', :raw => true).to_i
assert_equal 2, @cache.increment('foo')
assert_equal 2, @cache.read('foo', :raw => true).to_i
assert_equal 3, @cache.increment('foo')
assert_equal 3, @cache.read('foo', :raw => true).to_i
end

def test_decrement
@cache.write('foo', 3, :raw => true)
assert_equal 3, @cache.read('foo', :raw => true).to_i
assert_equal 2, @cache.decrement('foo')
assert_equal 2, @cache.read('foo', :raw => true).to_i
assert_equal 1, @cache.decrement('foo')
assert_equal 1, @cache.read('foo', :raw => true).to_i
end
end

class ExpiringMemoryStoreTest < Test::Unit::TestCase
def setup
@cache = ActiveSupport::Cache.lookup_store(:expiring_memory_store)
end

include CacheStoreBehavior

def test_store_objects_should_be_immutable
@cache.write('foo', 'bar')
assert_raise(ActiveSupport::FrozenObjectError) { @cache.read('foo').gsub!(/.*/, 'baz') }
assert_equal 'bar', @cache.read('foo')
end

def test_by_default_values_should_not_expire
time_now = Time.now
Time.stubs(:now).returns(time_now)

@cache.write('foo', 'bar')
assert_equal 'bar', @cache.read('foo')

Time.stubs(:now).returns(time_now + 5.years )
assert_equal 'bar', @cache.read('foo')
end

def test_values_should_expire_is_param_is_set
time_now = Time.now
Time.stubs(:now).returns(time_now)

@cache.write('foo', 'bar', :expires_in => 1.year)
assert_equal 'bar', @cache.read('foo')

Time.stubs(:now).returns(time_now + 5.years )
assert_equal nil, @cache.read('foo')
end

def test_values_should_expire_when_the_param_tells_it_to
time_now = Time.now
Time.stubs(:now).returns(time_now)

@cache.write('foo', 'bar', :expires_in => 1.year)
assert_equal 'bar', @cache.read('foo')

Time.stubs(:now).returns(time_now + 1.year - 1 )
assert_equal 'bar', @cache.read('foo')

Time.stubs(:now).returns(time_now + 1.year + 1 )
assert_equal nil, @cache.read('foo')
end
end

0 comments on commit c1c2dea

Please sign in to comment.