Skip to content

Instantly share code, notes, and snippets.

@xaviershay
Created June 12, 2012 00:14
Show Gist options
  • Save xaviershay/2913572 to your computer and use it in GitHub Desktop.
Save xaviershay/2913572 to your computer and use it in GitHub Desktop.
DatabaseCacheStore
# Implementation of Rails.cache backed by our database. We don't have high
# performance or space requirements, so using existing infrastructure for our
# caching needs is desirable.
class DatabaseCacheStore < ActiveSupport::Cache::Store
class CacheEntry < ActiveRecord::Base
end
def self.migrate_up(m)
m.create_table :cache_entries do |t|
t.string :key, :null => false
t.text :value, :null => false
t.datetime :expires_at, :null => false
end
# Because JDBC won't have it any other way
m.execute "ALTER TABLE cache_entries MODIFY COLUMN value mediumtext"
m.add_index :cache_entries, :key, :unique => true
m.add_index :cache_entries, :expires_at
end
def self.migrate_down(m)
m.drop_table :cache_entries if m.table_exists?(:cache_entries)
end
def self.purge
CacheEntry.delete_all
end
def self.trim
CacheEntry.where(['expires_at < ?', Time.now]).delete_all
end
def self.size
CacheEntry.count
end
attr_reader :options
def initialize(options={})
@options = options
end
def expires_in
@expires_in ||= options[:expires_in] || 1.hour
end
protected
def read_entry(key, options)
existing = CacheEntry.find_by_key(key)
if existing && Time.now < existing.expires_at
value = Marshal.load(Base64.decode64(existing.value))
ActiveSupport::Cache::Entry.new(value)
end
end
def write_entry(key, entry, options)
expires = Time.now + options.fetch(:expires_in, expires_in)
value = entry.value
attributes = {
:key => key,
:value => Base64.encode64(Marshal.dump(value)),
:expires_at => expires
}
begin
existing = CacheEntry.find_by_key(key)
if existing
existing.update_attributes(attributes)
else
CacheEntry.create!(attributes)
end
rescue ActiveRecord::RecordNotUnique
retry
end
end
def delete_entry(key, options)
CacheEntry.find_by_key(key).try(:destroy)
end
end
require 'integration_helper'
describe DatabaseCacheStore do
let(:store) { DatabaseCacheStore.new }
it 'round-trips strings to the database' do
store.write('a', 'value')
store.read('a').should == 'value'
end
it 'round-trips hashes to the database' do
store.write('a', {})
store.read('a').should == {}
end
it 'round-trips arrays to the database' do
store.write('a', [])
store.read('a').should == []
end
it 'round-trips ints to the database' do
store.write('a', 1)
store.read('a').should == 1
end
it 'supports expiry' do
store.write('a', 'value', :expires_in => 0.seconds)
store.read('a').should be_nil
store.write('a', 'value', :expires_in => 1.minute)
store.read('a').should_not be_nil
end
it 'supports update' do
store.write('a', 'value')
store.write('a', 'new value')
store.read('a').should == 'new value'
end
it 'can handle missing values' do
store.read('a').should be_nil
store.delete('a') # Should be a noop
store.read('a').should be_nil
end
it 'handles insert race condition' do
# I don't know how to test this properly :(
end
it 'can delete values' do
store.write('a', 'value')
store.delete('a')
store.read('a').should be_nil
end
it 'purges values' do
store.write('a', 'value')
store.class.purge
store.read('a').should be_nil
end
it 'trims values' do
old_size = store.class.size
store.write('a', 'value', :expires_in => -1.seconds)
store.write('b', 'value', :expires_in => 1.minute)
store.class.trim
store.class.size.should == old_size + 1
store.read('a').should be_nil
store.read('b').should == 'value'
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment