Skip to content

Commit

Permalink
added (very) basic delegation which allows the system to fallback to …
Browse files Browse the repository at this point in the history
…AR finders on cache misses
  • Loading branch information
Tobias Lütke committed Jun 8, 2009
1 parent 8800202 commit 1383e82
Show file tree
Hide file tree
Showing 12 changed files with 262 additions and 93 deletions.
56 changes: 29 additions & 27 deletions Rakefile
@@ -1,27 +1,29 @@
require 'rubygems' unless ENV['NO_RUBYGEMS']
%w[rake rake/clean fileutils newgem rubigen].each { |f| require f }
require File.dirname(__FILE__) + '/lib/cached'

# Generate all the Rake tasks
# Run 'rake -T' to see list of generated tasks (from gem root directory)
$hoe = Hoe.new('cached', Cached::VERSION) do |p|
p.developer('Tobias Lütke', 'tobi@leetsoft.com')
p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
p.extra_deps = [
['activesupport','>= 2.3.2'],
]

p.extra_dev_deps = [
['newgem', ">= #{::Newgem::VERSION}"]
]
p.clean_globs |= %w[**/.DS_Store tmp *.log]
path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
p.rsync_args = '-av --delete --ignore-errors'
end

require 'newgem/tasks' # load /tasks/*.rake
Dir['tasks/**/*.rake'].each { |t| load t }

# TODO - want other tests/tasks run by default? Add them to the list
# task :default => [:spec, :features]
BEGIN {$VERBOSE = false}

require 'rubygems' unless ENV['NO_RUBYGEMS']
%w[rake rake/clean fileutils newgem rubigen].each { |f| require f }
require File.dirname(__FILE__) + '/lib/cached'

# Generate all the Rake tasks
# Run 'rake -T' to see list of generated tasks (from gem root directory)
$hoe = Hoe.new('cached', Cached::VERSION) do |p|
p.developer('Tobias Lütke', 'tobi@leetsoft.com')
p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
p.extra_deps = [
['activesupport','>= 2.3.2'],
]

p.extra_dev_deps = [
['newgem', ">= #{::Newgem::VERSION}"]
]
p.clean_globs |= %w[**/.DS_Store tmp *.log]
path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
p.rsync_args = '-av --delete --ignore-errors'
end

require 'newgem/tasks' # load /tasks/*.rake
Dir['tasks/**/*.rake'].each { |t| load t }

# TODO - want other tests/tasks run by default? Add them to the list
# task :default => [:spec, :features]
4 changes: 4 additions & 0 deletions lib/cached.rb
@@ -1,10 +1,14 @@
$:.unshift(File.dirname(__FILE__)) unless
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))

require 'rubygems'
require 'active_support'

module Cached
VERSION = '0.5.0'

mattr_accessor :store
self.store = ActiveSupport::Cache.lookup_store(:memory_store)
end


Expand Down
10 changes: 7 additions & 3 deletions lib/cached/config.rb
@@ -1,14 +1,18 @@
module Cached
class Config
attr_accessor :indexes, :class_name, :primary_key
attr_accessor :indexes, :class_name, :primary_key, :delegates

def initialize(class_name, primary_key)
@indexes, @class_name, @primary_key = [], class_name.to_s, primary_key.to_s
@delegates, @indexes, @class_name, @primary_key = [], [], class_name.to_s, primary_key.to_s
end

def index(*args)
@indexes.push [args].flatten
@indexes.push args.flatten
end

def delegate_to(*methods)
@delegates += methods.flatten
end

end
end
57 changes: 40 additions & 17 deletions lib/cached/config_compiler.rb
Expand Up @@ -7,7 +7,7 @@ def initialize(config)
end

def to_ruby
[compiled_meta_methods, compiled_save_method, compiled_fetch_methods].join
[compiled_meta_methods, compiled_save_methods, compiled_fetch_methods].join
end

def compiled_meta_methods
Expand All @@ -23,35 +23,58 @@ def compiled_meta_methods
"def self.object_cache_hash(*args); args.join.hash; end;"
end

def compiled_save_method
lines = ['k = object_cache_key', "Cached.store.write(k, self)"]

@config.indexes.each do |index|
index_name = index_name(index)
cache_key = index_cache_key(index)

lines.push "Cached.store.write(#{cache_key}, k)"
end
def compiled_save_methods
compiled_save_object_method + compiled_save_index_method
end

def compiled_save_object_method
"def save_object_to_cache;" +
"Cached.store.write(object_cache_key, self);" +
"end;" +

"def save_to_cache;" +
lines.join(';') +
"def expire_object_in_cache;" +
"Cached.store.delete(object_cache_key);" +
"end;"
end

def compiled_save_index_method

keys = @config.indexes.collect { |index| index_cache_key(index) }

"def save_indexes_to_cache;" +
"v = #{@config.primary_key};" +
keys.collect{|k| "Cached.store.write(#{k}, v);"}.join +
"end;" +

"def expire_indexes_in_cache;" +
keys.collect{|k| "Cached.store.delete(#{k});"}.join +
"end;"
end

def compiled_fetch_method_for(index)
index_name = index_name(index)
cache_key = index_cache_key(index)

"def self.lookup_by_#{index_name}(#{index.join(', ')});" +
method_suffix_and_parameters = "#{index_name}(#{index.join(', ')})"

delegates = @config.delegates.collect { |delegate| "#{delegate}_by_#{method_suffix_and_parameters}" }

