diff --git a/README.md b/README.md index 060eb45..a286738 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ cache = LruRedux::ThreadSafeCache.new(100) see: benchmark directory (a million random lookup / store) +#### LRU ``` $ ruby ./bench/bench.rb Rehearsal --------------------------------------------------------- @@ -88,6 +89,22 @@ lru_redux thread safe 2.190000 0.010000 2.200000 ( 2.185512) ``` +#### TTL +``` +$ ruby ./bench/bench_ttl.rb +Rehearsal ----------------------------------------------------------------------- +FastCache 6.240000 0.070000 6.310000 ( 6.302569) +LruRedux::TTL::Cache 4.700000 0.010000 4.710000 ( 4.712858) +LruRedux::TTL::ThreadSafeCache 6.300000 0.010000 6.310000 ( 6.319032) +LruRedux::TTL::Cache (TTL disabled) 2.460000 0.010000 2.470000 ( 2.470629) +------------------------------------------------------------- total: 19.800000sec + + user system total real +FastCache 6.470000 0.070000 6.540000 ( 6.536193) +LruRedux::TTL::Cache 4.640000 0.010000 4.650000 ( 4.661793) +LruRedux::TTL::ThreadSafeCache 6.310000 0.020000 6.330000 ( 6.328840) +LruRedux::TTL::Cache (TTL disabled) 2.440000 0.000000 2.440000 ( 2.446269) +``` ## Contributing @@ -98,6 +115,10 @@ lru_redux thread safe 2.190000 0.010000 2.200000 ( 2.185512) 5. Create new Pull Request ## Changlog +###version NEXT - TBD + +- New: TTL cache added. This cache is LRU like with the addition of time-based eviction. Check the TTL section in README.md for details. + ###version 1.0.0 - 26-Mar-2015 - Ruby Support: Ruby 1.9+ is now required by LruRedux. If you need to use LruRedux in Ruby 1.8, please specify gem version 0.8.4 in your Gemfile. v0.8.4 is the last 1.8 compatible release and included a number of fixes and performance improvements for the Ruby 1.8 implementation. @Seberius diff --git a/Rakefile b/Rakefile index af5ab75..0811fba 100644 --- a/Rakefile +++ b/Rakefile @@ -3,5 +3,5 @@ require "bundler/gem_tasks" require 'rake/testtask' Rake::TestTask.new do |t| - t.pattern = "test/*_test.rb" + t.pattern = "test/**/*_test.rb" end diff --git a/bench/bench.rb b/bench/bench.rb index 2afe0d1..a87cdb1 100644 --- a/bench/bench.rb +++ b/bench/bench.rb @@ -1,45 +1,43 @@ require 'bundler' -require 'lru' require 'benchmark' +require 'lru' require 'lru_cache' require 'threadsafe-lru' Bundler.require -lru = Cache::LRU.new(:max_elements => 1_000) +# Lru +lru = Cache::LRU.new(max_elements: 1_000) + +# LruCache lru_cache = LRUCache.new(1_000) -lru_redux = LruRedux::Cache.new(1_000) -lru_redux_thread_safe = LruRedux::ThreadSafeCache.new(1_000) +# ThreadSafeLru thread_safe_lru = ThreadSafeLru::LruCache.new(1_000) +# LruRedux +redux = LruRedux::Cache.new(1_000) +redux_thread_safe = LruRedux::ThreadSafeCache.new(1_000) + +puts "** LRU Benchmarks **" Benchmark.bmbm do |bm| + bm.report 'ThreadSafeLru' do + 1_000_000.times { thread_safe_lru.get(rand(2_000)) { :value } } + end + + bm.report 'LRU' do + 1_000_000.times { lru[rand(2_000)] ||= :value } + end - bm.report "thread safe lru" do - 1_000_000.times do - thread_safe_lru.get(rand(2_000)){ :value } - end + bm.report 'LRUCache' do + 1_000_000.times { lru_cache[rand(2_000)] ||= :value } end - [ - [lru, "lru gem"], - [lru_cache, "lru_cache gem"], - ].each do |cache, name| - bm.report name do - 1_000_000.times do - cache[rand(2_000)] ||= :value - end - end + bm.report 'LruRedux::Cache' do + 1_000_000.times { redux.getset(rand(2_000)) { :value } } end - [ - [lru_redux, "lru_redux gem"], - [lru_redux_thread_safe, "lru_redux thread safe"] - ].each do |cache, name| - bm.report name do - 1_000_000.times do - cache.getset(rand(2_000)) { :value } - end - end + bm.report 'LruRedux::ThreadSafeCache' do + 1_000_000.times { redux_thread_safe.getset(rand(2_000)) { :value } } end end diff --git a/bench/bench_ttl.rb b/bench/bench_ttl.rb new file mode 100644 index 0000000..c6fc474 --- /dev/null +++ b/bench/bench_ttl.rb @@ -0,0 +1,33 @@ +require 'bundler' +require 'benchmark' +require 'fast_cache' + +Bundler.require + +# FastCache +fast_cache = FastCache::Cache.new(1_000, 5 * 60) + +# LruRedux +redux_ttl = LruRedux::TTL::Cache.new(1_000, 5 * 60) +redux_ttl_thread_safe = LruRedux::TTL::ThreadSafeCache.new(1_000, 5 * 60) +redux_ttl_disabled = LruRedux::TTL::Cache.new(1_000, :none) + +puts +puts "** TTL Benchmarks **" +Benchmark.bmbm do |bm| + bm.report 'FastCache' do + 1_000_000.times { fast_cache.fetch(rand(2_000)) { :value } } + end + + bm.report 'LruRedux::TTL::Cache' do + 1_000_000.times { redux_ttl.getset(rand(2_000)) { :value } } + end + + bm.report 'LruRedux::TTL::ThreadSafeCache' do + 1_000_000.times { redux_ttl_thread_safe.getset(rand(2_000)) { :value } } + end + + bm.report 'LruRedux::TTL::Cache (TTL disabled)' do + 1_000_000.times { redux_ttl_disabled.getset(rand(2_000)) { :value } } + end +end \ No newline at end of file diff --git a/lib/lru_redux/cache.rb b/lib/lru_redux/cache.rb index e458458..8167983 100644 --- a/lib/lru_redux/cache.rb +++ b/lib/lru_redux/cache.rb @@ -5,7 +5,7 @@ class LruRedux::Cache def initialize(*args) max_size, _ = args - raise ArgumentError.new(:max_size) if @max_size < 1 + raise ArgumentError.new(:max_size) if max_size < 1 @max_size = max_size @data = {} @@ -14,7 +14,7 @@ def initialize(*args) def max_size=(max_size) max_size ||= @max_size - raise ArgumentError.new(:max_size) if @max_size < 1 + raise ArgumentError.new(:max_size) if max_size < 1 @max_size = max_size diff --git a/lib/lru_redux/ttl/cache.rb b/lib/lru_redux/ttl/cache.rb index b711ec7..1128ee5 100644 --- a/lib/lru_redux/ttl/cache.rb +++ b/lib/lru_redux/ttl/cache.rb @@ -1,6 +1,8 @@ module LruRedux module TTL class Cache + attr_reader :max_size, :ttl + def initialize(*args) max_size, ttl = args @@ -48,10 +50,10 @@ def getset(key) @data_lru[key] = value else result = @data_lru[key] = yield - @data_ttl = Time.now + @data_ttl[key] = Time.now.to_f if @data_lru.size > @max_size - key, _ = @data_lru.tail + key, _ = @data_lru.first @data_ttl.delete(key) @data_lru.delete(key) @@ -92,10 +94,10 @@ def []=(key, val) @data_ttl.delete(key) @data_lru[key] = val - @data_ttl[key] = Time.now + @data_ttl[key] = Time.now.to_f if @data_lru.size > @max_size - key, _ = @data_lru.tail + key, _ = @data_lru.first @data_ttl.delete(key) @data_lru.delete(key) @@ -145,6 +147,10 @@ def clear @data_ttl.clear end + def expire + ttl_evict + end + def count @data_lru.size end @@ -159,14 +165,14 @@ def valid? def ttl_evict return if @ttl == :none - ttl_horizon = Time.now - @ttl - key, time = @data_ttl.tail + ttl_horizon = Time.now.to_f - @ttl + key, time = @data_ttl.first until time.nil? || time > ttl_horizon @data_ttl.delete(key) @data_lru.delete(key) - key, time = @data_ttl.tail + key, time = @data_ttl.first end end @@ -174,7 +180,7 @@ def resize ttl_evict while @data_lru.size > @max_size - key, _ = @data_lru.tail + key, _ = @data_lru.first @data_ttl.delete(key) @data_lru.delete(key) diff --git a/lru_redux.gemspec b/lru_redux.gemspec index 37e2e4c..852b670 100644 --- a/lru_redux.gemspec +++ b/lru_redux.gemspec @@ -6,8 +6,8 @@ require 'lru_redux/version' Gem::Specification.new do |spec| spec.name = "lru_redux" spec.version = LruRedux::VERSION - spec.authors = ["Sam Saffron"] - spec.email = ["sam.saffron@gmail.com"] + spec.authors = ["Sam Saffron", "Kaijah Hougham"] + spec.email = ["sam.saffron@gmail.com", "github@seberius.com"] spec.description = %q{An efficient implementation of an lru cache} spec.summary = %q{An efficient implementation of an lru cache} spec.homepage = "https://github.com/SamSaffron/lru_redux" @@ -26,4 +26,5 @@ Gem::Specification.new do |spec| spec.add_development_dependency "guard-minitest" spec.add_development_dependency "guard" spec.add_development_dependency "rb-inotify" + spec.add_development_dependency "timecop", "~> 0.7" end diff --git a/test/ttl/cache_test.rb b/test/ttl/cache_test.rb new file mode 100644 index 0000000..f2bfca0 --- /dev/null +++ b/test/ttl/cache_test.rb @@ -0,0 +1,90 @@ +require 'timecop' + +class TTLCacheTest < CacheTest + def setup + Timecop.freeze(Time.now) + @c = LruRedux::TTL::Cache.new 3, 5 * 60 + end + + def teardown + Timecop.return + end + + def test_ttl + assert_equal 300, @c.ttl + + @c.ttl = 10 * 60 + + assert_equal 600, @c.ttl + end + + # TTL tests using Timecop + def test_ttl_eviction_on_access + @c[:a] = 1 + @c[:b] = 2 + + Timecop.freeze(Time.now + 330) + + @c[:c] = 3 + + assert_equal([[:c, 3]], @c.to_a) + end + + def test_ttl_eviction_on_expire + @c[:a] = 1 + @c[:b] = 2 + + Timecop.freeze(Time.now + 330) + + @c.expire + + assert_equal([], @c.to_a) + end + + def test_ttl_eviction_on_new_max_size + @c[:a] = 1 + @c[:b] = 2 + + Timecop.freeze(Time.now + 330) + + @c.max_size = 10 + + assert_equal([], @c.to_a) + end + + def test_ttl_eviction_on_new_ttl + @c[:a] = 1 + @c[:b] = 2 + + Timecop.freeze(Time.now + 330) + + @c.ttl = 10 * 60 + + assert_equal([[:b, 2], [:a, 1]], @c.to_a) + + @c.ttl = 2 * 60 + + assert_equal([], @c.to_a) + end + + def test_ttl_precedence_over_lru + @c[:a] = 1 + + Timecop.freeze(Time.now + 60) + + @c[:b] = 2 + @c[:c] = 3 + + @c[:a] + + assert_equal [[:a, 1], [:c, 3], [:b, 2]], + @c.to_a + + Timecop.freeze(Time.now + 270) + + @c[:d] = 4 + + assert_equal [[:d, 4], [:c, 3], [:b, 2]], + @c.to_a + end +end \ No newline at end of file diff --git a/test/ttl/thread_safe_cache_test.rb b/test/ttl/thread_safe_cache_test.rb new file mode 100644 index 0000000..0aaafc3 --- /dev/null +++ b/test/ttl/thread_safe_cache_test.rb @@ -0,0 +1,20 @@ +class TTLThreadSafeCacheTest < TTLCacheTest + def setup + Timecop.freeze(Time.now) + @c = LruRedux::TTL::ThreadSafeCache.new 3, 5 * 60 + end + + def teardown + Timecop.return + end + + def test_recursion + @c[:a] = 1 + @c[:b] = 2 + + # should not blow up + @c.each do |k, _| + @c[k] + end + end +end \ No newline at end of file