Permalink
Browse files

Initial sources

git-svn-id: http://svn.verbdev.com/rb/caches.rb/trunk@2 f28541f9-331e-0410-82d8-e175c77b79a1
  • Loading branch information...
1 parent e1daad9 commit 9a8974ab25cc04f49746a4f456d3a136a069acf9 yrashk committed Sep 24, 2006
Showing with 316 additions and 0 deletions.
  1. +24 −0 README
  2. +22 −0 Rakefile
  3. +1 −0 init.rb
  4. +68 −0 lib/caches.rb
  5. +178 −0 test/caches_test.rb
  6. +23 −0 test/time_mock.rb
View
24 README
@@ -0,0 +1,24 @@
+Caches.rb
+=========
+
+Caches.rb is a simplistic method cache for Ruby
+
+Simply do `BaseClass.extend Caches' and you will be able to specify
+
+caches :method_name
+or
+caches :method_name, :timeout => 2.minutes
+
+Default timeout is 60 seconds.
+
+What is important is that this solution caches calls with arguments. For example, if you will make a method_name(“Hello”) call you’ll get next method_name(“Hello”) cached, but method_name(“Bye”) will not be cached.
+
+Also you will be able to invalidate cache explicitly with invalidate_method_name_cache and invalidate_all_method_name_caches calls.
+
+Also you can invalidate caches with
+
+invalidate_all_caches
+or
+invalidate_all_caches :except => :name
+or
+invalidate_all_caches :except => [:name, :name1]
View
@@ -0,0 +1,22 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the request_routing plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the Caches.rb plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'Caches.rb'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
View
@@ -0,0 +1 @@
+require 'caches'
View
@@ -0,0 +1,68 @@
+class String
+ def starts_with?(prefix)
+ prefix = prefix.to_s
+ self[0, prefix.length] == prefix
+ end
+end
+
+module Caches
+ def caches(name,options = { :timeout => 60})
+ sanitized_name = name.to_s.delete('?')
+ saved_getter = "getter_#{name}"
+ saved_setter = "setter_#{name}"
+ setter = "#{name}="
+
+ has_setter = new.respond_to? setter.to_sym
+
+ module_eval "alias #{saved_getter} #{name}"
+ module_eval "alias #{saved_setter} #{setter.to_sym}" if has_setter
+ module_eval do
+ define_method("invalidate_all_caches") do |*opts|
+ unless opts.empty?
+ opthash = opts.first
+ except = opthash[:except]
+ if except
+ except = [except] unless except.kind_of? Enumerable
+ @propcache.each_pair do |k,v|
+ @propcache[k] = nil unless except.any? {|exception| k.starts_with?(exception.to_s)}
+ end
+ end
+ else
+ @propcache = {}
+ end
+ end
+ define_method("invalidate_#{sanitized_name}_cache") do |*args|
+ @propcache ||= {}
+ key = name.to_s+args.hash.to_s
+ @propcache[key] = nil
+ end
+ define_method("invalidate_all_#{sanitized_name}_caches") do
+ @propcache ||= {}
+ key = name.to_s
+ @propcache.keys.each {|k| @propcache[k] = nil if k.starts_with?(key) }
+ end
+ define_method("#{name}") do |*args| # FIXME: this implementation smells bad
+ @propcache ||= {}
+ key = name.to_s+args.hash.to_s
+ cached = @propcache[key]
+ unless cached
+ @propcache[key] = { :value => self.send(saved_getter.to_sym,*args), :expires_at => Time.now.to_i + options[:timeout]}
+ return @propcache[key][:value]
+ else
+ unless Time.now.to_i > cached[:expires_at]
+ cached[:value]
+ else
+ self.send "invalidate_#{sanitized_name}_cache".to_sym, *args
+ self.send name.to_sym, *args
+ end
+ end
+ end
+ if has_setter
+ define_method("#{setter}") do |new_val|
+ self.send "invalidate_#{sanitized_name}_cache".to_sym
+ self.send saved_setter.to_sym, new_val
+ end
+ end
+ end
+ end
+end
View
@@ -0,0 +1,178 @@
+require 'test/unit'
+require 'test/time_mock'
+
+require File.dirname(__FILE__) + "/../init"
+
+class CachedBase ; end ; CachedBase.extend Caches
+
+
+class CachedClass < CachedBase
+
+ attr_reader :accessor_counter
+
+ def initialize
+ @accessor = "Value"
+ @accessor_counter = 0
+ end
+
+ def accessor
+ @accessor_counter += 1
+ @accessor
+ end
+
+ def accessor=(new_val)
+ @accessor = new_val
+ end
+
+ alias :accessor2 :accessor
+
+ caches :accessor
+ caches :accessor2, :timeout => 120
+
+ def method_with_args(a,b)
+ "#{a}#{b}#{Time.now}"
+ end
+
+ caches :method_with_args
+
+end
+
+class CachesTest < Test::Unit::TestCase
+
+ def setup
+ @cached = CachedClass.new
+ end
+
+ def teardwon
+ Time.forced_now_time = nil
+ end
+
+ def test_default_timeout
+ val = @cached.accessor
+ assert_equal 1, @cached.accessor_counter
+ Time.forced_now_time = Time.now
+ 10.times { @cached.accessor }
+ assert_equal 1, @cached.accessor_counter
+ # In 60 seconds, we still cache
+ Time.forced_now_time = Time.now + 60
+ val = @cached.accessor
+ assert_equal 1, @cached.accessor_counter
+ # In 61, cache is invalidated
+ Time.forced_now_time = Time.now + 61
+ val = @cached.accessor
+ assert_equal 2, @cached.accessor_counter
+ end
+
+ def test_not_default_timeout
+ val = @cached.accessor2
+ assert_equal 1, @cached.accessor_counter
+ Time.forced_now_time = Time.now
+ 10.times { @cached.accessor2 }
+ assert_equal 1, @cached.accessor_counter
+ # In 2 minutes, we still cache
+ Time.forced_now_time = Time.now + 120
+ val = @cached.accessor2
+ assert_equal 1, @cached.accessor_counter
+ # In 2m1sec, cache is invalidated
+ Time.forced_now_time = Time.now + 121
+ val = @cached.accessor2
+ assert_equal 2, @cached.accessor_counter
+ end
+
+ def test_invalidate_accessor_by_assignment
+ val = @cached.accessor
+ assert_equal 1, @cached.accessor_counter
+
+ @cached.accessor = "Hello"
+ val = @cached.accessor
+ assert_equal 2, @cached.accessor_counter
+ end
+
+ def test_invalidate_accessor_explicitely
+ val = @cached.accessor
+ assert_equal 1, @cached.accessor_counter
+
+ @cached.invalidate_accessor_cache
+ val = @cached.accessor
+ assert_equal 2, @cached.accessor_counter
+
+ @cached.invalidate_all_accessor_caches
+ val = @cached.accessor
+ assert_equal 3, @cached.accessor_counter
+
+ @cached.invalidate_all_caches
+ val = @cached.accessor
+ assert_equal 4, @cached.accessor_counter
+ end
+
+ def test_caches_method_with_args
+ val = @cached.method_with_args("a","b")
+ valA = @cached.method_with_args("a","C")
+ assert !(val==valA)
+ Time.forced_now_time = Time.now + 1
+ val1 = @cached.method_with_args("a","b")
+ Time.forced_now_time = nil
+ assert_equal val, val1
+ end
+
+
+ def test_invalidate_method_with_args_explicitely
+ # Invalidating with another arguments does not work:
+ val = @cached.method_with_args("a","b")
+ @cached.invalidate_method_with_args_cache("A","B")
+ Time.forced_now_time = Time.now + 1
+ val1 = @cached.method_with_args("a","b")
+ Time.forced_now_time = nil
+ assert (val==val1)
+
+ @cached.invalidate_all_caches
+
+ val = @cached.method_with_args("a","b")
+ @cached.invalidate_method_with_args_cache("a","b")
+ Time.forced_now_time = Time.now + 1
+ val1 = @cached.method_with_args("a","b")
+ Time.forced_now_time = nil
+ assert !(val==val1)
+
+ @cached.invalidate_all_caches
+
+ val = @cached.method_with_args("a","b")
+ @cached.invalidate_all_method_with_args_caches
+ Time.forced_now_time = Time.now + 1
+ val1 = @cached.method_with_args("a","b")
+ Time.forced_now_time = nil
+ assert !(val==val1)
+
+ @cached.invalidate_all_caches
+
+ val = @cached.method_with_args("a","b")
+ @cached.invalidate_all_caches
+ Time.forced_now_time = Time.now + 1
+ val1 = @cached.method_with_args("a","b")
+ Time.forced_now_time = nil
+ assert !(val==val1)
+ end
+
+ def test_invalidate_all_caches_except
+ val = @cached.accessor
+ val2 = @cached.accessor2
+ valM = @cached.method_with_args("a","b")
+ @cached.invalidate_all_caches :except => :method_with_args
+ Time.forced_now_time = Time.now + 1
+ valM1 = @cached.method_with_args("a","b")
+ Time.forced_now_time = nil
+ assert (valM==valM1)
+
+ @cached.invalidate_all_caches
+
+ val = @cached.accessor
+ val2 = @cached.accessor2
+ valM = @cached.method_with_args("a","b")
+ @cached.invalidate_all_caches :except => [:method_with_args]
+ Time.forced_now_time = Time.now + 1
+ valM1 = @cached.method_with_args("a","b")
+ Time.forced_now_time = nil
+ assert (valM==valM1)
+ end
+
+end
View
@@ -0,0 +1,23 @@
+class Time
+ @@forced_now_time = nil
+
+ def self.forced_now_time
+ @@forced_now_time
+ end
+
+ def self.forced_now_time=(time)
+ @@forced_now_time = time
+ end
+
+ class << self
+ def now_with_forcing
+ if @@forced_now_time
+ @@forced_now_time
+ else
+ now_without_forcing
+ end
+ end
+ alias_method :now_without_forcing, :now
+ alias_method :now, :now_with_forcing
+ end
+end

0 comments on commit 9a8974a

Please sign in to comment.