"def self.lookup_by_#{method_suffix_and_parameters};" +
" key = Cached.store.read(#{cache_key}); "+
" key ? Cached.store.read(key): nil;" +
#}" key ? Cached.store.read(key): Cached.store.fetch(key) { #{ delegates.join(' || ') } } ;" +
" key ? lookup(key): nil;" +
"end;"
end

def compiled_fetch_method_for_primary_key
"def self.lookup(pk);" +
" Cached.store.read(\"#\{object_cache_prefix}:#\{pk}\");"+
"end;" +

delegation = @config.delegates.collect{|c| "|| #{c}(pk)" }.join

"def self.lookup(pk);" +
" Cached.store.fetch(\"#\{object_cache_prefix}:#\{pk}\") { nil #{delegation} };" +
"end;" +


"def self.lookup_by_#{@config.primary_key}(pk);" +
" lookup(pk); "+
"end;"
Expand Down
17 changes: 7 additions & 10 deletions lib/cached/model.rb
@@ -1,29 +1,26 @@
module Cached
module Cached

module Model

module ClassMethods
def cache_by_key(primary_key, &block)

config = Config.new(name.underscore.downcase, primary_key)
config.instance_eval(&block)

self.class_eval ConfigCompiler.new(config).to_ruby

self.class_eval "def self.hello; 'hi'; end"
end
self.class_eval ConfigCompiler.new(config).to_ruby, __FILE__, __LINE__
end
end

def self.included(base)
base.extend ClassMethods
end



def save_to_cache
Cached.store.write(object_cache_key, self)
save_object_to_cache
save_indexes_to_cache
end



end


Expand Down
25 changes: 25 additions & 0 deletions lib/cached/record.rb
@@ -0,0 +1,25 @@
module Cached
module Record

def self.included?(base)

if base.respond_to?(:save)
base.alias_method_chain :save, :cached
end

end


def save_with_cached
save_without_cached

# expire the cache, the object will be stored in it's prestine form
# after the next lookup call hopefull.
expire_object_in_cache

# update the indexes for the new values.
store_indexes_in_cache
end

end
end
12 changes: 12 additions & 0 deletions test/people_database.rb
@@ -0,0 +1,12 @@
require 'active_record'

ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => '/tmp/people.db')

ActiveRecord::Schema.define do

create_table :people, :force => true do |t|
t.column :first_name, :string
t.column :last_name, :string
end
end

66 changes: 36 additions & 30 deletions test/test_cached.rb
@@ -1,9 +1,5 @@
require File.dirname(__FILE__) + '/test_helper.rb'

require 'active_support/cache'

Cached.store = ActiveSupport::Cache.lookup_store(:memory_store)

class Product < Struct.new(:id, :name, :price, :vendor)
include Cached::Model

Expand All @@ -14,66 +10,76 @@ class Product < Struct.new(:id, :name, :price, :vendor)

end


class TestCached < Test::Unit::TestCase

def setup
@product = Product.new(1, 'ipod', 149.00, 'apple')
end

test "product can be stored to cache" do
@product.respond_to?(:save_to_cache)
end
context "cache storage" do

test "product stores meta data in instance methods" do
assert_equal "id", @product.object_cache_primary_key
end
test "product can be stored to cache" do
@product.respond_to?(:save_to_cache)
end

test "product has efficient object_cache_key instance method" do
assert_equal "product:1", @product.object_cache_key
end
test "product stores meta data in instance methods" do
assert_equal "id", @product.object_cache_primary_key
end

test "product stores itself to memcached on save_to_cache call" do
assert @product.save_to_cache
assert_equal @product, Cached.store.read('product:1')
end
test "product has efficient object_cache_key instance method" do
assert_equal "product:1", @product.object_cache_key
end

test "product stores defined indexes as backreference to product key" do
assert @product.save_to_cache
assert_equal 'product:1', Cached.store.read("product/name:#{'ipod'.hash}")
assert_equal 'product:1', Cached.store.read("product/vendor_and_name:#{'appleipod'.hash}")
end
test "product stores itself to memcached on save_to_cache call" do
assert @product.save_to_cache
assert_equal @product, Cached.store.read('product:1')
end

test "product stores defined indexes as backreference to product key" do
assert @product.save_to_cache
assert_equal 1, Cached.store.read("product/name:#{hash('ipod')}")
assert_equal 1, Cached.store.read("product/vendor_and_name:#{hash('appleipod')}")
end

end

context "lookups" do

test "product explicit lookup by primary_key" do
@product.save_to_cache
Cached.store.expects(:read).with('product:1')
Cached.store.expects(:read).with('product:1', {})
Product.lookup_by_id(1)
end

test "product lookup by primary_key" do
@product.save_to_cache
Cached.store.expects(:read).with('product:1')
Cached.store.expects(:read).with('product:1', {})
Product.lookup(1)
end

test "product lookup by index" do
@product.save_to_cache
Cached.store.expects(:read).with("product/name:#{'ipod'.hash}").returns('product:1')
Cached.store.expects(:read).with('product:1')
Cached.store.expects(:read).with("product/name:#{hash('ipod')}").returns(1)
Cached.store.expects(:read).with('product:1', {})
Product.lookup_by_name('ipod')
end

test "product lookup by multi index" do
@product.save_to_cache
Cached.store.expects(:read).with("product/vendor_and_name:#{'appleipod'.hash}").returns('product:1')
Cached.store.expects(:read).with('product:1')
Cached.store.expects(:read).with("product/vendor_and_name:#{hash('appleipod')}").returns(1)
Cached.store.expects(:read).with('product:1', {})
Product.lookup_by_vendor_and_name('apple', 'ipod')
end

end


private

def hash(text)
text.hash
end

end


0 comments on commit 1383e82

Please sign in to comment.